Phase 0: models.py → models/ Package aufgeteilt
models.py (3.496 Zeilen) in 6 Domain-Module aufgeteilt: - system.py: CSVImport, ApplicationPermission, AuditLog, BackupJob, AppConfiguration, HelpBox - land.py: Paechter, Land, LandVerpachtung, LandAbrechnung, DokumentLink - finanzen.py: Rentmeister, StiftungsKonto, BankTransaction, Verwaltungskosten - destinataere.py: Destinataer, Person, Foerderung, DestinataerUnterstuetzung, UnterstuetzungWiederkehrend, DestinataerNotiz, VierteljahresNachweis, DestinataerEmailEingang - veranstaltungen.py: BriefVorlage, Veranstaltung, Veranstaltungsteilnehmer - geschichte.py: GeschichteSeite, GeschichteBild, StiftungsKalenderEintrag __init__.py re-exportiert alle Models für volle Rückwärtskompatibilität. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
49
app/stiftung/models/__init__.py
Normal file
49
app/stiftung/models/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# models/ package – re-exports all models for backward compatibility
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
from .system import ( # noqa: F401
|
||||
AppConfiguration,
|
||||
ApplicationPermission,
|
||||
AuditLog,
|
||||
BackupJob,
|
||||
CSVImport,
|
||||
HelpBox,
|
||||
)
|
||||
|
||||
from .land import ( # noqa: F401
|
||||
DokumentLink,
|
||||
Land,
|
||||
LandAbrechnung,
|
||||
LandVerpachtung,
|
||||
Paechter,
|
||||
)
|
||||
|
||||
from .finanzen import ( # noqa: F401
|
||||
BankTransaction,
|
||||
Rentmeister,
|
||||
StiftungsKonto,
|
||||
Verwaltungskosten,
|
||||
)
|
||||
|
||||
from .destinataere import ( # noqa: F401
|
||||
Destinataer,
|
||||
DestinataerEmailEingang,
|
||||
DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
Foerderung,
|
||||
Person,
|
||||
UnterstuetzungWiederkehrend,
|
||||
VierteljahresNachweis,
|
||||
)
|
||||
|
||||
from .geschichte import ( # noqa: F401
|
||||
GeschichteBild,
|
||||
GeschichteSeite,
|
||||
StiftungsKalenderEintrag,
|
||||
)
|
||||
|
||||
from .veranstaltungen import ( # noqa: F401
|
||||
BriefVorlage,
|
||||
Veranstaltung,
|
||||
Veranstaltungsteilnehmer,
|
||||
)
|
||||
1143
app/stiftung/models/destinataere.py
Normal file
1143
app/stiftung/models/destinataere.py
Normal file
File diff suppressed because it is too large
Load Diff
385
app/stiftung/models/finanzen.py
Normal file
385
app/stiftung/models/finanzen.py
Normal file
@@ -0,0 +1,385 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Rentmeister(models.Model):
|
||||
"""Geschäftsführer der Stiftung (natürliche Personen)"""
|
||||
|
||||
ANREDE_CHOICES = [
|
||||
("herr", "Herr"),
|
||||
("frau", "Frau"),
|
||||
("dr", "Dr."),
|
||||
("prof", "Prof."),
|
||||
("prof_dr", "Prof. Dr."),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
anrede = models.CharField(
|
||||
max_length=10, choices=ANREDE_CHOICES, blank=True, verbose_name="Anrede"
|
||||
)
|
||||
vorname = models.CharField(max_length=100, verbose_name="Vorname")
|
||||
nachname = models.CharField(max_length=100, verbose_name="Nachname")
|
||||
titel = models.CharField(max_length=50, blank=True, verbose_name="Titel")
|
||||
|
||||
# Kontaktdaten
|
||||
email = models.EmailField(blank=True, verbose_name="E-Mail")
|
||||
telefon = models.CharField(max_length=20, blank=True, verbose_name="Telefon")
|
||||
mobil = models.CharField(max_length=20, blank=True, verbose_name="Mobil")
|
||||
|
||||
# Adresse
|
||||
strasse = models.CharField(max_length=200, blank=True, verbose_name="Straße")
|
||||
plz = models.CharField(max_length=10, blank=True, verbose_name="PLZ")
|
||||
ort = models.CharField(max_length=100, blank=True, verbose_name="Ort")
|
||||
|
||||
# Bankdaten für Abrechnungen
|
||||
iban = models.CharField(max_length=34, blank=True, verbose_name="IBAN")
|
||||
bic = models.CharField(max_length=11, blank=True, verbose_name="BIC")
|
||||
bank_name = models.CharField(max_length=100, blank=True, verbose_name="Bank")
|
||||
|
||||
# Stiftungs-spezifisch
|
||||
seit_datum = models.DateField(verbose_name="Rentmeister seit")
|
||||
bis_datum = models.DateField(null=True, blank=True, verbose_name="Rentmeister bis")
|
||||
aktiv = models.BooleanField(default=True, verbose_name="Aktiv")
|
||||
|
||||
# Vergütung/Aufwandsentschädigung
|
||||
monatliche_verguetung = models.DecimalField(
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Monatliche Vergütung (€)",
|
||||
)
|
||||
km_pauschale = models.DecimalField(
|
||||
max_digits=4,
|
||||
decimal_places=2,
|
||||
default=0.30,
|
||||
verbose_name="Kilometerpauschale (€/km)",
|
||||
)
|
||||
|
||||
notizen = models.TextField(blank=True, verbose_name="Notizen")
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Rentmeister"
|
||||
verbose_name_plural = "Rentmeister"
|
||||
ordering = ["nachname", "vorname"]
|
||||
|
||||
def __str__(self):
|
||||
name_parts = []
|
||||
if self.anrede:
|
||||
name_parts.append(self.get_anrede_display())
|
||||
if self.vorname:
|
||||
name_parts.append(self.vorname)
|
||||
name_parts.append(self.nachname)
|
||||
if self.titel:
|
||||
name_parts.append(f"({self.titel})")
|
||||
return " ".join(name_parts)
|
||||
|
||||
def get_full_name(self):
|
||||
"""Vollständiger Name ohne Anrede"""
|
||||
if self.vorname:
|
||||
return f"{self.vorname} {self.nachname}"
|
||||
return self.nachname
|
||||
|
||||
def get_address(self):
|
||||
"""Vollständige Adresse als String"""
|
||||
parts = []
|
||||
if self.strasse:
|
||||
parts.append(self.strasse)
|
||||
if self.plz and self.ort:
|
||||
parts.append(f"{self.plz} {self.ort}")
|
||||
elif self.ort:
|
||||
parts.append(self.ort)
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
class StiftungsKonto(models.Model):
|
||||
"""Bankkonten der Stiftung"""
|
||||
|
||||
KONTO_TYP_CHOICES = [
|
||||
("girokonto", "Girokonto"),
|
||||
("sparkonto", "Sparkonto"),
|
||||
("festgeld", "Festgeld"),
|
||||
("tagesgeld", "Tagesgeld"),
|
||||
("depot", "Depot"),
|
||||
("sonstiges", "Sonstiges"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
kontoname = models.CharField(max_length=200, verbose_name="Kontoname")
|
||||
bank_name = models.CharField(max_length=200, verbose_name="Bank")
|
||||
iban = models.CharField(max_length=34, verbose_name="IBAN")
|
||||
bic = models.CharField(max_length=11, blank=True, verbose_name="BIC")
|
||||
konto_typ = models.CharField(
|
||||
max_length=20,
|
||||
choices=KONTO_TYP_CHOICES,
|
||||
default="girokonto",
|
||||
verbose_name="Kontotyp",
|
||||
)
|
||||
saldo = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, default=0.00, verbose_name="Aktueller Saldo"
|
||||
)
|
||||
saldo_datum = models.DateField(null=True, blank=True, verbose_name="Saldo-Datum")
|
||||
zinssatz = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Zinssatz (%)",
|
||||
)
|
||||
laufzeit_bis = models.DateField(null=True, blank=True, verbose_name="Laufzeit bis")
|
||||
aktiv = models.BooleanField(default=True, verbose_name="Aktiv")
|
||||
notizen = models.TextField(blank=True, verbose_name="Notizen")
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Stiftungskonto"
|
||||
verbose_name_plural = "Stiftungskonten"
|
||||
ordering = ["bank_name", "kontoname"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.bank_name} - {self.kontoname}"
|
||||
|
||||
|
||||
class BankTransaction(models.Model):
|
||||
"""Banktransaktionen aus importierten Kontodaten"""
|
||||
|
||||
TRANSACTION_TYPE_CHOICES = [
|
||||
("eingang", "Eingang"),
|
||||
("ausgang", "Ausgang"),
|
||||
("lastschrift", "Lastschrift"),
|
||||
("ueberweisung", "Überweisung"),
|
||||
("dauerauftrag", "Dauerauftrag"),
|
||||
("kartenzahlung", "Kartenzahlung"),
|
||||
("zinsen", "Zinsen"),
|
||||
("gebuehren", "Gebühren"),
|
||||
("sonstiges", "Sonstiges"),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
("imported", "Importiert"),
|
||||
("verified", "Geprüft"),
|
||||
("assigned", "Zugeordnet"),
|
||||
("ignored", "Ignoriert"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
konto = models.ForeignKey(
|
||||
StiftungsKonto, on_delete=models.CASCADE, verbose_name="Konto"
|
||||
)
|
||||
|
||||
# Transaktionsdaten
|
||||
datum = models.DateField(verbose_name="Buchungsdatum")
|
||||
valuta = models.DateField(null=True, blank=True, verbose_name="Valutadatum")
|
||||
betrag = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, verbose_name="Betrag (€)"
|
||||
)
|
||||
waehrung = models.CharField(max_length=3, default="EUR", verbose_name="Währung")
|
||||
|
||||
# Transaktionsdetails
|
||||
verwendungszweck = models.TextField(verbose_name="Verwendungszweck")
|
||||
empfaenger_zahlungspflichtiger = models.CharField(
|
||||
max_length=200, blank=True, verbose_name="Empfänger/Zahlungspflichtiger"
|
||||
)
|
||||
iban_gegenpartei = models.CharField(
|
||||
max_length=34, blank=True, verbose_name="IBAN Gegenpartei"
|
||||
)
|
||||
bic_gegenpartei = models.CharField(
|
||||
max_length=11, blank=True, verbose_name="BIC Gegenpartei"
|
||||
)
|
||||
|
||||
# Bankspezifische Daten
|
||||
referenz = models.CharField(
|
||||
max_length=100, blank=True, verbose_name="Referenz/Transaktions-ID"
|
||||
)
|
||||
transaction_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=TRANSACTION_TYPE_CHOICES,
|
||||
default="sonstiges",
|
||||
verbose_name="Transaktionsart",
|
||||
)
|
||||
|
||||
# Verwaltung
|
||||
status = models.CharField(
|
||||
max_length=20, choices=STATUS_CHOICES, default="imported", verbose_name="Status"
|
||||
)
|
||||
kommentare = models.TextField(blank=True, verbose_name="Kommentare")
|
||||
verwaltungskosten = models.ForeignKey(
|
||||
"Verwaltungskosten",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="Zugeordnete Verwaltungskosten",
|
||||
)
|
||||
|
||||
# Import-Metadaten
|
||||
import_datei = models.CharField(
|
||||
max_length=255, blank=True, verbose_name="Import-Datei"
|
||||
)
|
||||
importiert_am = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Importiert am"
|
||||
)
|
||||
saldo_nach_buchung = models.DecimalField(
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Saldo nach Buchung",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Banktransaktion"
|
||||
verbose_name_plural = "Banktransaktionen"
|
||||
ordering = ["-datum", "-importiert_am"]
|
||||
unique_together = ["konto", "datum", "betrag", "referenz"] # Prevent duplicates
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.datum} - {self.betrag}€ - {self.verwendungszweck[:50]}"
|
||||
|
||||
def is_income(self):
|
||||
"""Prüft ob es sich um einen Geldeingang handelt"""
|
||||
return self.betrag > 0
|
||||
|
||||
def get_absolute_amount(self):
|
||||
"""Gibt den absoluten Betrag zurück"""
|
||||
return abs(self.betrag)
|
||||
|
||||
|
||||
class Verwaltungskosten(models.Model):
|
||||
"""Administrative Kosten und Ausgaben der Stiftung"""
|
||||
|
||||
KATEGORIE_CHOICES = [
|
||||
("rechnung_intern", "Interne Rechnung"),
|
||||
("bueroausstattung", "Büroausstattung"),
|
||||
("fahrtkosten", "Fahrtkosten"),
|
||||
("porto", "Porto & Versand"),
|
||||
("telefon_internet", "Telefon & Internet"),
|
||||
("software", "Software & Lizenzen"),
|
||||
("beratung", "Beratung & Dienstleistungen"),
|
||||
("versicherung", "Versicherungen"),
|
||||
("steuerberatung", "Steuerberatung"),
|
||||
("bankgebuehren", "Bankgebühren"),
|
||||
("sonstiges", "Sonstiges"),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
("geplant", "Geplant"),
|
||||
("bestellt", "Bestellt"),
|
||||
("erhalten", "Erhalten"),
|
||||
("in_bearbeitung", "In Bearbeitung"),
|
||||
("bezahlt", "Bezahlt"),
|
||||
("storniert", "Storniert"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
bezeichnung = models.CharField(max_length=200, verbose_name="Bezeichnung")
|
||||
kategorie = models.CharField(
|
||||
max_length=30, choices=KATEGORIE_CHOICES, verbose_name="Kategorie"
|
||||
)
|
||||
betrag = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, verbose_name="Betrag (€)"
|
||||
)
|
||||
datum = models.DateField(verbose_name="Datum")
|
||||
lieferant_firma = models.CharField(
|
||||
max_length=200, blank=True, verbose_name="Lieferant/Firma"
|
||||
)
|
||||
rechnungsnummer = models.CharField(
|
||||
max_length=100, blank=True, verbose_name="Rechnungsnummer"
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20, choices=STATUS_CHOICES, default="geplant", verbose_name="Status"
|
||||
)
|
||||
|
||||
# Zuständigkeit und Zahlung
|
||||
rentmeister = models.ForeignKey(
|
||||
Rentmeister,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Zuständiger Rentmeister",
|
||||
)
|
||||
zahlungskonto = models.ForeignKey(
|
||||
StiftungsKonto,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="zahlungen",
|
||||
verbose_name="Zahlungskonto",
|
||||
)
|
||||
quellkonto = models.ForeignKey(
|
||||
StiftungsKonto,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="ausgaben",
|
||||
verbose_name="Quellkonto",
|
||||
)
|
||||
|
||||
# Legacy field für Rückwärtskompatibilität
|
||||
konto = models.ForeignKey(
|
||||
StiftungsKonto,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Konto (Legacy)",
|
||||
help_text="Veraltet - verwende Zahlungskonto und Quellkonto",
|
||||
)
|
||||
|
||||
# Fahrtkosten spezifisch
|
||||
km_anzahl = models.DecimalField(
|
||||
max_digits=8, decimal_places=1, null=True, blank=True, verbose_name="Kilometer"
|
||||
)
|
||||
km_satz = models.DecimalField(
|
||||
max_digits=4, decimal_places=2, null=True, blank=True, verbose_name="€/km"
|
||||
)
|
||||
von_ort = models.CharField(max_length=100, blank=True, verbose_name="Von (Ort)")
|
||||
nach_ort = models.CharField(max_length=100, blank=True, verbose_name="Nach (Ort)")
|
||||
zweck = models.CharField(max_length=200, blank=True, verbose_name="Zweck der Fahrt")
|
||||
|
||||
beschreibung = models.TextField(blank=True, verbose_name="Beschreibung")
|
||||
notizen = models.TextField(blank=True, verbose_name="Notizen")
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Verwaltungskosten"
|
||||
verbose_name_plural = "Verwaltungskosten"
|
||||
ordering = ["-datum", "-erstellt_am"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.bezeichnung} - €{self.betrag} ({self.datum})"
|
||||
|
||||
def get_status_color(self):
|
||||
colors = {
|
||||
"geplant": "secondary",
|
||||
"bestellt": "warning",
|
||||
"erhalten": "info",
|
||||
"in_bearbeitung": "primary",
|
||||
"bezahlt": "success",
|
||||
"storniert": "danger",
|
||||
}
|
||||
return colors.get(self.status, "secondary")
|
||||
|
||||
def get_effective_zahlungskonto(self):
|
||||
"""Gibt das Zahlungskonto zurück, fallback auf Legacy-Konto"""
|
||||
return self.zahlungskonto or self.konto
|
||||
|
||||
def get_effective_quellkonto(self):
|
||||
"""Gibt das Quellkonto zurück, fallback auf Zahlungskonto oder Legacy-Konto"""
|
||||
return self.quellkonto or self.zahlungskonto or self.konto
|
||||
|
||||
def is_fahrtkosten(self):
|
||||
"""Prüft ob es sich um Fahrtkosten handelt"""
|
||||
return self.kategorie == "fahrtkosten"
|
||||
|
||||
def calculate_fahrtkosten(self):
|
||||
"""Berechnet Fahrtkosten automatisch wenn km_anzahl und km_satz gesetzt sind"""
|
||||
if self.km_anzahl and self.km_satz:
|
||||
return self.km_anzahl * self.km_satz
|
||||
return None
|
||||
213
app/stiftung/models/geschichte.py
Normal file
213
app/stiftung/models/geschichte.py
Normal file
@@ -0,0 +1,213 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class GeschichteSeite(models.Model):
|
||||
"""Wiki-style pages for foundation history"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
titel = models.CharField(max_length=200, verbose_name="Titel")
|
||||
slug = models.SlugField(max_length=200, unique=True, verbose_name="URL-Slug")
|
||||
inhalt = models.TextField(
|
||||
verbose_name="Inhalt (Markdown)",
|
||||
blank=True,
|
||||
help_text="Sie können Markdown verwenden: **fett**, *kursiv*, # Überschriften, [Links](URL), Listen, etc."
|
||||
)
|
||||
|
||||
# Metadata
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am")
|
||||
erstellt_von = models.ForeignKey(
|
||||
'auth.User',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='geschichte_seiten_erstellt',
|
||||
verbose_name="Erstellt von"
|
||||
)
|
||||
aktualisiert_von = models.ForeignKey(
|
||||
'auth.User',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='geschichte_seiten_aktualisiert',
|
||||
verbose_name="Aktualisiert von"
|
||||
)
|
||||
|
||||
# Options
|
||||
ist_veroeffentlicht = models.BooleanField(default=True, verbose_name="Veröffentlicht")
|
||||
sortierung = models.IntegerField(default=0, verbose_name="Sortierung")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Geschichte Seite"
|
||||
verbose_name_plural = "Geschichte Seiten"
|
||||
ordering = ['sortierung', 'titel']
|
||||
|
||||
def __str__(self):
|
||||
return self.titel
|
||||
|
||||
def get_absolute_url(self):
|
||||
from django.urls import reverse
|
||||
return reverse('stiftung:geschichte_detail', kwargs={'slug': self.slug})
|
||||
|
||||
|
||||
class GeschichteBild(models.Model):
|
||||
"""Images for history pages"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
seite = models.ForeignKey(
|
||||
GeschichteSeite,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='bilder',
|
||||
verbose_name="Geschichte Seite"
|
||||
)
|
||||
titel = models.CharField(max_length=200, verbose_name="Bildtitel")
|
||||
bild = models.ImageField(
|
||||
upload_to='geschichte/bilder/%Y/%m/',
|
||||
verbose_name="Bild"
|
||||
)
|
||||
beschreibung = models.TextField(blank=True, verbose_name="Beschreibung")
|
||||
alt_text = models.CharField(max_length=200, blank=True, verbose_name="Alt-Text")
|
||||
|
||||
# Metadata
|
||||
hochgeladen_am = models.DateTimeField(auto_now_add=True, verbose_name="Hochgeladen am")
|
||||
hochgeladen_von = models.ForeignKey(
|
||||
'auth.User',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Hochgeladen von"
|
||||
)
|
||||
|
||||
sortierung = models.IntegerField(default=0, verbose_name="Sortierung")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Geschichte Bild"
|
||||
verbose_name_plural = "Geschichte Bilder"
|
||||
ordering = ['sortierung', 'titel']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.titel} ({self.seite.titel})"
|
||||
|
||||
|
||||
class StiftungsKalenderEintrag(models.Model):
|
||||
"""Custom calendar events for foundation management"""
|
||||
|
||||
KATEGORIE_CHOICES = [
|
||||
('termin', 'Termin/Meeting'),
|
||||
('zahlung', 'Zahlungserinnerung'),
|
||||
('deadline', 'Frist/Deadline'),
|
||||
('geburtstag', 'Geburtstag'),
|
||||
('vertrag', 'Vertrag läuft aus'),
|
||||
('pruefung', 'Prüfung/Nachweis'),
|
||||
('sonstiges', 'Sonstiges'),
|
||||
]
|
||||
|
||||
PRIORITAET_CHOICES = [
|
||||
('niedrig', 'Niedrig'),
|
||||
('normal', 'Normal'),
|
||||
('hoch', 'Hoch'),
|
||||
('kritisch', 'Kritisch'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
titel = models.CharField(max_length=200, verbose_name="Titel")
|
||||
beschreibung = models.TextField(blank=True, verbose_name="Beschreibung")
|
||||
|
||||
# Date and time
|
||||
datum = models.DateField(verbose_name="Datum")
|
||||
uhrzeit = models.TimeField(null=True, blank=True, verbose_name="Uhrzeit")
|
||||
ganztags = models.BooleanField(default=True, verbose_name="Ganztägig")
|
||||
|
||||
# Categorization
|
||||
kategorie = models.CharField(
|
||||
max_length=20,
|
||||
choices=KATEGORIE_CHOICES,
|
||||
default='termin',
|
||||
verbose_name="Kategorie"
|
||||
)
|
||||
prioritaet = models.CharField(
|
||||
max_length=20,
|
||||
choices=PRIORITAET_CHOICES,
|
||||
default='normal',
|
||||
verbose_name="Priorität"
|
||||
)
|
||||
|
||||
# Links to related objects
|
||||
destinataer = models.ForeignKey(
|
||||
'stiftung.Destinataer',
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Bezogener Destinatär"
|
||||
)
|
||||
verpachtung = models.ForeignKey(
|
||||
'stiftung.LandVerpachtung',
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Bezogene Verpachtung"
|
||||
)
|
||||
|
||||
# Status and completion
|
||||
erledigt = models.BooleanField(default=False, verbose_name="Erledigt")
|
||||
erledigt_am = models.DateTimeField(null=True, blank=True, verbose_name="Erledigt am")
|
||||
|
||||
# Metadata
|
||||
erstellt_von = models.CharField(
|
||||
max_length=100,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Erstellt von"
|
||||
)
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Kalender Eintrag"
|
||||
verbose_name_plural = "Kalender Einträge"
|
||||
ordering = ['datum', 'uhrzeit']
|
||||
indexes = [
|
||||
models.Index(fields=['datum']),
|
||||
models.Index(fields=['kategorie', 'datum']),
|
||||
models.Index(fields=['erledigt', 'datum']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.datum}: {self.titel}"
|
||||
|
||||
def get_kategorie_icon(self):
|
||||
icons = {
|
||||
'termin': 'fas fa-calendar-alt',
|
||||
'zahlung': 'fas fa-euro-sign',
|
||||
'deadline': 'fas fa-exclamation-triangle',
|
||||
'geburtstag': 'fas fa-birthday-cake',
|
||||
'vertrag': 'fas fa-file-contract',
|
||||
'pruefung': 'fas fa-clipboard-check',
|
||||
'sonstiges': 'fas fa-calendar',
|
||||
}
|
||||
return icons.get(self.kategorie, 'fas fa-calendar')
|
||||
|
||||
def get_prioritaet_color(self):
|
||||
colors = {
|
||||
'niedrig': 'success',
|
||||
'normal': 'primary',
|
||||
'hoch': 'warning',
|
||||
'kritisch': 'danger',
|
||||
}
|
||||
return colors.get(self.prioritaet, 'primary')
|
||||
|
||||
def is_overdue(self):
|
||||
"""Check if event is overdue (past due and not completed)"""
|
||||
if self.erledigt:
|
||||
return False
|
||||
return self.datum < timezone.now().date()
|
||||
|
||||
def is_upcoming(self, days=7):
|
||||
"""Check if event is upcoming within specified days"""
|
||||
if self.erledigt:
|
||||
return False
|
||||
today = timezone.now().date()
|
||||
return today <= self.datum <= (today + timezone.timedelta(days=days))
|
||||
1089
app/stiftung/models/land.py
Normal file
1089
app/stiftung/models/land.py
Normal file
File diff suppressed because it is too large
Load Diff
471
app/stiftung/models/system.py
Normal file
471
app/stiftung/models/system.py
Normal file
@@ -0,0 +1,471 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class CSVImport(models.Model):
|
||||
"""Track CSV import operations for audit purposes"""
|
||||
|
||||
IMPORT_TYPE_CHOICES = [
|
||||
("destinataere", "Destinatäre"),
|
||||
("paechter", "Pächter"),
|
||||
("laendereien", "Ländereien"),
|
||||
("verpachtungen", "Verpachtungen"),
|
||||
("personen", "Personen (Legacy)"),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
("pending", "Ausstehend"),
|
||||
("processing", "Wird verarbeitet"),
|
||||
("completed", "Abgeschlossen"),
|
||||
("failed", "Fehlgeschlagen"),
|
||||
("partial", "Teilweise erfolgreich"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
import_type = models.CharField(
|
||||
max_length=20, choices=IMPORT_TYPE_CHOICES, verbose_name="Import-Typ"
|
||||
)
|
||||
filename = models.CharField(max_length=255, verbose_name="Dateiname")
|
||||
file_size = models.IntegerField(verbose_name="Dateigröße (Bytes)")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
|
||||
|
||||
# Results
|
||||
total_rows = models.IntegerField(default=0, verbose_name="Gesamtzeilen")
|
||||
imported_rows = models.IntegerField(default=0, verbose_name="Importierte Zeilen")
|
||||
failed_rows = models.IntegerField(default=0, verbose_name="Fehlgeschlagene Zeilen")
|
||||
error_log = models.TextField(null=True, blank=True, verbose_name="Fehlerprotokoll")
|
||||
|
||||
# Metadata
|
||||
created_by = models.CharField(
|
||||
max_length=100, null=True, blank=True, verbose_name="Erstellt von"
|
||||
)
|
||||
started_at = models.DateTimeField(auto_now_add=True, verbose_name="Gestartet um")
|
||||
completed_at = models.DateTimeField(
|
||||
null=True, blank=True, verbose_name="Abgeschlossen um"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "CSV Import"
|
||||
verbose_name_plural = "CSV Imports"
|
||||
ordering = ["-started_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_import_type_display()} - {self.filename} ({self.status})"
|
||||
|
||||
def get_duration(self):
|
||||
"""Calculate import duration"""
|
||||
if self.completed_at and self.started_at:
|
||||
return self.completed_at - self.started_at
|
||||
return None
|
||||
|
||||
def get_success_rate(self):
|
||||
"""Calculate success rate percentage"""
|
||||
if self.total_rows > 0:
|
||||
return (self.imported_rows / self.total_rows) * 100
|
||||
return 0
|
||||
|
||||
|
||||
class ApplicationPermission(models.Model):
|
||||
"""Custom permissions for application functions"""
|
||||
|
||||
class Meta:
|
||||
managed = False # No database table creation
|
||||
default_permissions = () # Remove default Django permissions
|
||||
permissions = [
|
||||
# Entity Management Permissions
|
||||
("manage_destinataere", "Kann Destinatäre verwalten"),
|
||||
("view_destinataere", "Kann Destinatäre anzeigen"),
|
||||
("manage_land", "Kann Ländereien verwalten"),
|
||||
("view_land", "Kann Ländereien anzeigen"),
|
||||
("manage_paechter", "Kann Pächter verwalten"),
|
||||
("view_paechter", "Kann Pächter anzeigen"),
|
||||
("manage_verpachtungen", "Kann Verpachtungen verwalten"),
|
||||
("view_verpachtungen", "Kann Verpachtungen anzeigen"),
|
||||
("manage_foerderungen", "Kann Förderungen verwalten"),
|
||||
("view_foerderungen", "Kann Förderungen anzeigen"),
|
||||
# Document Management Permissions
|
||||
("manage_documents", "Kann Dokumente verwalten"),
|
||||
("view_documents", "Kann Dokumente anzeigen"),
|
||||
("link_documents", "Kann Dokumente verknüpfen"),
|
||||
# Financial Management Permissions
|
||||
("manage_verwaltungskosten", "Kann Verwaltungskosten verwalten"),
|
||||
("view_verwaltungskosten", "Kann Verwaltungskosten anzeigen"),
|
||||
("approve_payments", "Kann Zahlungen genehmigen"),
|
||||
("manage_konten", "Kann Stiftungskonten verwalten"),
|
||||
("view_konten", "Kann Stiftungskonten anzeigen"),
|
||||
("manage_rentmeister", "Kann Rentmeister verwalten"),
|
||||
("view_rentmeister", "Kann Rentmeister anzeigen"),
|
||||
# Administration Permissions
|
||||
("access_administration", "Kann Administration aufrufen"),
|
||||
("view_audit_logs", "Kann Audit-Logs anzeigen"),
|
||||
("manage_backups", "Kann Backups erstellen und verwalten"),
|
||||
("manage_users", "Kann Benutzer verwalten"),
|
||||
("manage_permissions", "Kann Berechtigungen verwalten"),
|
||||
# Veranstaltungen Permissions
|
||||
("manage_veranstaltungen", "Kann Veranstaltungen verwalten"),
|
||||
("view_veranstaltungen", "Kann Veranstaltungen anzeigen"),
|
||||
# Import/Export Permissions
|
||||
("import_data", "Kann Daten importieren"),
|
||||
("export_data", "Kann Daten exportieren"),
|
||||
# System Permissions
|
||||
("access_django_admin", "Kann Django Admin aufrufen"),
|
||||
("view_system_stats", "Kann Systemstatistiken anzeigen"),
|
||||
]
|
||||
|
||||
|
||||
class AuditLog(models.Model):
|
||||
"""Audit Log für alle Benutzeraktionen im System"""
|
||||
|
||||
ACTION_TYPES = [
|
||||
("create", "Erstellt"),
|
||||
("update", "Aktualisiert"),
|
||||
("delete", "Gelöscht"),
|
||||
("link", "Verknüpft"),
|
||||
("unlink", "Verknüpfung entfernt"),
|
||||
("login", "Anmeldung"),
|
||||
("logout", "Abmeldung"),
|
||||
("backup", "Backup erstellt"),
|
||||
("restore", "Wiederherstellung"),
|
||||
("export", "Export"),
|
||||
("import", "Import"),
|
||||
]
|
||||
|
||||
ENTITY_TYPES = [
|
||||
("destinataer", "Destinatär"),
|
||||
("land", "Länderei"),
|
||||
("paechter", "Pächter"),
|
||||
("verpachtung", "Verpachtung"),
|
||||
("foerderung", "Förderung"),
|
||||
("rentmeister", "Rentmeister"),
|
||||
("stiftungskonto", "Stiftungskonto"),
|
||||
("verwaltungskosten", "Verwaltungskosten"),
|
||||
("banktransaction", "Bank-Transaktion"),
|
||||
("dokumentlink", "Dokument-Verknüpfung"),
|
||||
("system", "System"),
|
||||
("user", "Benutzer"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
# Benutzer und Zeitpunkt
|
||||
user = models.ForeignKey(
|
||||
"auth.User", on_delete=models.SET_NULL, null=True, verbose_name="Benutzer"
|
||||
)
|
||||
username = models.CharField(
|
||||
max_length=150, verbose_name="Benutzername"
|
||||
) # Fallback falls User gelöscht wird
|
||||
timestamp = models.DateTimeField(auto_now_add=True, verbose_name="Zeitpunkt")
|
||||
|
||||
# Aktion
|
||||
action = models.CharField(
|
||||
max_length=20, choices=ACTION_TYPES, verbose_name="Aktion"
|
||||
)
|
||||
entity_type = models.CharField(
|
||||
max_length=20, choices=ENTITY_TYPES, verbose_name="Entitätstyp"
|
||||
)
|
||||
entity_id = models.CharField(max_length=100, blank=True, verbose_name="Entitäts-ID")
|
||||
entity_name = models.CharField(max_length=255, verbose_name="Entitätsname")
|
||||
|
||||
# Details
|
||||
description = models.TextField(verbose_name="Beschreibung")
|
||||
changes = models.JSONField(
|
||||
null=True, blank=True, verbose_name="Änderungen"
|
||||
) # Alte und neue Werte
|
||||
|
||||
# Request-Informationen
|
||||
ip_address = models.GenericIPAddressField(
|
||||
null=True, blank=True, verbose_name="IP-Adresse"
|
||||
)
|
||||
user_agent = models.TextField(blank=True, verbose_name="User Agent")
|
||||
session_key = models.CharField(
|
||||
max_length=40, blank=True, verbose_name="Session-Key"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Audit Log Eintrag"
|
||||
verbose_name_plural = "Audit Log Einträge"
|
||||
ordering = ["-timestamp"]
|
||||
indexes = [
|
||||
models.Index(fields=["timestamp"]),
|
||||
models.Index(fields=["user", "timestamp"]),
|
||||
models.Index(fields=["entity_type", "timestamp"]),
|
||||
models.Index(fields=["action", "timestamp"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.username} - {self.get_action_display()} {self.get_entity_type_display()} '{self.entity_name}' ({self.timestamp.strftime('%d.%m.%Y %H:%M')})"
|
||||
|
||||
def get_changes_summary(self):
|
||||
"""Erstellt eine lesbare Zusammenfassung der Änderungen"""
|
||||
if not self.changes:
|
||||
return "Keine Details verfügbar"
|
||||
|
||||
if isinstance(self.changes, dict):
|
||||
summary = []
|
||||
for field, values in self.changes.items():
|
||||
if isinstance(values, dict) and "old" in values and "new" in values:
|
||||
old_val = values["old"] or "Leer"
|
||||
new_val = values["new"] or "Leer"
|
||||
summary.append(f"{field}: '{old_val}' → '{new_val}'")
|
||||
return "; ".join(summary) if summary else "Keine Änderungen dokumentiert"
|
||||
|
||||
return str(self.changes)
|
||||
|
||||
|
||||
class BackupJob(models.Model):
|
||||
"""Backup-Jobs und deren Status"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
("pending", "Wartend"),
|
||||
("running", "Läuft"),
|
||||
("completed", "Abgeschlossen"),
|
||||
("failed", "Fehlgeschlagen"),
|
||||
("cancelled", "Abgebrochen"),
|
||||
]
|
||||
|
||||
TYPE_CHOICES = [
|
||||
("full", "Vollständiges Backup"),
|
||||
("database", "Nur Datenbank"),
|
||||
("files", "Nur Dateien"),
|
||||
]
|
||||
|
||||
OPERATION_CHOICES = [
|
||||
("backup", "Backup"),
|
||||
("restore", "Wiederherstellung"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
# Job-Details
|
||||
operation = models.CharField(
|
||||
max_length=20, choices=OPERATION_CHOICES, default="backup", verbose_name="Vorgang"
|
||||
)
|
||||
backup_type = models.CharField(
|
||||
max_length=20, choices=TYPE_CHOICES, verbose_name="Backup-Typ"
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20, choices=STATUS_CHOICES, default="pending", verbose_name="Status"
|
||||
)
|
||||
|
||||
# Ausführung
|
||||
created_by = models.ForeignKey(
|
||||
"auth.User", on_delete=models.SET_NULL, null=True, verbose_name="Erstellt von"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
|
||||
started_at = models.DateTimeField(
|
||||
null=True, blank=True, verbose_name="Gestartet am"
|
||||
)
|
||||
completed_at = models.DateTimeField(
|
||||
null=True, blank=True, verbose_name="Abgeschlossen am"
|
||||
)
|
||||
|
||||
# Ergebnis
|
||||
backup_filename = models.CharField(
|
||||
max_length=255, blank=True, verbose_name="Backup-Dateiname"
|
||||
)
|
||||
backup_size = models.BigIntegerField(
|
||||
null=True, blank=True, verbose_name="Backup-Größe (Bytes)"
|
||||
)
|
||||
error_message = models.TextField(blank=True, verbose_name="Fehlermeldung")
|
||||
|
||||
# Metadaten
|
||||
database_size = models.BigIntegerField(
|
||||
null=True, blank=True, verbose_name="Datenbankgröße (Bytes)"
|
||||
)
|
||||
files_count = models.IntegerField(
|
||||
null=True, blank=True, verbose_name="Anzahl Dateien"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Backup-Job"
|
||||
verbose_name_plural = "Backup-Jobs"
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_backup_type_display()} - {self.get_status_display()} ({self.created_at.strftime('%d.%m.%Y %H:%M')})"
|
||||
|
||||
def get_duration(self):
|
||||
"""Berechnet die Dauer des Backup-Jobs"""
|
||||
if self.started_at and self.completed_at:
|
||||
return self.completed_at - self.started_at
|
||||
elif self.started_at:
|
||||
from django.utils import timezone
|
||||
|
||||
return timezone.now() - self.started_at
|
||||
return None
|
||||
|
||||
def get_size_display(self):
|
||||
"""Formatiert die Backup-Größe für die Anzeige"""
|
||||
if not self.backup_size:
|
||||
return "Unbekannt"
|
||||
|
||||
size = self.backup_size
|
||||
for unit in ["B", "KB", "MB", "GB"]:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
|
||||
class AppConfiguration(models.Model):
|
||||
"""Application configuration settings that can be managed through the admin interface"""
|
||||
|
||||
SETTING_TYPE_CHOICES = [
|
||||
("text", "Text"),
|
||||
("number", "Number"),
|
||||
("boolean", "Boolean"),
|
||||
("url", "URL"),
|
||||
("tag", "Tag Name"),
|
||||
("tag_id", "Tag ID"),
|
||||
]
|
||||
|
||||
CATEGORY_CHOICES = [
|
||||
("paperless", "Paperless Integration"),
|
||||
("general", "General Settings"),
|
||||
("corporate", "Corporate Identity"),
|
||||
("notifications", "Notifications"),
|
||||
("system", "System Settings"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
key = models.CharField(max_length=100, unique=True, verbose_name="Setting Key")
|
||||
display_name = models.CharField(max_length=200, verbose_name="Display Name")
|
||||
description = models.TextField(blank=True, null=True, verbose_name="Description")
|
||||
value = models.TextField(verbose_name="Value")
|
||||
default_value = models.TextField(verbose_name="Default Value")
|
||||
setting_type = models.CharField(
|
||||
max_length=20, choices=SETTING_TYPE_CHOICES, default="text", verbose_name="Type"
|
||||
)
|
||||
category = models.CharField(
|
||||
max_length=50,
|
||||
choices=CATEGORY_CHOICES,
|
||||
default="general",
|
||||
verbose_name="Category",
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name="Active")
|
||||
is_system = models.BooleanField(
|
||||
default=False, verbose_name="System Setting (read-only)"
|
||||
)
|
||||
order = models.IntegerField(default=0, verbose_name="Display Order")
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "App Configuration"
|
||||
verbose_name_plural = "App Configurations"
|
||||
ordering = ["category", "order", "display_name"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.display_name} ({self.key})"
|
||||
|
||||
def get_typed_value(self):
|
||||
"""Return the value converted to the appropriate type"""
|
||||
if self.setting_type == "boolean":
|
||||
return self.value.lower() in ("true", "1", "yes", "on")
|
||||
elif self.setting_type == "number":
|
||||
try:
|
||||
if "." in self.value:
|
||||
return float(self.value)
|
||||
return int(self.value)
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def get_setting(cls, key, default=None):
|
||||
"""Get a setting value by key"""
|
||||
try:
|
||||
setting = cls.objects.get(key=key, is_active=True)
|
||||
return setting.get_typed_value()
|
||||
except cls.DoesNotExist:
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
def set_setting(
|
||||
cls,
|
||||
key,
|
||||
value,
|
||||
display_name=None,
|
||||
description=None,
|
||||
setting_type="text",
|
||||
category="general",
|
||||
):
|
||||
"""Set or update a setting value"""
|
||||
setting, created = cls.objects.get_or_create(
|
||||
key=key,
|
||||
defaults={
|
||||
"display_name": display_name or key,
|
||||
"description": description,
|
||||
"value": str(value),
|
||||
"default_value": str(value),
|
||||
"setting_type": setting_type,
|
||||
"category": category,
|
||||
},
|
||||
)
|
||||
if not created:
|
||||
setting.value = str(value)
|
||||
setting.save()
|
||||
return setting
|
||||
|
||||
|
||||
class HelpBox(models.Model):
|
||||
"""Editierbare Hilfe-Infoboxen für Formulare"""
|
||||
|
||||
PAGE_CHOICES = [
|
||||
("destinataer_new", "Neuer Destinatär"),
|
||||
("unterstuetzung_new", "Neue Unterstützung"),
|
||||
("foerderung_new", "Neue Förderung"),
|
||||
("paechter_new", "Neuer Pächter"),
|
||||
("laenderei_new", "Neue Länderei"),
|
||||
("verpachtung_new", "Neue Verpachtung"),
|
||||
("land_abrechnung_new", "Neue Landabrechnung"),
|
||||
("person_new", "Neue Person"),
|
||||
("konto_new", "Neues Konto"),
|
||||
("verwaltungskosten_new", "Neue Verwaltungskosten"),
|
||||
("rentmeister_new", "Neuer Rentmeister"),
|
||||
("dokument_new", "Neues Dokument"),
|
||||
("user_new", "Neuer Benutzer"),
|
||||
("csv_import_new", "CSV Import"),
|
||||
("destinataer_notiz_new", "Destinatär Notiz"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
page_key = models.CharField(
|
||||
max_length=50, choices=PAGE_CHOICES, unique=True, verbose_name="Seite"
|
||||
)
|
||||
title = models.CharField(max_length=200, verbose_name="Titel der Hilfsbox")
|
||||
content = models.TextField(
|
||||
verbose_name="Inhalt (Markdown unterstützt)",
|
||||
help_text="Sie können Markdown verwenden: **fett**, *kursiv*, `code`, [Link](url), etc.",
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name="Aktiv")
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am")
|
||||
created_by = models.CharField(
|
||||
max_length=100, null=True, blank=True, verbose_name="Erstellt von"
|
||||
)
|
||||
updated_by = models.CharField(
|
||||
max_length=100, null=True, blank=True, verbose_name="Aktualisiert von"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Hilfs-Infobox"
|
||||
verbose_name_plural = "Hilfs-Infoboxen"
|
||||
ordering = ["page_key"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_page_key_display()}: {self.title}"
|
||||
|
||||
@classmethod
|
||||
def get_help_for_page(cls, page_key):
|
||||
"""Hole die aktive Hilfs-Infobox für eine bestimmte Seite"""
|
||||
try:
|
||||
return cls.objects.get(page_key=page_key, is_active=True)
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
215
app/stiftung/models/veranstaltungen.py
Normal file
215
app/stiftung/models/veranstaltungen.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class BriefVorlage(models.Model):
|
||||
"""Wiederverwendbare Briefvorlagen für Serienbriefe (Veranstaltungseinladungen u.ä.)"""
|
||||
|
||||
name = models.CharField(max_length=100, verbose_name="Vorlagenname")
|
||||
beschreibung = models.TextField(
|
||||
blank=True,
|
||||
verbose_name="Beschreibung",
|
||||
help_text="Kurze Beschreibung des Verwendungszwecks dieser Vorlage.",
|
||||
)
|
||||
briefvorlage = models.TextField(
|
||||
verbose_name="Brieftext (HTML)",
|
||||
help_text=(
|
||||
"HTML-Text des Briefs. Verfügbare Platzhalter: "
|
||||
"{{ anrede }}, {{ vorname }}, {{ nachname }}, {{ strasse }}, "
|
||||
"{{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, "
|
||||
"{{ veranstaltungsort }}, {{ gasthaus_adresse }}"
|
||||
),
|
||||
)
|
||||
betreff = models.CharField(
|
||||
max_length=300,
|
||||
blank=True,
|
||||
verbose_name="Standard-Betreff",
|
||||
help_text="Wird beim Laden der Vorlage in die Veranstaltung übernommen. Leer = unveränderter Betreff.",
|
||||
)
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Briefvorlage"
|
||||
verbose_name_plural = "Briefvorlagen"
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Veranstaltung(models.Model):
|
||||
"""Veranstaltungen der Stiftung, z.B. Stiftungsessen mit Rechnungslegung"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
("geplant", "Geplant"),
|
||||
("einladungen_versendet", "Einladungen versendet"),
|
||||
("abgeschlossen", "Abgeschlossen"),
|
||||
("abgesagt", "Abgesagt"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
titel = models.CharField(max_length=200, verbose_name="Titel")
|
||||
datum = models.DateField(verbose_name="Datum")
|
||||
uhrzeit = models.TimeField(null=True, blank=True, verbose_name="Uhrzeit")
|
||||
ort = models.CharField(max_length=200, verbose_name="Ort / Gasthaus")
|
||||
adresse = models.TextField(blank=True, verbose_name="Adresse Gasthaus")
|
||||
beschreibung = models.TextField(blank=True, verbose_name="Beschreibung / Zweck")
|
||||
status = models.CharField(
|
||||
max_length=30,
|
||||
choices=STATUS_CHOICES,
|
||||
default="geplant",
|
||||
verbose_name="Status",
|
||||
)
|
||||
budget_pro_person = models.DecimalField(
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Budget pro Person (€)",
|
||||
help_text="Geschätztes Budget je Teilnehmer in €",
|
||||
)
|
||||
briefvorlage = models.TextField(
|
||||
blank=True,
|
||||
verbose_name="Briefvorlage",
|
||||
help_text=(
|
||||
"HTML/Text-Template für Serienbrief. Platzhalter: "
|
||||
"{{ anrede }}, {{ vorname }}, {{ nachname }}, {{ strasse }}, "
|
||||
"{{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, "
|
||||
"{{ veranstaltungsort }}, {{ gasthaus_adresse }}"
|
||||
),
|
||||
)
|
||||
betreff = models.CharField(
|
||||
max_length=300,
|
||||
blank=True,
|
||||
verbose_name="Betreff",
|
||||
help_text="Betreffzeile des Serienbriefs. Leer = Standardbetreff.",
|
||||
)
|
||||
unterschrift_1_name = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="Katrin Kleinpaß",
|
||||
verbose_name="Unterschrift 1 – Name",
|
||||
)
|
||||
unterschrift_1_titel = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="Rentmeisterin",
|
||||
verbose_name="Unterschrift 1 – Titel",
|
||||
)
|
||||
unterschrift_2_name = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="Jan Remmer Siebels",
|
||||
verbose_name="Unterschrift 2 – Name",
|
||||
)
|
||||
unterschrift_2_titel = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="Rentmeister",
|
||||
verbose_name="Unterschrift 2 – Titel",
|
||||
)
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Veranstaltung"
|
||||
verbose_name_plural = "Veranstaltungen"
|
||||
ordering = ["-datum"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.titel} ({self.datum})"
|
||||
|
||||
def get_teilnehmer_count(self):
|
||||
return self.teilnehmer.count()
|
||||
|
||||
def get_zugesagte_count(self):
|
||||
return self.teilnehmer.filter(rsvp_status="zugesagt").count()
|
||||
|
||||
def get_abgesagte_count(self):
|
||||
return self.teilnehmer.filter(rsvp_status="abgesagt").count()
|
||||
|
||||
def get_keine_rueckmeldung_count(self):
|
||||
return self.teilnehmer.filter(rsvp_status="keine_rueckmeldung").count()
|
||||
|
||||
|
||||
class Veranstaltungsteilnehmer(models.Model):
|
||||
"""Teilnehmer einer Veranstaltung – primär freie Eingabe für Familienmitglieder"""
|
||||
|
||||
ANREDE_CHOICES = [
|
||||
("Herr", "Herr"),
|
||||
("Frau", "Frau"),
|
||||
("", "Keine Anrede"),
|
||||
]
|
||||
|
||||
RSVP_CHOICES = [
|
||||
("eingeladen", "Eingeladen"),
|
||||
("zugesagt", "Zugesagt"),
|
||||
("abgesagt", "Abgesagt"),
|
||||
("keine_rueckmeldung", "Keine Rückmeldung"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
veranstaltung = models.ForeignKey(
|
||||
Veranstaltung,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="teilnehmer",
|
||||
verbose_name="Veranstaltung",
|
||||
)
|
||||
|
||||
# Optionale Verknüpfung zu bestehenden Datensätzen
|
||||
paechter = models.ForeignKey(
|
||||
"stiftung.Paechter",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="Pächter (optional)",
|
||||
)
|
||||
destinataer = models.ForeignKey(
|
||||
"stiftung.Destinataer",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="Destinatär (optional)",
|
||||
)
|
||||
|
||||
# Freie Felder (Pflichtfelder für Serienbrief)
|
||||
anrede = models.CharField(
|
||||
max_length=10, choices=ANREDE_CHOICES, blank=True, verbose_name="Anrede"
|
||||
)
|
||||
vorname = models.CharField(max_length=100, verbose_name="Vorname")
|
||||
nachname = models.CharField(max_length=100, verbose_name="Nachname")
|
||||
strasse = models.CharField(max_length=200, blank=True, verbose_name="Straße")
|
||||
plz = models.CharField(max_length=10, blank=True, verbose_name="PLZ")
|
||||
ort = models.CharField(max_length=100, blank=True, verbose_name="Ort")
|
||||
email = models.EmailField(
|
||||
blank=True, verbose_name="E-Mail", help_text="Optional, für späteren E-Mail-Versand"
|
||||
)
|
||||
|
||||
rsvp_status = models.CharField(
|
||||
max_length=20,
|
||||
choices=RSVP_CHOICES,
|
||||
default="eingeladen",
|
||||
verbose_name="RSVP-Status",
|
||||
)
|
||||
bemerkungen = models.TextField(blank=True, verbose_name="Bemerkungen")
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Veranstaltungsteilnehmer"
|
||||
verbose_name_plural = "Veranstaltungsteilnehmer"
|
||||
ordering = ["nachname", "vorname"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.anrede} {self.vorname} {self.nachname}".strip()
|
||||
|
||||
def get_full_name(self):
|
||||
return f"{self.vorname} {self.nachname}".strip()
|
||||
|
||||
def get_full_address(self):
|
||||
parts = [self.strasse, f"{self.plz} {self.ort}".strip()]
|
||||
return ", ".join(p for p in parts if p)
|
||||
Reference in New Issue
Block a user