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