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>
472 lines
17 KiB
Python
472 lines
17 KiB
Python
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
|
|
|