Files
stiftung-management-system/app/stiftung/admin.py
Stiftung CEO Agent 4b21f553c3
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
feat: Email-Eingangsverarbeitung für Destinatäre implementieren
Neues System zur automatischen Verarbeitung eingehender E-Mails von
Destinatären. IMAP-Polling alle 15 Minuten via Celery Beat, automatische
Zuordnung zu Destinatären anhand der E-Mail-Adresse, Upload von Anhängen
zu Paperless-NGX.

Umfasst:
- DestinataerEmailEingang Model mit Status-Tracking
- Celery Task für IMAP-Polling und Paperless-Integration
- Web-UI (Liste + Detail) mit Such- und Filterfunktion
- Admin-Interface mit Bulk-Actions
- Agent-Dokumentation (SysAdmin, RentmeisterAI)
- Dev-Environment Modernisierung (docker compose v2)

Reviewed by: SysAdmin (STI-15), RentmeisterAI (STI-16)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:11:22 +00:00

1235 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.contrib import admin
from django.db.models import Count, Sum
from django.urls import reverse
from django.utils import timezone
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from . import models
from .models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
CSVImport, Destinataer, DestinataerEmailEingang,
DestinataerUnterstuetzung,
DokumentLink, Foerderung, Land, LandVerpachtung, Paechter, Person,
Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend,
Verwaltungskosten, VierteljahresNachweis)
@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('<span class="badge bg-{}">{:.1f}%</span>', 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(
'<span style="font-family: monospace;">{}</span>', 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(
'<span style="font-family: monospace;">{}</span>', 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(
'<span style="font-family: monospace;">{}</span>', 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('<span style="color: {};">{:.1f}%</span>', 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(LandVerpachtung)
class LandVerpachtungAdmin(admin.ModelAdmin):
list_display = [
"land",
"paechter",
"pachtzins_pauschal",
"pachtbeginn",
"pachtende",
"status_display",
"erstellt_am",
]
list_filter = ["status", "pachtbeginn", "pachtende", "erstellt_am"]
search_fields = ["land__lfd_nr", "land__gemeinde", "paechter__vorname", "paechter__nachname", "vertragsnummer"]
ordering = ["-erstellt_am"]
readonly_fields = ["id", "erstellt_am", "aktualisiert_am"]
fieldsets = (
("Verpachtungsdetails", {
"fields": ("land", "paechter", "vertragsnummer", "status")
}),
("Laufzeit", {
"fields": ("pachtbeginn", "pachtende", "verlaengerung_klausel")
}),
("Fläche", {
"fields": ("verpachtete_flaeche",)
}),
("Pachtzins", {
"fields": ("pachtzins_pauschal", "pachtzins_pro_ha", "zahlungsweise")
}),
("Umsatzsteuer", {
"fields": ("ust_option", "ust_satz"),
"classes": ("collapse",)
}),
("Umlagen", {
"fields": ("grundsteuer_umlage", "versicherungen_umlage", "verbandsbeitraege_umlage", "jagdpacht_anteil_umlage"),
"classes": ("collapse",)
}),
("Zusatzinformationen", {
"fields": ("bemerkungen",),
"classes": ("collapse",)
}),
("System", {
"fields": ("id", "erstellt_am", "aktualisiert_am"),
"classes": ("collapse",)
}),
)
def status_display(self, obj):
colors = {
'aktiv': 'green',
'beendet': 'red',
'geplant': 'orange',
'gekündigt': 'red'
}
color = colors.get(obj.status, 'black')
return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
color,
obj.get_status_display()
)
status_display.short_description = "Status"
@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(
'<a href="{}">{}</a>',
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(
'<a href="{}" target="_blank">{}</a>',
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)
@admin.register(VierteljahresNachweis)
class VierteljahresNachweisAdmin(admin.ModelAdmin):
list_display = [
"destinataer",
"jahr",
"quartal",
"status",
"completion_percentage",
"faelligkeitsdatum",
"is_overdue_display",
"eingereicht_am",
"geprueft_von",
]
list_filter = [
"jahr",
"quartal",
"status",
"studiennachweis_erforderlich",
"studiennachweis_eingereicht",
"einkommenssituation_bestaetigt",
"vermogenssituation_bestaetigt",
"faelligkeitsdatum",
]
search_fields = [
"destinataer__vorname",
"destinataer__nachname",
"destinataer__email",
]
readonly_fields = [
"id",
"erstellt_am",
"aktualisiert_am",
"completion_percentage",
"is_overdue_display",
]
ordering = ["-jahr", "-quartal", "destinataer__nachname"]
fieldsets = (
(
"Grundinformationen",
{
"fields": (
"destinataer",
"jahr",
"quartal",
"status",
"faelligkeitsdatum",
)
},
),
(
"Studiennachweis",
{
"fields": (
"studiennachweis_erforderlich",
"studiennachweis_eingereicht",
"studiennachweis_datei",
"studiennachweis_bemerkung",
),
"classes": ("collapse",),
},
),
(
"Einkommenssituation",
{
"fields": (
"einkommenssituation_bestaetigt",
"einkommenssituation_text",
"einkommenssituation_datei",
),
"classes": ("collapse",),
},
),
(
"Vermögenssituation",
{
"fields": (
"vermogenssituation_bestaetigt",
"vermogenssituation_text",
"vermogenssituation_datei",
),
"classes": ("collapse",),
},
),
(
"Weitere Dokumente",
{
"fields": (
"weitere_dokumente",
"weitere_dokumente_beschreibung",
),
"classes": ("collapse",),
},
),
(
"Verwaltung & Prüfung",
{
"fields": (
"interne_notizen",
"eingereicht_am",
"geprueft_am",
"geprueft_von",
),
"classes": ("collapse",),
},
),
(
"Metadaten",
{
"fields": (
"id",
"erstellt_am",
"aktualisiert_am",
"completion_percentage",
"is_overdue_display",
)
},
),
)
def completion_percentage(self, obj):
"""Show completion percentage as colored badge"""
percentage = obj.get_completion_percentage()
if percentage == 100:
color = "success"
elif percentage >= 70:
color = "info"
elif percentage >= 30:
color = "warning"
else:
color = "danger"
return format_html(
'<span class="badge bg-{}">{} %</span>',
color,
percentage
)
completion_percentage.short_description = "Fortschritt"
def is_overdue_display(self, obj):
"""Display overdue status with icon"""
if obj.is_overdue():
return format_html(
'<span class="text-danger"><i class="fas fa-exclamation-triangle"></i> Ja</span>'
)
return format_html(
'<span class="text-success"><i class="fas fa-check"></i> Nein</span>'
)
is_overdue_display.short_description = "Überfällig"
actions = ["mark_as_approved", "mark_as_needs_revision"]
def mark_as_approved(self, request, queryset):
"""Bulk action to approve submitted confirmations"""
count = 0
for nachweis in queryset.filter(status="eingereicht"):
nachweis.status = "geprueft"
nachweis.geprueft_am = timezone.now()
nachweis.geprueft_von = request.user
nachweis.save()
count += 1
if count:
self.message_user(
request,
f"{count} Nachweise wurden als geprüft und freigegeben markiert."
)
else:
self.message_user(
request,
"Keine eingereichten Nachweise gefunden.",
level="warning"
)
mark_as_approved.short_description = "Ausgewählte Nachweise freigeben"
def mark_as_needs_revision(self, request, queryset):
"""Bulk action to mark confirmations as needing revision"""
count = queryset.exclude(status__in=["offen", "nachbesserung"]).update(
status="nachbesserung"
)
if count:
self.message_user(
request,
f"{count} Nachweise wurden als nachbesserungsbedürftig markiert."
)
mark_as_needs_revision.short_description = "Nachbesserung erforderlich markieren"
@admin.register(DestinataerEmailEingang)
class DestinataerEmailEingangAdmin(admin.ModelAdmin):
list_display = [
"eingangsdatum",
"absender_email",
"absender_name",
"destinataer_link",
"betreff_kurz",
"anzahl_anhaenge",
"status",
"created_at",
]
list_filter = ["status", "eingangsdatum"]
search_fields = [
"absender_email",
"absender_name",
"betreff",
"destinataer__vorname",
"destinataer__nachname",
]
readonly_fields = ["created_at", "absender_email", "absender_name", "eingangsdatum",
"email_text", "paperless_dokument_ids", "fehler_details"]
raw_id_fields = ["destinataer", "quartalsnachweis"]
date_hierarchy = "eingangsdatum"
ordering = ["-eingangsdatum"]
fieldsets = [
("E-Mail-Metadaten", {
"fields": ["eingangsdatum", "absender_name", "absender_email", "betreff"],
}),
("Zuordnung", {
"fields": ["destinataer", "status", "quartalsnachweis"],
}),
("Inhalt & Anhänge", {
"fields": ["email_text", "paperless_dokument_ids"],
}),
("Notizen & Fehler", {
"fields": ["notizen", "fehler_details"],
"classes": ["collapse"],
}),
("System", {
"fields": ["created_at"],
"classes": ["collapse"],
}),
]
def destinataer_link(self, obj):
if obj.destinataer:
url = reverse("admin:stiftung_destinataer_change", args=[obj.destinataer.pk])
return format_html('<a href="{}">{}</a>', url, obj.destinataer)
return format_html('<span style="color:red;"></span>')
destinataer_link.short_description = "Destinatär"
def betreff_kurz(self, obj):
return (obj.betreff or "")[:60]
betreff_kurz.short_description = "Betreff"
def anzahl_anhaenge(self, obj):
n = len(obj.paperless_dokument_ids or [])
return n if n else ""
anzahl_anhaenge.short_description = "Anhänge"
actions = ["mark_verarbeitet"]
def mark_verarbeitet(self, request, queryset):
updated = queryset.filter(status__in=["neu", "zugewiesen"]).update(status="verarbeitet")
self.message_user(request, f"{updated} E-Mail(s) als verarbeitet markiert.")
mark_verarbeitet.short_description = "Als verarbeitet markieren"
# Customize admin site
admin.site.site_header = "Stiftungsverwaltung Administration"
admin.site.site_title = "Stiftungsverwaltung Admin"
admin.site.index_title = "Willkommen zur Stiftungsverwaltung"