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