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>
1235 lines
36 KiB
Python
1235 lines
36 KiB
Python
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"
|