Phase 0: forms.py, admin.py und views.py in Domain-Packages aufteilen
- forms.py → forms/ Package (8 Domänen: destinataere, land, finanzen, foerderung, dokumente, veranstaltung, system, geschichte) - admin.py → admin/ Package (7 Domänen, alle 22 @admin.register dekoriert) - views.py (8845 Zeilen) → views/ Package (10 Domänen: dashboard, destinataere, land, paechter, finanzen, foerderung, dokumente, unterstuetzungen, veranstaltung, geschichte, system) - __init__.py in jedem Package re-exportiert alle Symbole für Rückwärtskompatibilität - urls.py bleibt unverändert (funktioniert durch Re-Exports) - Django system check: 0 Fehler, alle URL-Auflösungen funktionieren Keine funktionalen Änderungen – reine Strukturverbesserung für Vision 2026. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
14
app/stiftung/admin/__init__.py
Normal file
14
app/stiftung/admin/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from . import destinataere # noqa: F401
|
||||
from . import land # noqa: F401
|
||||
from . import finanzen # noqa: F401
|
||||
from . import foerderung # noqa: F401
|
||||
from . import dokumente # noqa: F401
|
||||
from . import veranstaltung # noqa: F401
|
||||
from . import system # noqa: F401
|
||||
|
||||
# Customize admin site
|
||||
admin.site.site_header = "Stiftungsverwaltung Administration"
|
||||
admin.site.site_title = "Stiftungsverwaltung Admin"
|
||||
admin.site.index_title = "Willkommen zur Stiftungsverwaltung"
|
||||
178
app/stiftung/admin/destinataere.py
Normal file
178
app/stiftung/admin/destinataere.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
from ..models import Destinataer, DestinataerEmailEingang, DestinataerUnterstuetzung
|
||||
|
||||
|
||||
@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(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(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"
|
||||
20
app/stiftung/admin/dokumente.py
Normal file
20
app/stiftung/admin/dokumente.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from ..models import DokumentLink
|
||||
|
||||
|
||||
@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",)}),
|
||||
)
|
||||
191
app/stiftung/admin/finanzen.py
Normal file
191
app/stiftung/admin/finanzen.py
Normal file
@@ -0,0 +1,191 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from ..models import BankTransaction, Rentmeister, StiftungsKonto, Verwaltungskosten
|
||||
|
||||
|
||||
@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")
|
||||
)
|
||||
69
app/stiftung/admin/foerderung.py
Normal file
69
app/stiftung/admin/foerderung.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from django.contrib import admin
|
||||
from django.db.models import Sum
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
from ..models import Foerderung
|
||||
|
||||
|
||||
@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"
|
||||
206
app/stiftung/admin/land.py
Normal file
206
app/stiftung/admin/land.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
|
||||
from ..models import Land, LandVerpachtung, Paechter
|
||||
|
||||
|
||||
@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(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"
|
||||
579
app/stiftung/admin/system.py
Normal file
579
app/stiftung/admin/system.py
Normal file
@@ -0,0 +1,579 @@
|
||||
from django.contrib import admin
|
||||
from django.db.models import Sum
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
|
||||
from .. import models
|
||||
from ..models import (AppConfiguration, AuditLog, BackupJob, CSVImport, Person,
|
||||
UnterstuetzungWiederkehrend, 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(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(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(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(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"
|
||||
190
app/stiftung/admin/veranstaltung.py
Normal file
190
app/stiftung/admin/veranstaltung.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
|
||||
from ..models import BriefVorlage, Veranstaltung, Veranstaltungsteilnehmer
|
||||
|
||||
|
||||
class VeranstaltungsteilnehmerInline(admin.TabularInline):
|
||||
model = Veranstaltungsteilnehmer
|
||||
extra = 1
|
||||
fields = [
|
||||
"anrede", "vorname", "nachname", "strasse", "plz", "ort",
|
||||
"email", "rsvp_status", "bemerkungen",
|
||||
]
|
||||
|
||||
|
||||
class BriefVorlageWidget(forms.Textarea):
|
||||
"""Erweitertes Textarea-Widget für HTML-Briefvorlagen mit Editor-Panel und Platzhalter-Hilfe."""
|
||||
|
||||
class Media:
|
||||
js = ["stiftung/js/briefvorlage_editor.js"]
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
default_attrs = {"rows": 18, "cols": 80, "class": "briefvorlage-textarea", "style": "font-family: monospace; font-size: 13px;"}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
super().__init__(attrs=default_attrs)
|
||||
|
||||
|
||||
class VeranstaltungAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Veranstaltung
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"briefvorlage": BriefVorlageWidget(),
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Veranstaltung)
|
||||
class VeranstaltungAdmin(admin.ModelAdmin):
|
||||
form = VeranstaltungAdminForm
|
||||
list_display = [
|
||||
"titel", "datum", "uhrzeit", "ort", "status",
|
||||
"get_teilnehmer_count", "get_zugesagte_count", "budget_pro_person",
|
||||
]
|
||||
list_filter = ["status", "datum"]
|
||||
search_fields = ["titel", "ort", "beschreibung"]
|
||||
ordering = ["-datum"]
|
||||
readonly_fields = ["id", "erstellt_am", "aktualisiert_am", "serienbrief_aktionen", "platzhalter_dokumentation"]
|
||||
inlines = [VeranstaltungsteilnehmerInline]
|
||||
|
||||
fieldsets = (
|
||||
("Grunddaten", {"fields": ("titel", "datum", "uhrzeit", "status")}),
|
||||
("Veranstaltungsort", {"fields": ("ort", "adresse")}),
|
||||
("Details", {"fields": ("beschreibung", "budget_pro_person")}),
|
||||
(
|
||||
"Serienbrief – Vorlage",
|
||||
{
|
||||
"fields": (
|
||||
"platzhalter_dokumentation",
|
||||
"betreff",
|
||||
"briefvorlage",
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Serienbrief – Unterschriften & Aktionen",
|
||||
{
|
||||
"fields": (
|
||||
"unterschrift_1_name", "unterschrift_1_titel",
|
||||
"unterschrift_2_name", "unterschrift_2_titel",
|
||||
"serienbrief_aktionen",
|
||||
),
|
||||
},
|
||||
),
|
||||
("System", {"fields": ("id", "erstellt_am", "aktualisiert_am"), "classes": ("collapse",)}),
|
||||
)
|
||||
|
||||
def get_teilnehmer_count(self, obj):
|
||||
return obj.get_teilnehmer_count()
|
||||
get_teilnehmer_count.short_description = "Teilnehmer gesamt"
|
||||
|
||||
def get_zugesagte_count(self, obj):
|
||||
return obj.get_zugesagte_count()
|
||||
get_zugesagte_count.short_description = "Zugesagt"
|
||||
|
||||
def platzhalter_dokumentation(self, obj):
|
||||
return format_html(
|
||||
"""<div class="help" style="background:#f8f9fa;border:1px solid #dee2e6;border-radius:4px;padding:10px 14px;margin-bottom:4px;">
|
||||
<strong>Verfügbare Platzhalter im Brieftext:</strong><br>
|
||||
<table style="margin-top:6px;border-collapse:collapse;font-size:13px;">
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ anrede }}}}</td><td>Anredetitel (Herr / Frau)</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ vorname }}}}</td><td>Vorname des Empfängers</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ nachname }}}}</td><td>Nachname des Empfängers</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ strasse }}}}</td><td>Straße und Hausnummer</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ plz }}}}</td><td>Postleitzahl</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ ort }}}}</td><td>Wohnort des Empfängers</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ datum }}}}</td><td>Datum der Veranstaltung (z.B. Freitag, 17. April 2026)</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ uhrzeit }}}}</td><td>Uhrzeit der Veranstaltung (z.B. 19:00 Uhr)</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ veranstaltungsort }}}}</td><td>Name des Veranstaltungsorts / Gasthaus</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ gasthaus_adresse }}}}</td><td>Adresse des Gasthauses</td></tr>
|
||||
</table>
|
||||
<div style="margin-top:8px;font-size:12px;color:#6c757d;">
|
||||
Platzhalter werden beim PDF-Export automatisch mit den Empfänger- und Veranstaltungsdaten befüllt.
|
||||
Tipp: Vorlagen unter <a href="/admin/stiftung/briefvorlage/" target="_blank">Verwaltung → Briefvorlagen</a> speichern und wiederverwenden.
|
||||
</div>
|
||||
</div>"""
|
||||
)
|
||||
platzhalter_dokumentation.short_description = "Platzhalter-Dokumentation"
|
||||
platzhalter_dokumentation.allow_tags = True
|
||||
|
||||
def serienbrief_aktionen(self, obj):
|
||||
if obj.pk:
|
||||
from django.urls import reverse as url_reverse
|
||||
pdf_url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[obj.pk])
|
||||
vorschau_url = url_reverse("stiftung:veranstaltung_serienbrief_vorschau", args=[obj.pk])
|
||||
return format_html(
|
||||
'<a href="{}" target="_blank" class="button" style="margin-right:8px;">Serienbrief-PDF generieren</a>'
|
||||
'<a href="{}" target="_blank" class="button default">Vorschau im Browser</a>',
|
||||
pdf_url, vorschau_url,
|
||||
)
|
||||
return "–"
|
||||
serienbrief_aktionen.short_description = "Aktionen"
|
||||
|
||||
actions = ["generate_serienbrief"]
|
||||
|
||||
def generate_serienbrief(self, request, queryset):
|
||||
if queryset.count() != 1:
|
||||
self.message_user(
|
||||
request,
|
||||
"Bitte genau eine Veranstaltung auswählen.",
|
||||
level="error",
|
||||
)
|
||||
return
|
||||
from django.urls import reverse as url_reverse
|
||||
from django.shortcuts import redirect
|
||||
veranstaltung = queryset.first()
|
||||
url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[veranstaltung.pk])
|
||||
return redirect(url)
|
||||
generate_serienbrief.short_description = "Serienbrief-PDF generieren"
|
||||
|
||||
|
||||
@admin.register(BriefVorlage)
|
||||
class BriefVorlageAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "beschreibung_kurz", "erstellt_am", "aktualisiert_am"]
|
||||
search_fields = ["name", "beschreibung"]
|
||||
ordering = ["name"]
|
||||
readonly_fields = ["erstellt_am", "aktualisiert_am"]
|
||||
|
||||
fieldsets = (
|
||||
(None, {"fields": ("name", "beschreibung")}),
|
||||
(
|
||||
"Briefinhalt",
|
||||
{
|
||||
"fields": ("betreff", "briefvorlage"),
|
||||
"description": (
|
||||
"Verfügbare Platzhalter: {{ anrede }}, {{ vorname }}, {{ nachname }}, "
|
||||
"{{ strasse }}, {{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, "
|
||||
"{{ veranstaltungsort }}, {{ gasthaus_adresse }}"
|
||||
),
|
||||
},
|
||||
),
|
||||
("System", {"fields": ("erstellt_am", "aktualisiert_am"), "classes": ("collapse",)}),
|
||||
)
|
||||
|
||||
def beschreibung_kurz(self, obj):
|
||||
return obj.beschreibung[:80] + "…" if len(obj.beschreibung) > 80 else obj.beschreibung
|
||||
beschreibung_kurz.short_description = "Beschreibung"
|
||||
|
||||
|
||||
@admin.register(Veranstaltungsteilnehmer)
|
||||
class VeranstaltungsteilnehmerAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"nachname", "vorname", "anrede", "veranstaltung", "rsvp_status", "ort",
|
||||
]
|
||||
list_filter = ["rsvp_status", "veranstaltung", "anrede"]
|
||||
search_fields = ["vorname", "nachname", "ort", "email"]
|
||||
ordering = ["veranstaltung", "nachname", "vorname"]
|
||||
readonly_fields = ["id", "erstellt_am"]
|
||||
|
||||
fieldsets = (
|
||||
("Zuordnung", {"fields": ("veranstaltung", "paechter", "destinataer")}),
|
||||
(
|
||||
"Persönliche Daten",
|
||||
{"fields": ("anrede", "vorname", "nachname", "email")},
|
||||
),
|
||||
("Adresse", {"fields": ("strasse", "plz", "ort")}),
|
||||
("RSVP", {"fields": ("rsvp_status", "bemerkungen")}),
|
||||
("System", {"fields": ("id", "erstellt_am"), "classes": ("collapse",)}),
|
||||
)
|
||||
Reference in New Issue
Block a user