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:
SysAdmin Agent
2026-03-11 09:02:08 +00:00
parent 709903e627
commit b4bad7bc83
7 changed files with 3565 additions and 0 deletions

View 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,
)

File diff suppressed because it is too large Load Diff

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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)