Files
stiftung-management-system/app/stiftung/models/system.py
SysAdmin Agent e0b377014c
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled
v4.1.0: DMS email documents, category-specific Nachweis linking, version system
- Save cover email body as DMS document with new 'email' context type
- Show email body separately from attachments in email detail view
- Add per-category DMS document assignment in quarterly confirmation
  (Studiennachweis, Einkommenssituation, Vermögenssituation)
- Add VERSION file and context processor for automatic version display
- Add MCP server, agent system, import/export, and new migrations
- Update compose files and production environment template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:48:52 +00:00

480 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"),
("foerderungen", "Förderungen"),
("konten", "Stiftungskonten"),
("verwaltungskosten", "Verwaltungskosten"),
("rentmeister", "Rentmeister"),
("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"),
# AI Agent Permissions
("can_use_agent", "Kann AI-Assistenten nutzen"),
]
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"),
("password", "Password"),
("number", "Number"),
("boolean", "Boolean"),
("url", "URL"),
("tag", "Tag Name"),
("tag_id", "Tag ID"),
]
CATEGORY_CHOICES = [
("paperless", "Paperless Integration"),
("email", "E-Mail / IMAP"),
("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