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:
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
|
||||
|
||||
Reference in New Issue
Block a user