from django.contrib import admin from django.utils.html import format_html from django.urls import reverse from django.db.models import Sum, Count from django.utils.safestring import mark_safe from . import models from .models import ( Person, Paechter, Destinataer, DokumentLink, Foerderung, Land, CSVImport, Rentmeister, StiftungsKonto, Verwaltungskosten, BankTransaction, AuditLog, BackupJob, AppConfiguration, DestinataerUnterstuetzung, UnterstuetzungWiederkehrend ) @admin.register(CSVImport) class CSVImportAdmin(admin.ModelAdmin): list_display = ['import_type', 'filename', 'status', 'total_rows', 'imported_rows', 'failed_rows', 'created_by', 'started_at', 'duration_display'] list_filter = ['import_type', 'status', 'started_at'] search_fields = ['filename', 'created_by'] readonly_fields = ['id', 'started_at', 'completed_at', 'get_success_rate'] ordering = ['-started_at'] fieldsets = ( ('Grundinformationen', { 'fields': ('import_type', 'filename', 'file_size', 'status') }), ('Ergebnisse', { 'fields': ('total_rows', 'imported_rows', 'failed_rows', 'get_success_rate', 'error_log') }), ('Metadaten', { 'fields': ('created_by', 'started_at', 'completed_at') }), ) def duration_display(self, obj): duration = obj.get_duration() if duration: return f"{duration.total_seconds():.1f}s" return "-" duration_display.short_description = "Dauer" def get_success_rate(self, obj): rate = obj.get_success_rate() if rate >= 90: color = "success" elif rate >= 70: color = "warning" else: color = "danger" return format_html('{:.1f}%', color, rate) get_success_rate.short_description = "Erfolgsrate" @admin.register(Person) class PersonAdmin(admin.ModelAdmin): list_display = ['nachname', 'vorname', 'familienzweig', 'geburtsdatum', 'email', 'iban_display'] list_filter = ['familienzweig', 'geburtsdatum'] search_fields = ['nachname', 'vorname', 'email', 'familienzweig'] ordering = ['nachname', 'vorname'] readonly_fields = ['id'] fieldsets = ( ('Persönliche Daten', { 'fields': ('vorname', 'nachname', 'geburtsdatum', 'email', 'telefon') }), ('Stiftungsdaten', { 'fields': ('familienzweig', 'iban', 'adresse') }), ('Zusätzlich', { 'fields': ('notizen', 'aktiv') }), ('System', { 'fields': ('id',), 'classes': ('collapse',) }), ) def iban_display(self, obj): if obj.iban: return format_html('{}', obj.iban) return '-' iban_display.short_description = 'IBAN' def get_queryset(self, request): return super().get_queryset(request).annotate( total_foerderungen=Sum('foerderung__betrag') ) @admin.register(Paechter) class PaechterAdmin(admin.ModelAdmin): list_display = ['nachname', 'vorname', 'pachtnummer', 'pachtzins_aktuell', 'landwirtschaftliche_ausbildung', 'aktiv'] list_filter = ['landwirtschaftliche_ausbildung', 'aktiv'] search_fields = ['nachname', 'vorname', 'email', 'pachtnummer'] ordering = ['nachname', 'vorname'] readonly_fields = ['id'] fieldsets = ( ('Persönliche Daten', { 'fields': ('vorname', 'nachname', 'geburtsdatum', 'email', 'telefon') }), ('Pacht-Informationen', { 'fields': ('pachtnummer', 'pachtbeginn_erste', 'pachtende_letzte', 'pachtzins_aktuell') }), ('Landwirtschaftliche Qualifikation', { 'fields': ('landwirtschaftliche_ausbildung', 'berufserfahrung_jahre', 'spezialisierung') }), ('Kontaktdaten', { 'fields': ('iban', 'strasse', 'plz', 'ort') }), ('Pächter-Typ', { 'fields': ('personentyp',) }), ('Zusätzlich', { 'fields': ('notizen', 'aktiv') }), ('System', { 'fields': ('id',), 'classes': ('collapse',) }), ) def iban_display(self, obj): if obj.iban: return format_html('{}', obj.iban) return '-' iban_display.short_description = 'IBAN' @admin.register(Destinataer) class DestinataerAdmin(admin.ModelAdmin): list_display = ['nachname', 'vorname', 'familienzweig', 'berufsgruppe', 'institution', 'finanzielle_notlage', 'aktiv'] list_filter = ['familienzweig', 'berufsgruppe', 'finanzielle_notlage', 'aktiv'] search_fields = ['nachname', 'vorname', 'email', 'institution', 'familienzweig'] ordering = ['nachname', 'vorname'] readonly_fields = ['id'] fieldsets = ( ('Persönliche Daten', { 'fields': ('vorname', 'nachname', 'geburtsdatum', 'email', 'telefon') }), ('Berufliche Informationen', { 'fields': ('berufsgruppe', 'ausbildungsstand', 'institution') }), ('Projekt & Finanzen', { 'fields': ('projekt_beschreibung', 'jaehrliches_einkommen', 'finanzielle_notlage') }), ('Stiftungsdaten', { 'fields': ('familienzweig', 'iban', 'strasse', 'plz', 'ort') }), ('Zusätzlich', { 'fields': ('notizen', 'aktiv') }), ('System', { 'fields': ('id',), 'classes': ('collapse',) }), ) def iban_display(self, obj): if obj.iban: return format_html('{}', obj.iban) return '-' iban_display.short_description = 'IBAN' @admin.register(Land) class LandAdmin(admin.ModelAdmin): list_display = [ 'lfd_nr', 'gemeinde', 'gemarkung', 'flur', 'flurstueck', 'groesse_qm', 'verp_flaeche_aktuell', 'verpachtungsgrad_display', 'aktiv' ] list_filter = ['gemeinde', 'gemarkung', 'aktiv'] search_fields = ['lfd_nr', 'gemeinde', 'gemarkung', 'flur', 'flurstueck'] ordering = ['gemeinde', 'gemarkung', 'flur', 'flurstueck'] readonly_fields = ['id', 'gesamtflaeche_berechnet', 'verpachtungsgrad_berechnet'] fieldsets = ( ('Identifikation', { 'fields': ('lfd_nr', 'ew_nummer') }), ('Gerichtliche Zuständigkeit', { 'fields': ('amtsgericht',) }), ('Verwaltungsstruktur', { 'fields': ('gemeinde', 'gemarkung', 'flur', 'flurstueck') }), ('Flächenangaben', { 'fields': ('groesse_qm', 'gruenland_qm', 'acker_qm', 'wald_qm', 'sonstiges_qm') }), ('Verpachtung', { 'fields': ('verpachtete_gesamtflaeche', 'flaeche_alte_liste', 'verp_flaeche_aktuell') }), ('Steuern und Abgaben', { 'fields': ('anteil_grundsteuer', 'anteil_lwk') }), ('Status', { 'fields': ('aktiv', 'notizen') }), ('System', { 'fields': ('id', 'erstellt_am', 'aktualisiert_am'), 'classes': ('collapse',) }), ) def verpachtungsgrad_display(self, obj): grad = obj.get_verpachtungsgrad() if grad > 90: color = 'green' elif grad > 70: color = 'orange' else: color = 'red' return format_html('{:.1f}%', color, grad) verpachtungsgrad_display.short_description = 'Verpachtungsgrad' def gesamtflaeche_berechnet(self, obj): return f"{obj.get_gesamtflaeche():.2f} qm" gesamtflaeche_berechnet.short_description = 'Berechnete Gesamtfläche' def verpachtungsgrad_berechnet(self, obj): return f"{obj.get_verpachtungsgrad():.1f}%" verpachtungsgrad_berechnet.short_description = 'Verpachtungsgrad' @admin.register(DokumentLink) class DokumentLinkAdmin(admin.ModelAdmin): list_display = ['titel', 'kontext', 'paperless_document_id'] list_filter = ['kontext'] search_fields = ['titel', 'kontext'] ordering = ['titel'] readonly_fields = ['id'] fieldsets = ( ('Dokument', { 'fields': ('titel', 'kontext', 'paperless_document_id', 'beschreibung') }), ('System', { 'fields': ('id',), 'classes': ('collapse',) }), ) @admin.register(Foerderung) class FoerderungAdmin(admin.ModelAdmin): list_display = ['destinataer', 'jahr', 'betrag', 'verwendungsnachweis_link', 'total_for_destinataer'] list_filter = ['jahr', 'destinataer__familienzweig'] search_fields = ['destinataer__nachname', 'destinataer__vorname', 'destinataer__familienzweig'] ordering = ['-jahr', '-betrag'] readonly_fields = ['id'] fieldsets = ( ('Förderung', { 'fields': ('destinataer', 'person', 'jahr', 'betrag', 'kategorie', 'status') }), ('Dokumentation', { 'fields': ('verwendungsnachweis', 'bemerkungen') }), ('Daten', { 'fields': ('antragsdatum', 'entscheidungsdatum') }), ('System', { 'fields': ('id',), 'classes': ('collapse',) }), ) def verwendungsnachweis_link(self, obj): if obj.verwendungsnachweis: return format_html( '{}', reverse('admin:stiftung_dokumentlink_change', args=[obj.verwendungsnachweis.id]), obj.verwendungsnachweis.titel ) return '-' verwendungsnachweis_link.short_description = 'Verwendungsnachweis' def total_for_destinataer(self, obj): total = Foerderung.objects.filter(destinataer=obj.destinataer).aggregate(Sum('betrag'))['betrag__sum'] or 0 return f"€{total:,.2f}" total_for_destinataer.short_description = 'Gesamt für Destinatär' @admin.register(Rentmeister) class RentmeisterAdmin(admin.ModelAdmin): list_display = ['__str__', 'email', 'telefon', 'seit_datum', 'bis_datum', 'aktiv', 'monatliche_verguetung'] list_filter = ['aktiv', 'seit_datum', 'anrede'] search_fields = ['vorname', 'nachname', 'email', 'telefon', 'ort'] ordering = ['nachname', 'vorname'] readonly_fields = ['id', 'erstellt_am', 'aktualisiert_am'] fieldsets = ( ('Persönliche Daten', { 'fields': ('anrede', 'vorname', 'nachname', 'titel') }), ('Kontaktdaten', { 'fields': ('email', 'telefon', 'mobil', 'strasse', 'plz', 'ort') }), ('Bankdaten', { 'fields': ('iban', 'bic', 'bank_name'), 'classes': ['collapse'] }), ('Stiftungsdaten', { 'fields': ('seit_datum', 'bis_datum', 'aktiv', 'monatliche_verguetung', 'km_pauschale') }), ('Zusätzliche Informationen', { 'fields': ('notizen',), 'classes': ['collapse'] }), ('System', { 'fields': ('id', 'erstellt_am', 'aktualisiert_am'), 'classes': ['collapse'] }) ) @admin.register(StiftungsKonto) class StiftungsKontoAdmin(admin.ModelAdmin): list_display = ['kontoname', 'bank_name', 'konto_typ', 'saldo', 'saldo_datum', 'aktiv'] list_filter = ['konto_typ', 'aktiv', 'bank_name'] search_fields = ['kontoname', 'bank_name', 'iban'] ordering = ['bank_name', 'kontoname'] readonly_fields = ['id', 'erstellt_am', 'aktualisiert_am'] fieldsets = ( ('Kontodaten', { 'fields': ('kontoname', 'bank_name', 'iban', 'bic', 'konto_typ') }), ('Finanzdaten', { 'fields': ('saldo', 'saldo_datum', 'zinssatz', 'laufzeit_bis') }), ('Status', { 'fields': ('aktiv', 'notizen') }), ('System', { 'fields': ('id', 'erstellt_am', 'aktualisiert_am'), 'classes': ['collapse'] }) ) @admin.register(Verwaltungskosten) class VerwaltungskostenAdmin(admin.ModelAdmin): list_display = ['bezeichnung', 'kategorie', 'betrag', 'datum', 'status', 'rentmeister', 'konto'] list_filter = ['kategorie', 'status', 'datum', 'rentmeister', 'konto'] search_fields = ['bezeichnung', 'lieferant_firma', 'rechnungsnummer', 'beschreibung'] ordering = ['-datum', '-erstellt_am'] readonly_fields = ['id', 'erstellt_am', 'aktualisiert_am'] date_hierarchy = 'datum' fieldsets = ( ('Grunddaten', { 'fields': ('bezeichnung', 'kategorie', 'betrag', 'datum', 'status') }), ('Zuordnung', { 'fields': ('rentmeister', 'konto') }), ('Lieferant/Rechnung', { 'fields': ('lieferant_firma', 'rechnungsnummer'), 'classes': ['collapse'] }), ('Fahrtkosten', { 'fields': ('km_anzahl', 'km_satz', 'von_ort', 'nach_ort', 'zweck'), 'classes': ['collapse'], 'description': 'Nur für Kategorie "Fahrtkosten" relevant' }), ('Zusätzliche Informationen', { 'fields': ('beschreibung', 'notizen'), 'classes': ['collapse'] }), ('System', { 'fields': ('id', 'erstellt_am', 'aktualisiert_am'), 'classes': ['collapse'] }) ) @admin.register(BankTransaction) class BankTransactionAdmin(admin.ModelAdmin): list_display = [ 'datum', 'konto', 'betrag', 'empfaenger_zahlungspflichtiger', 'transaction_type', 'status', 'verwaltungskosten' ] list_filter = [ 'konto', 'transaction_type', 'status', 'datum', 'importiert_am' ] search_fields = [ 'verwendungszweck', 'empfaenger_zahlungspflichtiger', 'referenz' ] readonly_fields = ['importiert_am', 'import_datei'] ordering = ['-datum', '-importiert_am'] fieldsets = ( ('Basisdaten', { 'fields': ('konto', 'datum', 'valuta', 'betrag', 'waehrung') }), ('Transaktionsdetails', { 'fields': ('verwendungszweck', 'empfaenger_zahlungspflichtiger', 'iban_gegenpartei', 'bic_gegenpartei', 'referenz', 'transaction_type') }), ('Verwaltung', { 'fields': ('status', 'kommentare', 'verwaltungskosten') }), ('Import-Information', { 'fields': ('import_datei', 'importiert_am', 'saldo_nach_buchung'), 'classes': ('collapse',) }), ) def get_queryset(self, request): return super().get_queryset(request).select_related('konto', 'verwaltungskosten') @admin.register(AuditLog) class AuditLogAdmin(admin.ModelAdmin): list_display = ['timestamp', 'username', 'action', 'entity_type', 'entity_name', 'ip_address'] list_filter = ['action', 'entity_type', 'timestamp', 'username'] search_fields = ['username', 'entity_name', 'description', 'ip_address'] readonly_fields = ['id', 'timestamp', 'user', 'username', 'action', 'entity_type', 'entity_id', 'entity_name', 'description', 'changes', 'ip_address', 'user_agent', 'session_key'] ordering = ['-timestamp'] date_hierarchy = 'timestamp' fieldsets = ( ('Benutzer und Zeit', { 'fields': ('timestamp', 'user', 'username', 'session_key') }), ('Aktion', { 'fields': ('action', 'entity_type', 'entity_id', 'entity_name', 'description') }), ('Änderungsdetails', { 'fields': ('changes',), 'classes': ['collapse'] }), ('Request-Informationen', { 'fields': ('ip_address', 'user_agent'), 'classes': ['collapse'] }), ('System', { 'fields': ('id',), 'classes': ['collapse'] }) ) def has_add_permission(self, request): return False # Don't allow manual creation def has_change_permission(self, request, obj=None): return False # Don't allow editing @admin.register(BackupJob) class BackupJobAdmin(admin.ModelAdmin): list_display = ['created_at', 'backup_type', 'status', 'backup_size_display', 'duration_display', 'created_by'] list_filter = ['backup_type', 'status', 'created_at'] search_fields = ['backup_filename', 'created_by__username'] readonly_fields = ['id', 'created_at', 'started_at', 'completed_at', 'backup_size', 'get_duration'] ordering = ['-created_at'] fieldsets = ( ('Job-Details', { 'fields': ('backup_type', 'status', 'created_by') }), ('Zeitpunkte', { 'fields': ('created_at', 'started_at', 'completed_at', 'get_duration') }), ('Ergebnis', { 'fields': ('backup_filename', 'backup_size', 'database_size', 'files_count') }), ('Fehlerbehandlung', { 'fields': ('error_message',), 'classes': ['collapse'] }), ('System', { 'fields': ('id',), 'classes': ['collapse'] }) ) def backup_size_display(self, obj): return obj.get_size_display() backup_size_display.short_description = 'Backup-Größe' def duration_display(self, obj): duration = obj.get_duration() if duration: return f"{duration.total_seconds():.1f}s" return "-" duration_display.short_description = "Dauer" def has_add_permission(self, request): return False # Use the web interface for creating backups @admin.register(AppConfiguration) class AppConfigurationAdmin(admin.ModelAdmin): list_display = ['display_name', 'key', 'value_display', 'category', 'setting_type', 'is_active', 'updated_at'] list_filter = ['category', 'setting_type', 'is_active'] search_fields = ['key', 'display_name', 'description'] readonly_fields = ['id', 'created_at', 'updated_at'] ordering = ['category', 'order', 'display_name'] fieldsets = ( ('Basic Information', { 'fields': ('key', 'display_name', 'description', 'category', 'setting_type') }), ('Value Configuration', { 'fields': ('value', 'default_value') }), ('Options', { 'fields': ('is_active', 'is_system', 'order') }), ('Metadata', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) def value_display(self, obj): """Display value with type formatting""" value = obj.value if obj.setting_type == 'boolean': icon = '✅' if obj.get_typed_value() else '❌' return format_html('{} {}', icon, value) elif obj.setting_type == 'url': return format_html('{}', value, value[:50] + '...' if len(value) > 50 else value) elif len(value) > 100: return value[:100] + '...' return value value_display.short_description = 'Current Value' def get_readonly_fields(self, request, obj=None): readonly = list(self.readonly_fields) if obj and obj.is_system: readonly.extend(['key', 'setting_type', 'is_system']) return readonly @admin.register(DestinataerUnterstuetzung) class DestinataerUnterstuetzungAdmin(admin.ModelAdmin): list_display = ['__str__', 'destinataer', 'betrag', 'faellig_am', 'status', 'wiederkehrend_von', 'ausgezahlt_am'] list_filter = ['status', 'faellig_am', 'erstellt_am', 'konto'] search_fields = ['destinataer__vorname', 'destinataer__nachname', 'beschreibung', 'empfaenger_name'] readonly_fields = ['id', 'erstellt_am', 'aktualisiert_am'] fieldsets = ( ('Grundinformationen', { 'fields': ('destinataer', 'konto', 'betrag', 'faellig_am', 'status', 'beschreibung') }), ('Überweisungsdaten', { 'fields': ('empfaenger_iban', 'empfaenger_name', 'verwendungszweck') }), ('Zahlungsinformationen', { 'fields': ('ausgezahlt_am', 'ausgezahlt_von') }), ('Wiederkehrend', { 'fields': ('wiederkehrend_von',) }), ('Metadaten', { 'fields': ('id', 'erstellt_am', 'aktualisiert_am'), 'classes': ('collapse',) }), ) @admin.register(UnterstuetzungWiederkehrend) class UnterstuetzungWiederkehrendAdmin(admin.ModelAdmin): list_display = ['__str__', 'destinataer', 'betrag', 'intervall', 'aktiv', 'naechste_generierung'] list_filter = ['intervall', 'aktiv', 'erstellt_am'] search_fields = ['destinataer__vorname', 'destinataer__nachname', 'beschreibung', 'empfaenger_name'] readonly_fields = ['id', 'erstellt_am'] fieldsets = ( ('Grundinformationen', { 'fields': ('destinataer', 'konto', 'betrag', 'intervall', 'beschreibung', 'aktiv') }), ('Überweisungsdaten', { 'fields': ('empfaenger_iban', 'empfaenger_name', 'verwendungszweck') }), ('Zeitplanung', { 'fields': ('erste_zahlung_am', 'letzte_zahlung_am', 'naechste_generierung') }), ('Metadaten', { 'fields': ('id', 'erstellt_von', 'erstellt_am'), 'classes': ('collapse',) }), ) @admin.register(models.HelpBox) class HelpBoxAdmin(admin.ModelAdmin): list_display = ['get_page_display', 'title', 'is_active', 'updated_at', 'updated_by'] list_filter = ['page_key', 'is_active', 'updated_at'] search_fields = ['title', 'content'] fieldsets = ( ('Grundinformationen', { 'fields': ('page_key', 'title', 'is_active') }), ('Inhalt', { 'fields': ('content',), 'description': 'Markdown wird unterstützt: **fett**, *kursiv*, `code`, [Link](url), Listen mit - oder 1.' }), ('Metadaten', { 'fields': ('created_at', 'updated_at', 'created_by', 'updated_by'), 'classes': ('collapse',) }), ) readonly_fields = ['created_at', 'updated_at'] def get_page_display(self, obj): return obj.get_page_key_display() get_page_display.short_description = "Seite" def save_model(self, request, obj, form, change): if not change: # Neues Objekt obj.created_by = request.user.username obj.updated_by = request.user.username super().save_model(request, obj, form, change) # Customize admin site admin.site.site_header = "Stiftungsverwaltung Administration" admin.site.site_title = "Stiftungsverwaltung Admin" admin.site.index_title = "Willkommen zur Stiftungsverwaltung"