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:
File diff suppressed because it is too large
Load Diff
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",)}),
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
58
app/stiftung/forms/__init__.py
Normal file
58
app/stiftung/forms/__init__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from .destinataere import (DestinataerForm, DestinataerNotizForm,
|
||||
DestinataerUnterstuetzungForm,
|
||||
UnterstuetzungForm, UnterstuetzungMarkAsPaidForm,
|
||||
UnterstuetzungWiederkehrendForm,
|
||||
VierteljahresNachweisForm)
|
||||
from .dokumente import DokumentLinkForm
|
||||
from .finanzen import (BankImportForm, BankTransactionForm, RentmeisterForm,
|
||||
StiftungsKontoForm, VerwaltungskostenForm)
|
||||
from .foerderung import FoerderungForm
|
||||
from .geschichte import GeschichteBildForm, GeschichteSeiteForm
|
||||
from .land import LandAbrechnungForm, LandForm, LandVerpachtungForm, PaechterForm
|
||||
from .system import (BackupTokenRegenerateForm, PasswordChangeForm, PersonForm,
|
||||
TwoFactorDisableForm, TwoFactorSetupForm,
|
||||
TwoFactorVerifyForm, UserCreationForm, UserPermissionForm,
|
||||
UserUpdateForm)
|
||||
from .veranstaltung import VeranstaltungForm, VeranstaltungsteilnehmerForm
|
||||
|
||||
__all__ = [
|
||||
# destinataere
|
||||
"DestinataerForm",
|
||||
"DestinataerNotizForm",
|
||||
"DestinataerUnterstuetzungForm",
|
||||
"UnterstuetzungForm",
|
||||
"UnterstuetzungMarkAsPaidForm",
|
||||
"UnterstuetzungWiederkehrendForm",
|
||||
"VierteljahresNachweisForm",
|
||||
# dokumente
|
||||
"DokumentLinkForm",
|
||||
# finanzen
|
||||
"BankImportForm",
|
||||
"BankTransactionForm",
|
||||
"RentmeisterForm",
|
||||
"StiftungsKontoForm",
|
||||
"VerwaltungskostenForm",
|
||||
# foerderung
|
||||
"FoerderungForm",
|
||||
# geschichte
|
||||
"GeschichteBildForm",
|
||||
"GeschichteSeiteForm",
|
||||
# land
|
||||
"LandAbrechnungForm",
|
||||
"LandForm",
|
||||
"LandVerpachtungForm",
|
||||
"PaechterForm",
|
||||
# system
|
||||
"BackupTokenRegenerateForm",
|
||||
"PasswordChangeForm",
|
||||
"PersonForm",
|
||||
"TwoFactorDisableForm",
|
||||
"TwoFactorSetupForm",
|
||||
"TwoFactorVerifyForm",
|
||||
"UserCreationForm",
|
||||
"UserPermissionForm",
|
||||
"UserUpdateForm",
|
||||
# veranstaltung
|
||||
"VeranstaltungForm",
|
||||
"VeranstaltungsteilnehmerForm",
|
||||
]
|
||||
428
app/stiftung/forms/destinataere.py
Normal file
428
app/stiftung/forms/destinataere.py
Normal file
@@ -0,0 +1,428 @@
|
||||
from django import forms
|
||||
from django.utils import timezone
|
||||
|
||||
from ..models import (Destinataer, DestinataerNotiz, DestinataerUnterstuetzung,
|
||||
UnterstuetzungWiederkehrend, VierteljahresNachweis)
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class DestinataerForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Destinatären"""
|
||||
|
||||
class Meta:
|
||||
model = Destinataer
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"anrede": forms.Select(attrs={"class": "form-select"}),
|
||||
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"titel": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"strasse": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"plz": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"telefon": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"mobil": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
||||
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
"ist_abkoemmling": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"haushaltsgroesse": forms.NumberInput(
|
||||
attrs={"class": "form-control", "min": 1}
|
||||
),
|
||||
"vermoegen": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"unterstuetzung_bestaetigt": forms.CheckboxInput(
|
||||
attrs={"class": "form-check-input"}
|
||||
),
|
||||
"standard_konto": forms.Select(attrs={"class": "form-select"}, choices=[(None, "---")] + [(c.pk, str(c)) for c in getattr(Destinataer, 'konten_queryset', lambda: [])()]),
|
||||
"vierteljaehrlicher_betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"studiennachweis_erforderlich": forms.CheckboxInput(
|
||||
attrs={"class": "form-check-input"}
|
||||
),
|
||||
"letzter_studiennachweis": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"familienzweig": forms.Select(attrs={"class": "form-select"}),
|
||||
"berufsgruppe": forms.Select(attrs={"class": "form-select"}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field_name, field in self.fields.items():
|
||||
if field_name not in ["vorname", "nachname"]:
|
||||
field.required = False
|
||||
# Set choices for familienzweig and berufsgruppe to match model
|
||||
self.fields["familienzweig"].choices = [("", "Bitte wählen...")] + list(Destinataer.FAMILIENZWIG_CHOICES)
|
||||
self.fields["berufsgruppe"].choices = [("", "Bitte wählen...")] + list(Destinataer.BERUFSGRUPPE_CHOICES)
|
||||
# Set choices for standard_konto to allow blank
|
||||
self.fields["standard_konto"].empty_label = "---"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field_name, field in self.fields.items():
|
||||
if field_name not in ["vorname", "nachname"]:
|
||||
field.required = False
|
||||
|
||||
|
||||
class DestinataerUnterstuetzungForm(forms.ModelForm):
|
||||
"""Form für geplante/ausgeführte Destinatärunterstützungen"""
|
||||
|
||||
class Meta:
|
||||
model = DestinataerUnterstuetzung
|
||||
fields = [
|
||||
"destinataer",
|
||||
"konto",
|
||||
"betrag",
|
||||
"faellig_am",
|
||||
"status",
|
||||
"beschreibung",
|
||||
"empfaenger_iban",
|
||||
"empfaenger_name",
|
||||
"verwendungszweck",
|
||||
]
|
||||
widgets = {
|
||||
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
||||
"konto": forms.Select(attrs={"class": "form-select"}),
|
||||
"betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"faellig_am": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"beschreibung": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"empfaenger_iban": forms.TextInput(
|
||||
attrs={"class": "form-control", "placeholder": "DE89 3704 0044 0532 0130 00"}
|
||||
),
|
||||
"empfaenger_name": forms.TextInput(
|
||||
attrs={"class": "form-control", "placeholder": "Max Mustermann"}
|
||||
),
|
||||
"verwendungszweck": forms.TextInput(
|
||||
attrs={"class": "form-control", "placeholder": "Vierteljährliche Unterstützung Q1/2025"}
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Make faellig_am read-only for automatically generated quarterly payments
|
||||
self.is_auto_generated = False
|
||||
if self.instance and self.instance.pk and self.instance.beschreibung:
|
||||
if "Vierteljährliche Unterstützung" in self.instance.beschreibung and "(automatisch erstellt)" in self.instance.beschreibung:
|
||||
self.is_auto_generated = True
|
||||
|
||||
# Use a TextInput widget with readonly attribute to display the date
|
||||
from django import forms
|
||||
current_date = self.instance.faellig_am
|
||||
if current_date:
|
||||
self.fields['faellig_am'].widget = forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"readonly": True,
|
||||
"value": current_date.strftime('%d.%m.%Y'), # German date format
|
||||
"style": "background-color: #f8f9fa; cursor: not-allowed;"
|
||||
}
|
||||
)
|
||||
self.fields['faellig_am'].initial = current_date
|
||||
|
||||
self.fields['faellig_am'].help_text = "Fälligkeitsdatum wird automatisch basierend auf Quartal berechnet"
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# For auto-generated payments, preserve the original due date
|
||||
if self.is_auto_generated and self.instance and self.instance.pk:
|
||||
cleaned_data['faellig_am'] = self.instance.faellig_am
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class DestinataerNotizForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = DestinataerNotiz
|
||||
fields = ["titel", "text", "datei"]
|
||||
widgets = {
|
||||
"titel": forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "z.B. Telefonat vom 29.08.2025",
|
||||
}
|
||||
),
|
||||
"text": forms.Textarea(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"rows": 5,
|
||||
"placeholder": "Notiztext...",
|
||||
}
|
||||
),
|
||||
"datei": forms.ClearableFileInput(attrs={"class": "form-control"}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Make all fields optional
|
||||
self.fields["datei"].required = False
|
||||
self.fields["titel"].required = False
|
||||
self.fields["text"].required = False
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
titel = cleaned.get("titel", "").strip()
|
||||
text = cleaned.get("text", "").strip()
|
||||
if not (titel or text):
|
||||
raise forms.ValidationError(
|
||||
"Bitte geben Sie einen Titel oder einen Text ein."
|
||||
)
|
||||
return cleaned
|
||||
|
||||
|
||||
class UnterstuetzungForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Unterstützungen"""
|
||||
|
||||
# Special field for creating recurring payments
|
||||
ist_wiederkehrend = forms.BooleanField(
|
||||
required=False,
|
||||
label="Wiederkehrende Zahlung",
|
||||
help_text="Aktivieren Sie diese Option um automatisch wiederkehrende Zahlungen zu erstellen",
|
||||
)
|
||||
intervall = forms.ChoiceField(
|
||||
choices=[("", "--- Wählen Sie ein Intervall ---")]
|
||||
+ UnterstuetzungWiederkehrend.INTERVALL_CHOICES,
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
label="Zahlungsintervall",
|
||||
)
|
||||
letzte_zahlung_am = forms.DateField(
|
||||
required=False,
|
||||
widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
||||
label="Letzte Zahlung am (optional)",
|
||||
help_text="Leer lassen für unbegrenzte Wiederholung",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DestinataerUnterstuetzung
|
||||
fields = [
|
||||
"destinataer",
|
||||
"konto",
|
||||
"faellig_am",
|
||||
"betrag",
|
||||
"status",
|
||||
"beschreibung",
|
||||
"empfaenger_iban",
|
||||
"empfaenger_name",
|
||||
"verwendungszweck",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
||||
"konto": forms.Select(attrs={"class": "form-select"}),
|
||||
"faellig_am": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"beschreibung": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"empfaenger_iban": forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "DE89 3704 0044 0532 0130 00",
|
||||
}
|
||||
),
|
||||
"empfaenger_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"verwendungszweck": forms.TextInput(
|
||||
attrs={"class": "form-control", "maxlength": "140"}
|
||||
),
|
||||
}
|
||||
|
||||
labels = {
|
||||
"destinataer": "Destinatär",
|
||||
"konto": "Zahlungskonto",
|
||||
"faellig_am": "Fällig am",
|
||||
"betrag": "Betrag (€)",
|
||||
"status": "Status",
|
||||
"beschreibung": "Beschreibung",
|
||||
"empfaenger_iban": "Empfänger IBAN",
|
||||
"empfaenger_name": "Empfänger Name",
|
||||
"verwendungszweck": "Verwendungszweck",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Add onchange event to destinataer field for AJAX IBAN fetching
|
||||
self.fields["destinataer"].widget.attrs["onchange"] = "updateDestinataerInfo()"
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
ist_wiederkehrend = cleaned_data.get("ist_wiederkehrend")
|
||||
intervall = cleaned_data.get("intervall")
|
||||
|
||||
if ist_wiederkehrend and not intervall:
|
||||
raise forms.ValidationError(
|
||||
"Bitte wählen Sie ein Zahlungsintervall für wiederkehrende Zahlungen."
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class UnterstuetzungWiederkehrendForm(forms.ModelForm):
|
||||
"""Form für das Bearbeiten von wiederkehrenden Unterstützungsvorlagen"""
|
||||
|
||||
class Meta:
|
||||
model = UnterstuetzungWiederkehrend
|
||||
fields = [
|
||||
"destinataer",
|
||||
"konto",
|
||||
"betrag",
|
||||
"intervall",
|
||||
"beschreibung",
|
||||
"empfaenger_iban",
|
||||
"empfaenger_name",
|
||||
"verwendungszweck",
|
||||
"erste_zahlung_am",
|
||||
"letzte_zahlung_am",
|
||||
"aktiv",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
||||
"konto": forms.Select(attrs={"class": "form-select"}),
|
||||
"betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"intervall": forms.Select(attrs={"class": "form-select"}),
|
||||
"beschreibung": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"empfaenger_iban": forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "DE89 3704 0044 0532 0130 00",
|
||||
}
|
||||
),
|
||||
"empfaenger_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"verwendungszweck": forms.TextInput(
|
||||
attrs={"class": "form-control", "maxlength": "140"}
|
||||
),
|
||||
"erste_zahlung_am": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"letzte_zahlung_am": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
}
|
||||
|
||||
|
||||
class UnterstuetzungMarkAsPaidForm(forms.Form):
|
||||
"""Simple form to mark an Unterstützung as paid"""
|
||||
|
||||
ausgezahlt_am = forms.DateField(
|
||||
widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
||||
label="Ausgezahlt am",
|
||||
initial=timezone.now().date(),
|
||||
)
|
||||
|
||||
bemerkung = forms.CharField(
|
||||
widget=forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
label="Bemerkung (optional)",
|
||||
required=False,
|
||||
help_text="Optionale Notiz zur Zahlung",
|
||||
)
|
||||
|
||||
|
||||
class VierteljahresNachweisForm(forms.ModelForm):
|
||||
"""Form for quarterly confirmations (Vierteljahresnachweise)"""
|
||||
|
||||
class Meta:
|
||||
model = VierteljahresNachweis
|
||||
fields = [
|
||||
'studiennachweis_eingereicht',
|
||||
'studiennachweis_datei',
|
||||
'studiennachweis_bemerkung',
|
||||
'einkommenssituation_bestaetigt',
|
||||
'einkommenssituation_text',
|
||||
'einkommenssituation_datei',
|
||||
'vermogenssituation_bestaetigt',
|
||||
'vermogenssituation_text',
|
||||
'vermogenssituation_datei',
|
||||
'weitere_dokumente',
|
||||
'weitere_dokumente_beschreibung',
|
||||
'interne_notizen',
|
||||
]
|
||||
|
||||
widgets = {
|
||||
'studiennachweis_eingereicht': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'studiennachweis_datei': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
||||
'studiennachweis_bemerkung': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'einkommenssituation_bestaetigt': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'einkommenssituation_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Z.B. "Keine Änderungen seit letzter Meldung"'}),
|
||||
'einkommenssituation_datei': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
||||
'vermogenssituation_bestaetigt': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'vermogenssituation_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Z.B. "Keine Änderungen seit letzter Meldung"'}),
|
||||
'vermogenssituation_datei': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
||||
'weitere_dokumente': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
||||
'weitere_dokumente_beschreibung': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
||||
'interne_notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
}
|
||||
|
||||
labels = {
|
||||
'studiennachweis_erforderlich': 'Studiennachweis erforderlich',
|
||||
'studiennachweis_eingereicht': 'Studiennachweis eingereicht',
|
||||
'studiennachweis_datei': 'Studiennachweis (Datei)',
|
||||
'studiennachweis_bemerkung': 'Bemerkung zum Studiennachweis',
|
||||
'einkommenssituation_bestaetigt': 'Einkommenssituation bestätigt',
|
||||
'einkommenssituation_text': 'Einkommenssituation (Text)',
|
||||
'einkommenssituation_datei': 'Einkommenssituation (Datei)',
|
||||
'vermogenssituation_bestaetigt': 'Vermögenssituation bestätigt',
|
||||
'vermogenssituation_text': 'Vermögenssituation (Text)',
|
||||
'vermogenssituation_datei': 'Vermögenssituation (Datei)',
|
||||
'weitere_dokumente': 'Weitere Dokumente',
|
||||
'weitere_dokumente_beschreibung': 'Beschreibung weitere Dokumente',
|
||||
'interne_notizen': 'Interne Notizen (nur für Verwaltung)',
|
||||
}
|
||||
|
||||
help_texts = {
|
||||
'einkommenssituation_text': 'Z.B. "Keine Änderungen seit letzter Meldung" oder Details zu Änderungen',
|
||||
'vermogenssituation_text': 'Z.B. "Keine Änderungen seit letzter Meldung" oder Details zu Änderungen',
|
||||
'interne_notizen': 'Diese Notizen sind nur für die interne Verwaltung sichtbar',
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Validate that at least one form of confirmation is provided for income situation
|
||||
einkommenssituation_text = cleaned_data.get('einkommenssituation_text')
|
||||
einkommenssituation_datei = cleaned_data.get('einkommenssituation_datei')
|
||||
einkommenssituation_bestaetigt = cleaned_data.get('einkommenssituation_bestaetigt')
|
||||
|
||||
if einkommenssituation_bestaetigt and not einkommenssituation_text and not einkommenssituation_datei:
|
||||
raise ValidationError(
|
||||
'Wenn die Einkommenssituation bestätigt wird, muss entweder ein Text oder eine Datei angegeben werden.'
|
||||
)
|
||||
|
||||
# Validate that at least one form of confirmation is provided for asset situation
|
||||
vermogenssituation_text = cleaned_data.get('vermogenssituation_text')
|
||||
vermogenssituation_datei = cleaned_data.get('vermogenssituation_datei')
|
||||
vermogenssituation_bestaetigt = cleaned_data.get('vermogenssituation_bestaetigt')
|
||||
|
||||
if vermogenssituation_bestaetigt and not vermogenssituation_text and not vermogenssituation_datei:
|
||||
raise ValidationError(
|
||||
'Wenn die Vermögenssituation bestätigt wird, muss entweder ein Text oder eine Datei angegeben werden.'
|
||||
)
|
||||
|
||||
# Validate study proof if required and marked as submitted
|
||||
studiennachweis_erforderlich = cleaned_data.get('studiennachweis_erforderlich')
|
||||
studiennachweis_eingereicht = cleaned_data.get('studiennachweis_eingereicht')
|
||||
studiennachweis_datei = cleaned_data.get('studiennachweis_datei')
|
||||
studiennachweis_bemerkung = cleaned_data.get('studiennachweis_bemerkung')
|
||||
|
||||
if studiennachweis_erforderlich and studiennachweis_eingereicht:
|
||||
if not studiennachweis_datei and not studiennachweis_bemerkung:
|
||||
raise ValidationError(
|
||||
'Wenn der Studiennachweis als eingereicht markiert wird, muss entweder eine Datei oder eine Bemerkung angegeben werden.'
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
19
app/stiftung/forms/dokumente.py
Normal file
19
app/stiftung/forms/dokumente.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django import forms
|
||||
|
||||
from ..models import DokumentLink
|
||||
|
||||
|
||||
class DokumentLinkForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Dokumentverknüpfungen"""
|
||||
|
||||
class Meta:
|
||||
model = DokumentLink
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"paperless_id": forms.NumberInput(attrs={"class": "form-control"}),
|
||||
"content_type": forms.Select(attrs={"class": "form-select"}),
|
||||
"object_id": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"verknuepft_am": forms.DateTimeInput(
|
||||
attrs={"class": "form-control", "type": "datetime-local"}
|
||||
),
|
||||
}
|
||||
351
app/stiftung/forms/finanzen.py
Normal file
351
app/stiftung/forms/finanzen.py
Normal file
@@ -0,0 +1,351 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from ..models import BankTransaction, Rentmeister, StiftungsKonto, Verwaltungskosten
|
||||
|
||||
|
||||
class RentmeisterForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Rentmeistern"""
|
||||
|
||||
class Meta:
|
||||
model = Rentmeister
|
||||
fields = [
|
||||
"anrede",
|
||||
"vorname",
|
||||
"nachname",
|
||||
"titel",
|
||||
"email",
|
||||
"telefon",
|
||||
"mobil",
|
||||
"strasse",
|
||||
"plz",
|
||||
"ort",
|
||||
"iban",
|
||||
"bic",
|
||||
"bank_name",
|
||||
"seit_datum",
|
||||
"bis_datum",
|
||||
"aktiv",
|
||||
"monatliche_verguetung",
|
||||
"km_pauschale",
|
||||
"notizen",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"anrede": forms.Select(attrs={"class": "form-select"}),
|
||||
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"titel": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
||||
"telefon": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"mobil": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"strasse": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"plz": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"iban": forms.TextInput(
|
||||
attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
|
||||
),
|
||||
"bic": forms.TextInput(
|
||||
attrs={"class": "form-control", "placeholder": "COBADEFFXXX"}
|
||||
),
|
||||
"bank_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"seit_datum": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"bis_datum": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"monatliche_verguetung": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"km_pauschale": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01", "value": "0.30"}
|
||||
),
|
||||
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
|
||||
}
|
||||
|
||||
labels = {
|
||||
"anrede": "Anrede",
|
||||
"vorname": "Vorname *",
|
||||
"nachname": "Nachname *",
|
||||
"titel": "Titel",
|
||||
"email": "E-Mail",
|
||||
"telefon": "Telefon",
|
||||
"mobil": "Mobil",
|
||||
"strasse": "Straße",
|
||||
"plz": "PLZ",
|
||||
"ort": "Ort",
|
||||
"iban": "IBAN",
|
||||
"bic": "BIC",
|
||||
"bank_name": "Bank",
|
||||
"seit_datum": "Rentmeister seit *",
|
||||
"bis_datum": "Rentmeister bis",
|
||||
"aktiv": "Aktiv",
|
||||
"monatliche_verguetung": "Monatliche Vergütung (€)",
|
||||
"km_pauschale": "Kilometerpauschale (€/km)",
|
||||
"notizen": "Notizen",
|
||||
}
|
||||
|
||||
help_texts = {
|
||||
"iban": "Internationale Bankkontonummer für Abrechnungen",
|
||||
"km_pauschale": "Standard: 0,30 € pro Kilometer",
|
||||
"seit_datum": "Datum des Amtsantritts als Rentmeister",
|
||||
"bis_datum": "Leer lassen für aktive Rentmeister",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Markiere Pflichtfelder
|
||||
self.fields["vorname"].required = True
|
||||
self.fields["nachname"].required = True
|
||||
self.fields["seit_datum"].required = True
|
||||
|
||||
def clean_iban(self):
|
||||
"""Validierung der IBAN"""
|
||||
iban = self.cleaned_data.get("iban")
|
||||
if iban:
|
||||
# Entferne Leerzeichen und konvertiere zu Großbuchstaben
|
||||
iban = re.sub(r"\s+", "", iban.upper())
|
||||
|
||||
# Einfache IBAN-Längenvalidierung für deutsche IBANs
|
||||
if iban.startswith("DE") and len(iban) != 22:
|
||||
raise ValidationError("Deutsche IBANs müssen 22 Zeichen lang sein.")
|
||||
|
||||
# Speichere die bereinigte IBAN
|
||||
return iban
|
||||
return iban
|
||||
|
||||
def clean_plz(self):
|
||||
"""Validierung der PLZ"""
|
||||
plz = self.cleaned_data.get("plz")
|
||||
if plz and not re.match(r"^\d{5}$", plz):
|
||||
raise ValidationError("PLZ muss aus 5 Ziffern bestehen.")
|
||||
return plz
|
||||
|
||||
def clean(self):
|
||||
"""Übergreifende Validierung"""
|
||||
from django.utils.dateparse import parse_date
|
||||
|
||||
cleaned_data = super().clean()
|
||||
seit_datum = cleaned_data.get("seit_datum")
|
||||
bis_datum = cleaned_data.get("bis_datum")
|
||||
|
||||
# Helper function to ensure we have date objects
|
||||
def ensure_date(date_value):
|
||||
if not date_value:
|
||||
return None
|
||||
if isinstance(date_value, str):
|
||||
return parse_date(date_value)
|
||||
return date_value
|
||||
|
||||
# Convert to date objects if they're strings
|
||||
seit_datum = ensure_date(seit_datum)
|
||||
bis_datum = ensure_date(bis_datum)
|
||||
|
||||
# Prüfe Datum-Logik
|
||||
if seit_datum and bis_datum and bis_datum <= seit_datum:
|
||||
raise ValidationError("Das End-Datum muss nach dem Start-Datum liegen.")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class StiftungsKontoForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Stiftungskonten"""
|
||||
|
||||
class Meta:
|
||||
model = StiftungsKonto
|
||||
fields = [
|
||||
"kontoname",
|
||||
"bank_name",
|
||||
"iban",
|
||||
"bic",
|
||||
"konto_typ",
|
||||
"saldo",
|
||||
"saldo_datum",
|
||||
"zinssatz",
|
||||
"laufzeit_bis",
|
||||
"aktiv",
|
||||
"notizen",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"kontoname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"bank_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"iban": forms.TextInput(
|
||||
attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
|
||||
),
|
||||
"bic": forms.TextInput(
|
||||
attrs={"class": "form-control", "placeholder": "COBADEFFXXX"}
|
||||
),
|
||||
"konto_typ": forms.Select(attrs={"class": "form-select"}),
|
||||
"saldo": forms.NumberInput(attrs={"class": "form-control", "step": "0.01"}),
|
||||
"saldo_datum": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"zinssatz": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"laufzeit_bis": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
}
|
||||
|
||||
|
||||
class VerwaltungskostenForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Verwaltungskosten"""
|
||||
|
||||
class Meta:
|
||||
model = Verwaltungskosten
|
||||
fields = [
|
||||
"bezeichnung",
|
||||
"kategorie",
|
||||
"betrag",
|
||||
"datum",
|
||||
"status",
|
||||
"rentmeister",
|
||||
"zahlungskonto",
|
||||
"quellkonto",
|
||||
"lieferant_firma",
|
||||
"rechnungsnummer",
|
||||
"km_anzahl",
|
||||
"km_satz",
|
||||
"von_ort",
|
||||
"nach_ort",
|
||||
"zweck",
|
||||
"beschreibung",
|
||||
"notizen",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"bezeichnung": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"kategorie": forms.Select(attrs={"class": "form-select"}),
|
||||
"betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"datum": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"rentmeister": forms.Select(attrs={"class": "form-select"}),
|
||||
"zahlungskonto": forms.Select(attrs={"class": "form-select"}),
|
||||
"quellkonto": forms.Select(attrs={"class": "form-select"}),
|
||||
"lieferant_firma": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"rechnungsnummer": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"km_anzahl": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.1"}
|
||||
),
|
||||
"km_satz": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"von_ort": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"nach_ort": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"zweck": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"beschreibung": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Filtere nur aktive Rentmeister und Konten
|
||||
self.fields["rentmeister"].queryset = Rentmeister.objects.filter(aktiv=True)
|
||||
self.fields["zahlungskonto"].queryset = StiftungsKonto.objects.filter(
|
||||
aktiv=True
|
||||
)
|
||||
self.fields["quellkonto"].queryset = StiftungsKonto.objects.filter(aktiv=True)
|
||||
|
||||
# Standardwerte setzen
|
||||
if not self.instance.pk: # Nur bei neuen Objekten
|
||||
# Standard km_satz auf 0.30 Euro setzen
|
||||
self.fields["km_satz"].initial = 0.30
|
||||
|
||||
|
||||
class BankTransactionForm(forms.ModelForm):
|
||||
"""Form für das Bearbeiten von Banktransaktionen"""
|
||||
|
||||
class Meta:
|
||||
model = BankTransaction
|
||||
fields = [
|
||||
"konto",
|
||||
"datum",
|
||||
"valuta",
|
||||
"betrag",
|
||||
"waehrung",
|
||||
"verwendungszweck",
|
||||
"empfaenger_zahlungspflichtiger",
|
||||
"iban_gegenpartei",
|
||||
"bic_gegenpartei",
|
||||
"transaction_type",
|
||||
"status",
|
||||
"kommentare",
|
||||
"verwaltungskosten",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"konto": forms.Select(attrs={"class": "form-select"}),
|
||||
"datum": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
||||
"valuta": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
||||
"betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"waehrung": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"verwendungszweck": forms.Textarea(
|
||||
attrs={"class": "form-control", "rows": 3}
|
||||
),
|
||||
"empfaenger_zahlungspflichtiger": forms.TextInput(
|
||||
attrs={"class": "form-control"}
|
||||
),
|
||||
"iban_gegenpartei": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"bic_gegenpartei": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"transaction_type": forms.Select(attrs={"class": "form-select"}),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"kommentare": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
|
||||
"verwaltungskosten": forms.Select(attrs={"class": "form-select"}),
|
||||
}
|
||||
|
||||
|
||||
class BankImportForm(forms.Form):
|
||||
"""Form für den Import von Bankdaten"""
|
||||
|
||||
konto = forms.ModelChoiceField(
|
||||
queryset=StiftungsKonto.objects.filter(aktiv=True),
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
label="Zielkonto",
|
||||
)
|
||||
|
||||
datei = forms.FileField(
|
||||
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv,.txt"}),
|
||||
label="Bankdatei",
|
||||
help_text="Unterstützte Formate: CSV, TXT (Sparkasse, Volksbank, etc.)",
|
||||
)
|
||||
|
||||
encoding = forms.ChoiceField(
|
||||
choices=[
|
||||
("utf-8", "UTF-8"),
|
||||
("latin1", "Latin-1 / ISO-8859-1"),
|
||||
("cp1252", "Windows-1252"),
|
||||
],
|
||||
initial="utf-8",
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
label="Zeichenkodierung",
|
||||
)
|
||||
|
||||
delimiter = forms.ChoiceField(
|
||||
choices=[
|
||||
(";", "Semikolon (;)"),
|
||||
(",", "Komma (,)"),
|
||||
("\t", "Tab"),
|
||||
],
|
||||
initial=";",
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
label="Trennzeichen",
|
||||
)
|
||||
|
||||
skip_header = forms.BooleanField(
|
||||
initial=True,
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
label="Erste Zeile überspringen (Spaltenüberschriften)",
|
||||
)
|
||||
73
app/stiftung/forms/foerderung.py
Normal file
73
app/stiftung/forms/foerderung.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from django import forms
|
||||
|
||||
from ..models import Destinataer, DokumentLink, Foerderung
|
||||
|
||||
|
||||
class FoerderungForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Förderungen"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Add empty option for optional fields
|
||||
self.fields["verwendungsnachweis"].empty_label = (
|
||||
"--- Kein Dokument verknüpfen ---"
|
||||
)
|
||||
# Ensure destinataer has proper choices
|
||||
from django.utils import timezone
|
||||
|
||||
from ..models import Destinataer, DokumentLink
|
||||
|
||||
self.fields["destinataer"].queryset = Destinataer.objects.all().order_by(
|
||||
"nachname", "vorname"
|
||||
)
|
||||
self.fields["verwendungsnachweis"].queryset = (
|
||||
DokumentLink.objects.all().order_by("titel")
|
||||
)
|
||||
# Set current year as default for new forms
|
||||
if not self.instance.pk:
|
||||
self.fields["jahr"].initial = timezone.now().year
|
||||
|
||||
class Meta:
|
||||
model = Foerderung
|
||||
fields = [
|
||||
"destinataer",
|
||||
"jahr",
|
||||
"betrag",
|
||||
"kategorie",
|
||||
"status",
|
||||
"antragsdatum",
|
||||
"entscheidungsdatum",
|
||||
"verwendungsnachweis",
|
||||
"bemerkungen",
|
||||
]
|
||||
widgets = {
|
||||
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
||||
"jahr": forms.NumberInput(attrs={"class": "form-control"}),
|
||||
"betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"kategorie": forms.Select(attrs={"class": "form-select"}),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"antragsdatum": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"entscheidungsdatum": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"verwendungsnachweis": forms.Select(attrs={"class": "form-select"}),
|
||||
"bemerkungen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
}
|
||||
|
||||
labels = {
|
||||
"destinataer": "Destinatär",
|
||||
"verwendungsnachweis": "Verknüpftes Dokument",
|
||||
"bemerkungen": "Bemerkungen/Beschreibung",
|
||||
"antragsdatum": "Antragsdatum",
|
||||
"entscheidungsdatum": "Entscheidungsdatum",
|
||||
}
|
||||
|
||||
help_texts = {
|
||||
"verwendungsnachweis": "Optionale Verknüpfung zu einem Dokument aus dem Paperless-System",
|
||||
"entscheidungsdatum": "Datum der Bewilligung/Ablehnung (optional)",
|
||||
"bemerkungen": "Zusätzliche Informationen zur Förderung",
|
||||
}
|
||||
107
app/stiftung/forms/geschichte.py
Normal file
107
app/stiftung/forms/geschichte.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from django import forms
|
||||
|
||||
from ..models import GeschichteBild, GeschichteSeite
|
||||
|
||||
|
||||
class GeschichteSeiteForm(forms.ModelForm):
|
||||
"""Form for creating and editing history pages"""
|
||||
|
||||
class Meta:
|
||||
from ..models import GeschichteSeite
|
||||
model = GeschichteSeite
|
||||
fields = ['titel', 'slug', 'inhalt', 'ist_veroeffentlicht', 'sortierung']
|
||||
widgets = {
|
||||
'titel': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'z.B. Gründung der Stiftung'
|
||||
}),
|
||||
'slug': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'z.B. gruendung-der-stiftung'
|
||||
}),
|
||||
'inhalt': forms.Textarea(attrs={
|
||||
'class': 'form-control rich-text-editor',
|
||||
'rows': 20,
|
||||
'placeholder': 'Schreiben Sie hier den Inhalt der Geschichtsseite...'
|
||||
}),
|
||||
'ist_veroeffentlicht': forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input'
|
||||
}),
|
||||
'sortierung': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': 0
|
||||
})
|
||||
}
|
||||
help_texts = {
|
||||
'slug': 'URL-freundliche Version des Titels (nur Buchstaben, Zahlen und Bindestriche)',
|
||||
'inhalt': 'Unterstützt Rich-Text-Formatierung, Bilder und Videos',
|
||||
'sortierung': 'Niedrigere Zahlen erscheinen zuerst in der Navigation'
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Auto-generate slug from title if not provided
|
||||
if not self.instance.pk:
|
||||
self.fields['slug'].required = False
|
||||
|
||||
def clean_slug(self):
|
||||
slug = self.cleaned_data.get('slug')
|
||||
titel = self.cleaned_data.get('titel', '')
|
||||
|
||||
if not slug and titel:
|
||||
# Auto-generate slug from title
|
||||
from django.utils.text import slugify
|
||||
slug = slugify(titel)
|
||||
|
||||
if not slug:
|
||||
raise forms.ValidationError('Slug ist erforderlich. Bitte geben Sie einen Titel ein.')
|
||||
|
||||
return slug
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
titel = cleaned_data.get('titel', '')
|
||||
slug = cleaned_data.get('slug', '')
|
||||
|
||||
# Auto-generate slug if empty
|
||||
if titel and not slug:
|
||||
from django.utils.text import slugify
|
||||
cleaned_data['slug'] = slugify(titel)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class GeschichteBildForm(forms.ModelForm):
|
||||
"""Form for uploading images to history pages"""
|
||||
|
||||
class Meta:
|
||||
from ..models import GeschichteBild
|
||||
model = GeschichteBild
|
||||
fields = ['titel', 'bild', 'beschreibung', 'alt_text', 'sortierung']
|
||||
widgets = {
|
||||
'titel': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'z.B. Gründungsurkunde 1895'
|
||||
}),
|
||||
'bild': forms.ClearableFileInput(attrs={
|
||||
'class': 'form-control'
|
||||
}),
|
||||
'beschreibung': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 3,
|
||||
'placeholder': 'Beschreibung des Bildes...'
|
||||
}),
|
||||
'alt_text': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Alternativtext für Bildschirmleser'
|
||||
}),
|
||||
'sortierung': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': 0
|
||||
})
|
||||
}
|
||||
help_texts = {
|
||||
'bild': 'Unterstützte Formate: JPG, PNG, GIF (max. 10MB)',
|
||||
'alt_text': 'Wichtig für Barrierefreiheit',
|
||||
'sortierung': 'Reihenfolge in der Bildergalerie'
|
||||
}
|
||||
293
app/stiftung/forms/land.py
Normal file
293
app/stiftung/forms/land.py
Normal file
@@ -0,0 +1,293 @@
|
||||
from django import forms
|
||||
|
||||
from ..models import Land, LandAbrechnung, LandVerpachtung, Paechter
|
||||
|
||||
|
||||
class LandForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Ländern"""
|
||||
|
||||
class Meta:
|
||||
model = Land
|
||||
fields = [
|
||||
# Grundlegende Identifikation
|
||||
"lfd_nr",
|
||||
"ew_nummer",
|
||||
"grundbuchblatt",
|
||||
# Gerichtliche Zuständigkeit
|
||||
"amtsgericht",
|
||||
# Verwaltungsstruktur
|
||||
"gemeinde",
|
||||
"gemarkung",
|
||||
"flur",
|
||||
"flurstueck",
|
||||
"adresse",
|
||||
# Flächenangaben
|
||||
"groesse_qm",
|
||||
"gruenland_qm",
|
||||
"acker_qm",
|
||||
"wald_qm",
|
||||
"sonstiges_qm",
|
||||
# Legacy Verpachtung (für Kompatibilität)
|
||||
"verpachtete_gesamtflaeche",
|
||||
"flaeche_alte_liste",
|
||||
"verp_flaeche_aktuell",
|
||||
# Aktuelle Verpachtung
|
||||
"aktueller_paechter",
|
||||
"paechter_name",
|
||||
"paechter_anschrift",
|
||||
"pachtbeginn",
|
||||
"pachtende",
|
||||
"verlaengerung_klausel",
|
||||
"zahlungsweise",
|
||||
"pachtzins_pro_ha",
|
||||
"pachtzins_pauschal",
|
||||
# Umsatzsteuer
|
||||
"ust_option",
|
||||
"ust_satz",
|
||||
# Umlagen
|
||||
"grundsteuer_umlage",
|
||||
"versicherungen_umlage",
|
||||
"verbandsbeitraege_umlage",
|
||||
"jagdpacht_anteil_umlage",
|
||||
# Legacy Steuern
|
||||
"anteil_grundsteuer",
|
||||
"anteil_lwk",
|
||||
# Status
|
||||
"aktiv",
|
||||
"notizen",
|
||||
]
|
||||
widgets = {
|
||||
# Grundlegende Identifikation
|
||||
"lfd_nr": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"ew_nummer": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"grundbuchblatt": forms.TextInput(attrs={"class": "form-control"}),
|
||||
# Gerichtliche Zuständigkeit
|
||||
"amtsgericht": forms.TextInput(attrs={"class": "form-control"}),
|
||||
# Verwaltungsstruktur
|
||||
"gemeinde": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"gemarkung": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"flur": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"flurstueck": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"adresse": forms.TextInput(attrs={"class": "form-control"}),
|
||||
# Flächenangaben
|
||||
"groesse_qm": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"gruenland_qm": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"acker_qm": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"wald_qm": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"sonstiges_qm": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
# Legacy Verpachtung
|
||||
"verpachtete_gesamtflaeche": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"flaeche_alte_liste": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"verp_flaeche_aktuell": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
# Aktuelle Verpachtung
|
||||
"aktueller_paechter": forms.Select(attrs={"class": "form-select"}),
|
||||
"paechter_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"paechter_anschrift": forms.Textarea(
|
||||
attrs={"class": "form-control", "rows": 3}
|
||||
),
|
||||
"pachtbeginn": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"pachtende": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"verlaengerung_klausel": forms.CheckboxInput(
|
||||
attrs={"class": "form-check-input"}
|
||||
),
|
||||
"zahlungsweise": forms.Select(attrs={"class": "form-select"}),
|
||||
"pachtzins_pro_ha": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"pachtzins_pauschal": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
# Umsatzsteuer
|
||||
"ust_option": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"ust_satz": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
# Umlagen
|
||||
"grundsteuer_umlage": forms.CheckboxInput(
|
||||
attrs={"class": "form-check-input"}
|
||||
),
|
||||
"versicherungen_umlage": forms.CheckboxInput(
|
||||
attrs={"class": "form-check-input"}
|
||||
),
|
||||
"verbandsbeitraege_umlage": forms.CheckboxInput(
|
||||
attrs={"class": "form-check-input"}
|
||||
),
|
||||
"jagdpacht_anteil_umlage": forms.CheckboxInput(
|
||||
attrs={"class": "form-check-input"}
|
||||
),
|
||||
# Legacy
|
||||
"anteil_grundsteuer": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"anteil_lwk": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
# Status
|
||||
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
}
|
||||
|
||||
|
||||
class LandVerpachtungForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Verpachtungen"""
|
||||
|
||||
class Meta:
|
||||
model = LandVerpachtung
|
||||
fields = [
|
||||
'land',
|
||||
'paechter',
|
||||
'vertragsnummer',
|
||||
'pachtbeginn',
|
||||
'pachtende',
|
||||
'verlaengerung_klausel',
|
||||
'verpachtete_flaeche',
|
||||
'pachtzins_pauschal',
|
||||
'pachtzins_pro_ha',
|
||||
'zahlungsweise',
|
||||
'ust_option',
|
||||
'ust_satz',
|
||||
'grundsteuer_umlage',
|
||||
'versicherungen_umlage',
|
||||
'verbandsbeitraege_umlage',
|
||||
'jagdpacht_anteil_umlage',
|
||||
'status',
|
||||
'bemerkungen'
|
||||
]
|
||||
widgets = {
|
||||
'land': forms.Select(attrs={'class': 'form-select'}),
|
||||
'paechter': forms.Select(attrs={'class': 'form-select'}),
|
||||
'vertragsnummer': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'pachtbeginn': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'pachtende': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'verlaengerung_klausel': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'verpachtete_flaeche': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'pachtzins_pauschal': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'pachtzins_pro_ha': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'zahlungsweise': forms.Select(attrs={'class': 'form-select'}),
|
||||
'ust_option': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'ust_satz': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'grundsteuer_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'versicherungen_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'verbandsbeitraege_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'jagdpacht_anteil_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'bemerkungen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
}
|
||||
|
||||
|
||||
class LandAbrechnungForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Landabrechnungen"""
|
||||
|
||||
class Meta:
|
||||
model = LandAbrechnung
|
||||
fields = [
|
||||
"land",
|
||||
"abrechnungsjahr",
|
||||
# Einnahmen
|
||||
"pacht_vereinnahmt",
|
||||
"umlagen_vereinnahmt",
|
||||
"sonstige_einnahmen",
|
||||
# Ausgaben
|
||||
"grundsteuer_bescheid_nr",
|
||||
"grundsteuer_betrag",
|
||||
"versicherungen_betrag",
|
||||
"verbandsbeitraege_betrag",
|
||||
"sonstige_abgaben_betrag",
|
||||
"instandhaltung_betrag",
|
||||
"verwaltung_recht_betrag",
|
||||
# Umsatzsteuer
|
||||
"vorsteuer_aus_umlagen",
|
||||
# Sonstiges
|
||||
"offene_posten",
|
||||
"bemerkungen",
|
||||
# Dokumente werden über Paperless verknüpft, nicht hochgeladen
|
||||
]
|
||||
widgets = {
|
||||
"land": forms.Select(attrs={"class": "form-select"}),
|
||||
"abrechnungsjahr": forms.NumberInput(
|
||||
attrs={"class": "form-control", "min": "2000", "max": "2050"}
|
||||
),
|
||||
# Einnahmen
|
||||
"pacht_vereinnahmt": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"umlagen_vereinnahmt": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"sonstige_einnahmen": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
# Ausgaben
|
||||
"grundsteuer_bescheid_nr": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"grundsteuer_betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"versicherungen_betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"verbandsbeitraege_betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"sonstige_abgaben_betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"instandhaltung_betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"verwaltung_recht_betrag": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
# Umsatzsteuer
|
||||
"vorsteuer_aus_umlagen": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
# Sonstiges
|
||||
"offene_posten": forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.01"}
|
||||
),
|
||||
"bemerkungen": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
|
||||
}
|
||||
|
||||
|
||||
class PaechterForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Pächtern"""
|
||||
|
||||
class Meta:
|
||||
model = Paechter
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"anrede": forms.Select(attrs={"class": "form-select"}),
|
||||
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
||||
"telefon": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"mobil": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"geburtsdatum": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"strasse": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"plz": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
}
|
||||
460
app/stiftung/forms/system.py
Normal file
460
app/stiftung/forms/system.py
Normal file
@@ -0,0 +1,460 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from ..models import Person
|
||||
|
||||
|
||||
class UserCreationForm(forms.Form):
|
||||
"""Form für die Erstellung neuer Benutzer"""
|
||||
|
||||
username = forms.CharField(
|
||||
label="Benutzername",
|
||||
max_length=150,
|
||||
help_text="Eindeutiger Benutzername für die Anmeldung",
|
||||
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
||||
email = forms.EmailField(
|
||||
label="E-Mail-Adresse",
|
||||
help_text="E-Mail-Adresse des Benutzers",
|
||||
widget=forms.EmailInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
||||
first_name = forms.CharField(
|
||||
label="Vorname",
|
||||
max_length=30,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
||||
last_name = forms.CharField(
|
||||
label="Nachname",
|
||||
max_length=150,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
||||
password1 = forms.CharField(
|
||||
label="Passwort",
|
||||
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
||||
help_text="Mindestens 8 Zeichen",
|
||||
)
|
||||
|
||||
password2 = forms.CharField(
|
||||
label="Passwort bestätigen",
|
||||
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
||||
help_text="Geben Sie das Passwort zur Bestätigung erneut ein",
|
||||
)
|
||||
|
||||
is_active = forms.BooleanField(
|
||||
label="Aktiv",
|
||||
required=False,
|
||||
initial=True,
|
||||
help_text="Benutzer kann sich anmelden",
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
)
|
||||
|
||||
is_staff = forms.BooleanField(
|
||||
label="Staff-Status",
|
||||
required=False,
|
||||
help_text="Benutzer kann auf Django Admin zugreifen",
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
)
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data["username"]
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
if User.objects.filter(username=username).exists():
|
||||
raise forms.ValidationError(
|
||||
"Ein Benutzer mit diesem Namen existiert bereits."
|
||||
)
|
||||
return username
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data["email"]
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
raise forms.ValidationError(
|
||||
"Ein Benutzer mit dieser E-Mail-Adresse existiert bereits."
|
||||
)
|
||||
return email
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
password1 = cleaned_data.get("password1")
|
||||
password2 = cleaned_data.get("password2")
|
||||
|
||||
if password1 and password2:
|
||||
if password1 != password2:
|
||||
raise forms.ValidationError("Die Passwörter stimmen nicht überein.")
|
||||
if len(password1) < 8:
|
||||
raise forms.ValidationError(
|
||||
"Das Passwort muss mindestens 8 Zeichen lang sein."
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class UserUpdateForm(forms.ModelForm):
|
||||
"""Form für die Bearbeitung bestehender Benutzer"""
|
||||
|
||||
class Meta:
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
model = User
|
||||
fields = [
|
||||
"username",
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"is_active",
|
||||
"is_staff",
|
||||
]
|
||||
widgets = {
|
||||
"username": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
||||
"first_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"last_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"is_staff": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
}
|
||||
labels = {
|
||||
"username": "Benutzername",
|
||||
"email": "E-Mail-Adresse",
|
||||
"first_name": "Vorname",
|
||||
"last_name": "Nachname",
|
||||
"is_active": "Aktiv",
|
||||
"is_staff": "Staff-Status",
|
||||
}
|
||||
help_texts = {
|
||||
"username": "Eindeutiger Benutzername für die Anmeldung",
|
||||
"email": "E-Mail-Adresse des Benutzers",
|
||||
"is_active": "Benutzer kann sich anmelden",
|
||||
"is_staff": "Benutzer kann auf Django Admin zugreifen",
|
||||
}
|
||||
|
||||
|
||||
class PasswordChangeForm(forms.Form):
|
||||
"""Form für Passwort-Änderungen"""
|
||||
|
||||
new_password1 = forms.CharField(
|
||||
label="Neues Passwort",
|
||||
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
||||
help_text="Mindestens 8 Zeichen",
|
||||
)
|
||||
|
||||
new_password2 = forms.CharField(
|
||||
label="Neues Passwort bestätigen",
|
||||
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
||||
help_text="Geben Sie das neue Passwort zur Bestätigung erneut ein",
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
password1 = cleaned_data.get("new_password1")
|
||||
password2 = cleaned_data.get("new_password2")
|
||||
|
||||
if password1 and password2:
|
||||
if password1 != password2:
|
||||
raise forms.ValidationError("Die Passwörter stimmen nicht überein.")
|
||||
if len(password1) < 8:
|
||||
raise forms.ValidationError(
|
||||
"Das Passwort muss mindestens 8 Zeichen lang sein."
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class UserPermissionForm(forms.Form):
|
||||
"""Form für die Zuweisung von Berechtigungen"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop("user", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
# Get all custom permissions for stiftung app
|
||||
app_permissions = Permission.objects.filter(
|
||||
content_type__app_label="stiftung"
|
||||
).order_by("name")
|
||||
|
||||
# Create checkbox fields for each permission
|
||||
for perm in app_permissions:
|
||||
field_name = f"perm_{perm.id}"
|
||||
self.fields[field_name] = forms.BooleanField(
|
||||
label=perm.name,
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
)
|
||||
|
||||
# Set initial values if user is provided
|
||||
if user:
|
||||
self.fields[field_name].initial = user.has_perm(
|
||||
f"stiftung.{perm.codename}"
|
||||
)
|
||||
|
||||
def get_permission_groups(self):
|
||||
"""Group permissions by functionality for template rendering"""
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
groups = {
|
||||
"entities": {
|
||||
"name": "Entitäten verwalten",
|
||||
"permissions": [],
|
||||
"icon": "fas fa-users",
|
||||
},
|
||||
"documents": {
|
||||
"name": "Dokumentenverwaltung",
|
||||
"permissions": [],
|
||||
"icon": "fas fa-folder-open",
|
||||
},
|
||||
"financial": {
|
||||
"name": "Finanzverwaltung",
|
||||
"permissions": [],
|
||||
"icon": "fas fa-euro-sign",
|
||||
},
|
||||
"administration": {
|
||||
"name": "Administration",
|
||||
"permissions": [],
|
||||
"icon": "fas fa-cogs",
|
||||
},
|
||||
"system": {"name": "System", "permissions": [], "icon": "fas fa-server"},
|
||||
}
|
||||
|
||||
# Get all permissions to properly categorize them
|
||||
for field_name, field in self.fields.items():
|
||||
if field_name.startswith("perm_"):
|
||||
# Extract permission ID from field name
|
||||
perm_id = field_name.replace("perm_", "")
|
||||
try:
|
||||
permission = Permission.objects.get(id=perm_id)
|
||||
label = permission.name.lower()
|
||||
codename = permission.codename.lower()
|
||||
|
||||
# Get bound field for proper template rendering
|
||||
bound_field = self[field_name]
|
||||
|
||||
# More precise categorization based on both name and codename
|
||||
if (
|
||||
any(
|
||||
word in codename
|
||||
for word in [
|
||||
"destinataer",
|
||||
"land",
|
||||
"paechter",
|
||||
"verpachtung",
|
||||
"foerderung",
|
||||
]
|
||||
)
|
||||
and "manage_" in codename
|
||||
or "view_" in codename
|
||||
):
|
||||
groups["entities"]["permissions"].append(
|
||||
(field_name, bound_field, permission)
|
||||
)
|
||||
elif (
|
||||
any(
|
||||
word in codename for word in ["documents", "link_documents"]
|
||||
)
|
||||
or "dokument" in label
|
||||
):
|
||||
groups["documents"]["permissions"].append(
|
||||
(field_name, bound_field, permission)
|
||||
)
|
||||
elif any(
|
||||
word in codename
|
||||
for word in [
|
||||
"verwaltungskosten",
|
||||
"konten",
|
||||
"rentmeister",
|
||||
"approve_payments",
|
||||
]
|
||||
) or any(
|
||||
word in label
|
||||
for word in [
|
||||
"verwaltungskosten",
|
||||
"konto",
|
||||
"rentmeister",
|
||||
"zahlung",
|
||||
]
|
||||
):
|
||||
groups["financial"]["permissions"].append(
|
||||
(field_name, bound_field, permission)
|
||||
)
|
||||
elif any(
|
||||
word in codename
|
||||
for word in [
|
||||
"administration",
|
||||
"audit",
|
||||
"backup",
|
||||
"manage_users",
|
||||
"manage_permissions",
|
||||
]
|
||||
) or any(
|
||||
word in label
|
||||
for word in [
|
||||
"administration",
|
||||
"audit",
|
||||
"backup",
|
||||
"benutzer",
|
||||
"berechtigung",
|
||||
]
|
||||
):
|
||||
groups["administration"]["permissions"].append(
|
||||
(field_name, bound_field, permission)
|
||||
)
|
||||
else:
|
||||
groups["system"]["permissions"].append(
|
||||
(field_name, bound_field, permission)
|
||||
)
|
||||
except Permission.DoesNotExist:
|
||||
# Create a fallback permission-like object with proper display
|
||||
class FallbackPermission:
|
||||
def __init__(self, field_name):
|
||||
self.name = field_name.replace('_', ' ').title()
|
||||
self.codename = field_name
|
||||
|
||||
fallback_perm = FallbackPermission(field_name)
|
||||
bound_field = self[field_name] # Get bound field for exception case too
|
||||
groups["system"]["permissions"].append((field_name, bound_field, fallback_perm))
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
class TwoFactorSetupForm(forms.Form):
|
||||
"""Form for setting up 2FA with TOTP verification"""
|
||||
token = forms.CharField(
|
||||
max_length=6,
|
||||
min_length=6,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control text-center',
|
||||
'placeholder': '000000',
|
||||
'autocomplete': 'off',
|
||||
'pattern': '[0-9]{6}',
|
||||
'inputmode': 'numeric'
|
||||
}),
|
||||
label='Bestätigungscode',
|
||||
help_text='6-stelliger Code aus Ihrer Authenticator-App'
|
||||
)
|
||||
|
||||
def clean_token(self):
|
||||
token = self.cleaned_data.get('token')
|
||||
if token and not token.isdigit():
|
||||
raise ValidationError('Der Code darf nur Zahlen enthalten.')
|
||||
return token
|
||||
|
||||
|
||||
class TwoFactorVerifyForm(forms.Form):
|
||||
"""Form for verifying 2FA during login"""
|
||||
otp_token = forms.CharField(
|
||||
max_length=8,
|
||||
min_length=6,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control form-control-lg text-center',
|
||||
'placeholder': '000000',
|
||||
'autocomplete': 'off',
|
||||
'autofocus': True
|
||||
}),
|
||||
label='Authentifizierungscode',
|
||||
help_text='6-stelliger Code aus der App oder 8-stelliger Backup-Code'
|
||||
)
|
||||
|
||||
def clean_otp_token(self):
|
||||
token = self.cleaned_data.get('otp_token')
|
||||
if token:
|
||||
token = token.strip().lower()
|
||||
# Allow 6-digit TOTP codes or 8-character backup codes
|
||||
if len(token) == 6 and token.isdigit():
|
||||
return token
|
||||
elif len(token) == 8 and re.match(r'^[a-f0-9]{8}$', token):
|
||||
return token
|
||||
else:
|
||||
raise ValidationError(
|
||||
'Bitte geben Sie einen 6-stelligen Authenticator-Code oder 8-stelligen Backup-Code ein.'
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
class TwoFactorDisableForm(forms.Form):
|
||||
"""Form for disabling 2FA with password confirmation"""
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={
|
||||
'class': 'form-control',
|
||||
'autocomplete': 'current-password',
|
||||
'autofocus': True
|
||||
}),
|
||||
label='Passwort',
|
||||
help_text='Geben Sie Ihr aktuelles Passwort zur Bestätigung ein'
|
||||
)
|
||||
|
||||
|
||||
class BackupTokenRegenerateForm(forms.Form):
|
||||
"""Form for regenerating backup tokens"""
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={
|
||||
'class': 'form-control',
|
||||
'autocomplete': 'current-password'
|
||||
}),
|
||||
label='Passwort',
|
||||
help_text='Geben Sie Ihr Passwort ein, um neue Backup-Codes zu generieren'
|
||||
)
|
||||
|
||||
|
||||
class PersonForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Personen (Legacy)"""
|
||||
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = [
|
||||
"familienzweig",
|
||||
"vorname",
|
||||
"nachname",
|
||||
"geburtsdatum",
|
||||
"email",
|
||||
"telefon",
|
||||
"iban",
|
||||
"adresse",
|
||||
"notizen",
|
||||
"aktiv",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"familienzweig": forms.Select(attrs={"class": "form-select"}),
|
||||
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"geburtsdatum": forms.DateInput(
|
||||
attrs={"class": "form-control", "type": "date"}
|
||||
),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
||||
"telefon": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"iban": forms.TextInput(
|
||||
attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
|
||||
),
|
||||
"adresse": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
}
|
||||
|
||||
labels = {
|
||||
"familienzweig": "Familienzweig",
|
||||
"vorname": "Vorname *",
|
||||
"nachname": "Nachname *",
|
||||
"geburtsdatum": "Geburtsdatum",
|
||||
"email": "E-Mail",
|
||||
"telefon": "Telefon",
|
||||
"iban": "IBAN",
|
||||
"adresse": "Adresse",
|
||||
"notizen": "Notizen",
|
||||
"aktiv": "Aktiv",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Markiere Pflichtfelder
|
||||
self.fields["vorname"].required = True
|
||||
self.fields["nachname"].required = True
|
||||
59
app/stiftung/forms/veranstaltung.py
Normal file
59
app/stiftung/forms/veranstaltung.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from django import forms
|
||||
|
||||
from ..models import Veranstaltung, Veranstaltungsteilnehmer
|
||||
|
||||
|
||||
class VeranstaltungForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Veranstaltungen inkl. Serienbrief-Felder"""
|
||||
|
||||
class Meta:
|
||||
model = Veranstaltung
|
||||
fields = [
|
||||
"titel", "datum", "uhrzeit", "ort", "adresse",
|
||||
"beschreibung", "status", "budget_pro_person",
|
||||
"betreff", "briefvorlage",
|
||||
"unterschrift_1_name", "unterschrift_1_titel",
|
||||
"unterschrift_2_name", "unterschrift_2_titel",
|
||||
]
|
||||
widgets = {
|
||||
"titel": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"datum": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
||||
"uhrzeit": forms.TimeInput(attrs={"class": "form-control", "type": "time"}),
|
||||
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"adresse": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
"beschreibung": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"budget_pro_person": forms.NumberInput(attrs={"class": "form-control", "step": "0.01"}),
|
||||
"betreff": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"briefvorlage": forms.Textarea(attrs={"class": "form-control", "rows": 12}),
|
||||
"unterschrift_1_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"unterschrift_1_titel": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"unterschrift_2_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"unterschrift_2_titel": forms.TextInput(attrs={"class": "form-control"}),
|
||||
}
|
||||
|
||||
|
||||
class VeranstaltungsteilnehmerForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Veranstaltungsteilnehmern"""
|
||||
|
||||
class Meta:
|
||||
model = Veranstaltungsteilnehmer
|
||||
fields = [
|
||||
"anrede", "vorname", "nachname",
|
||||
"strasse", "plz", "ort", "email",
|
||||
"rsvp_status", "bemerkungen",
|
||||
"paechter", "destinataer",
|
||||
]
|
||||
widgets = {
|
||||
"anrede": forms.Select(attrs={"class": "form-select"}),
|
||||
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"strasse": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"plz": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
||||
"rsvp_status": forms.Select(attrs={"class": "form-select"}),
|
||||
"bemerkungen": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
"paechter": forms.Select(attrs={"class": "form-select"}),
|
||||
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
199
app/stiftung/views/__init__.py
Normal file
199
app/stiftung/views/__init__.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# views/__init__.py
|
||||
# Phase 0: Vision 2026 – Re-exportiert alle View-Funktionen für Rückwärtskompatibilität
|
||||
|
||||
from .dashboard import ( # noqa: F401
|
||||
home,
|
||||
health_check,
|
||||
health,
|
||||
)
|
||||
|
||||
from .destinataere import ( # noqa: F401
|
||||
person_list,
|
||||
person_detail,
|
||||
person_create,
|
||||
person_update,
|
||||
person_delete,
|
||||
destinataer_list,
|
||||
destinataer_detail,
|
||||
destinataer_create,
|
||||
destinataer_update,
|
||||
destinataer_delete,
|
||||
destinataer_notiz_create,
|
||||
destinataer_export,
|
||||
)
|
||||
|
||||
from .dokumente import ( # noqa: F401
|
||||
dokument_management,
|
||||
paperless_document_redirect,
|
||||
dokument_list,
|
||||
dokument_detail,
|
||||
dokument_create,
|
||||
dokument_update,
|
||||
dokument_delete,
|
||||
paperless_ping,
|
||||
paperless_documents,
|
||||
paperless_debug,
|
||||
paperless_tags_only,
|
||||
link_document_search,
|
||||
create_paechter_link_for_verpachtung,
|
||||
link_document_create,
|
||||
link_document_list,
|
||||
link_document_update,
|
||||
link_document_delete,
|
||||
)
|
||||
|
||||
from .finanzen import ( # noqa: F401
|
||||
bericht_list,
|
||||
jahresbericht_generate,
|
||||
jahresbericht_generate_redirect,
|
||||
jahresbericht_pdf,
|
||||
geschaeftsfuehrung,
|
||||
konto_list,
|
||||
verwaltungskosten_list,
|
||||
rentmeister_list,
|
||||
rentmeister_detail,
|
||||
rentmeister_ausgaben,
|
||||
rentmeister_create,
|
||||
rentmeister_edit,
|
||||
konto_create,
|
||||
konto_edit,
|
||||
konto_detail,
|
||||
verwaltungskosten_create,
|
||||
verwaltungskosten_edit,
|
||||
verwaltungskosten_delete,
|
||||
mark_expense_paid,
|
||||
)
|
||||
|
||||
from .foerderung import ( # noqa: F401
|
||||
foerderung_list,
|
||||
foerderung_detail,
|
||||
foerderung_create,
|
||||
foerderung_update,
|
||||
foerderung_delete,
|
||||
)
|
||||
|
||||
from .geschichte import ( # noqa: F401
|
||||
geschichte_list,
|
||||
geschichte_detail,
|
||||
geschichte_create,
|
||||
geschichte_edit,
|
||||
geschichte_bild_upload,
|
||||
geschichte_bild_delete,
|
||||
kalender_view,
|
||||
kalender_create,
|
||||
kalender_detail,
|
||||
kalender_edit,
|
||||
kalender_delete,
|
||||
kalender_admin,
|
||||
kalender_api_events,
|
||||
email_eingang_list,
|
||||
email_eingang_detail,
|
||||
email_eingang_poll_trigger,
|
||||
)
|
||||
|
||||
from .land import ( # noqa: F401
|
||||
paechter_list,
|
||||
paechter_detail,
|
||||
paechter_create,
|
||||
paechter_update,
|
||||
paechter_delete,
|
||||
land_list,
|
||||
land_detail,
|
||||
land_create,
|
||||
land_update,
|
||||
land_delete,
|
||||
verpachtung_list,
|
||||
land_verpachtung_detail,
|
||||
land_verpachtung_update,
|
||||
land_verpachtung_end_direct,
|
||||
land_stats_api,
|
||||
paechter_export,
|
||||
land_export,
|
||||
verpachtung_export,
|
||||
land_abrechnung_list,
|
||||
land_abrechnung_detail,
|
||||
land_abrechnung_create,
|
||||
land_abrechnung_update,
|
||||
land_abrechnung_delete,
|
||||
land_verpachtung_create,
|
||||
land_verpachtung_end,
|
||||
land_verpachtung_edit,
|
||||
verpachtung_detail,
|
||||
verpachtung_create,
|
||||
verpachtung_update,
|
||||
verpachtung_delete,
|
||||
)
|
||||
|
||||
from .system import ( # noqa: F401
|
||||
get_pdf_generator,
|
||||
GrampsClient,
|
||||
get_gramps_client,
|
||||
gramps_debug_api,
|
||||
csv_import_list,
|
||||
csv_import_create,
|
||||
process_personen_csv,
|
||||
process_destinataere_csv,
|
||||
process_paechter_csv,
|
||||
process_laendereien_csv,
|
||||
gramps_search_api,
|
||||
administration,
|
||||
audit_log_list,
|
||||
backup_management,
|
||||
backup_download,
|
||||
backup_restore,
|
||||
backup_cancel,
|
||||
user_management,
|
||||
user_create,
|
||||
user_detail,
|
||||
user_edit,
|
||||
user_change_password,
|
||||
user_permissions,
|
||||
user_delete,
|
||||
user_login,
|
||||
user_logout,
|
||||
app_settings,
|
||||
edit_help_box,
|
||||
two_factor_setup,
|
||||
two_factor_qr,
|
||||
two_factor_verify,
|
||||
two_factor_disable,
|
||||
backup_tokens,
|
||||
)
|
||||
|
||||
from .unterstuetzungen import ( # noqa: F401
|
||||
unterstuetzungen_list,
|
||||
export_unterstuetzungen_csv,
|
||||
export_unterstuetzungen_pdf,
|
||||
export_foerderungen_csv,
|
||||
export_foerderungen_pdf,
|
||||
unterstuetzung_edit,
|
||||
unterstuetzung_delete,
|
||||
unterstuetzungen_all,
|
||||
unterstuetzung_create,
|
||||
get_destinataer_info,
|
||||
unterstuetzung_detail,
|
||||
unterstuetzung_mark_paid,
|
||||
wiederkehrende_unterstuetzungen,
|
||||
quarterly_confirmation_update,
|
||||
create_quarterly_support_payment,
|
||||
quarterly_confirmation_create,
|
||||
quarterly_confirmation_edit,
|
||||
quarterly_confirmation_approve,
|
||||
quarterly_confirmation_reset,
|
||||
)
|
||||
|
||||
from .veranstaltung import ( # noqa: F401
|
||||
veranstaltung_list,
|
||||
veranstaltung_detail,
|
||||
veranstaltung_serienbrief_pdf,
|
||||
veranstaltung_serienbrief_vorschau,
|
||||
veranstaltung_create,
|
||||
veranstaltung_update,
|
||||
veranstaltung_delete,
|
||||
teilnehmer_create,
|
||||
teilnehmer_update,
|
||||
teilnehmer_delete,
|
||||
)
|
||||
|
||||
# Non-view exports (helpers used elsewhere)
|
||||
from .system import GrampsClient, get_gramps_client, get_pdf_generator # noqa: F401
|
||||
117
app/stiftung/views/dashboard.py
Normal file
117
app/stiftung/views/dashboard.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# views/dashboard.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def home(request):
|
||||
"""Home page for the Stiftungsverwaltung application"""
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
|
||||
# Get upcoming events for the calendar widget
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get all events for the next 14 days
|
||||
from datetime import timedelta
|
||||
today = timezone.now().date()
|
||||
end_date = today + timedelta(days=14)
|
||||
all_events = calendar_service.get_all_events(today, end_date)
|
||||
|
||||
# Filter for upcoming and overdue
|
||||
upcoming_events = [e for e in all_events if not getattr(e, 'overdue', False)]
|
||||
overdue_events = [e for e in all_events if getattr(e, 'overdue', False)]
|
||||
|
||||
# Get current month events for mini calendar
|
||||
from calendar import monthrange
|
||||
_, last_day = monthrange(today.year, today.month)
|
||||
month_start = today.replace(day=1)
|
||||
month_end = today.replace(day=last_day)
|
||||
current_month_events = calendar_service.get_all_events(month_start, month_end)
|
||||
|
||||
context = {
|
||||
"title": "Stiftungsverwaltung",
|
||||
"description": "Foundation Management System",
|
||||
"upcoming_events": upcoming_events[:5], # Show only 5 upcoming events
|
||||
"overdue_events": overdue_events[:3], # Show only 3 overdue events
|
||||
"current_month_events": current_month_events,
|
||||
"today": today,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/home.html", context)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
def health_check(request):
|
||||
"""Simple health check endpoint for deployment monitoring"""
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": timezone.now().isoformat(),
|
||||
"service": "stiftung-web",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
## Removed duplicate paperless_ping referencing non-existent PAPERLESS_URL
|
||||
|
||||
|
||||
# CSV Import Views
|
||||
@api_view(["GET"])
|
||||
def health(_request):
|
||||
return Response({"status": "ok"})
|
||||
|
||||
|
||||
697
app/stiftung/views/destinataere.py
Normal file
697
app/stiftung/views/destinataere.py
Normal file
@@ -0,0 +1,697 @@
|
||||
# views/destinataere.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_list(request):
|
||||
search_query = request.GET.get("search", "")
|
||||
familienzweig_filter = request.GET.get("familienzweig", "")
|
||||
aktiv_filter = request.GET.get("aktiv", "")
|
||||
|
||||
persons = Person.objects.all()
|
||||
|
||||
if search_query:
|
||||
persons = persons.filter(
|
||||
Q(nachname__icontains=search_query)
|
||||
| Q(vorname__icontains=search_query)
|
||||
| Q(email__icontains=search_query)
|
||||
| Q(familienzweig__icontains=search_query)
|
||||
)
|
||||
|
||||
if familienzweig_filter:
|
||||
persons = persons.filter(familienzweig=familienzweig_filter)
|
||||
|
||||
if aktiv_filter == "true":
|
||||
persons = persons.filter(aktiv=True)
|
||||
elif aktiv_filter == "false":
|
||||
persons = persons.filter(aktiv=False)
|
||||
|
||||
# Annotate with total funding
|
||||
persons = persons.annotate(total_foerderungen=Sum("foerderung__betrag"))
|
||||
|
||||
paginator = Paginator(persons, 20)
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"search_query": search_query,
|
||||
"familienzweig_filter": familienzweig_filter,
|
||||
"aktiv_filter": aktiv_filter,
|
||||
"familienzweig_choices": Person.FAMILIENZWIG_CHOICES,
|
||||
}
|
||||
return render(request, "stiftung/person_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_detail(request, pk):
|
||||
person = get_object_or_404(Person, pk=pk)
|
||||
foerderungen = person.foerderung_set.all().order_by("-jahr", "-betrag")
|
||||
# Get new LandVerpachtungen for this person's Paechter instances
|
||||
verpachtungen = LandVerpachtung.objects.filter(paechter__person=person).order_by(
|
||||
"-pachtbeginn"
|
||||
)
|
||||
|
||||
context = {
|
||||
"person": person,
|
||||
"foerderungen": foerderungen,
|
||||
"verpachtungen": verpachtungen,
|
||||
}
|
||||
return render(request, "stiftung/person_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_create(request):
|
||||
if request.method == "POST":
|
||||
form = PersonForm(request.POST)
|
||||
if form.is_valid():
|
||||
person = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Person "{person.get_full_name()}" wurde erfolgreich erstellt.',
|
||||
)
|
||||
return redirect("stiftung:person_detail", pk=person.pk)
|
||||
else:
|
||||
form = PersonForm()
|
||||
|
||||
context = {"form": form, "title": "Neue Person erstellen"}
|
||||
return render(request, "stiftung/person_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_update(request, pk):
|
||||
person = get_object_or_404(Person, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = PersonForm(request.POST, instance=person)
|
||||
if form.is_valid():
|
||||
person = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Person "{person.get_full_name()}" wurde erfolgreich aktualisiert.',
|
||||
)
|
||||
return redirect("stiftung:person_detail", pk=person.pk)
|
||||
else:
|
||||
form = PersonForm(instance=person)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"person": person,
|
||||
"title": f"Person bearbeiten: {person.get_full_name()}",
|
||||
}
|
||||
return render(request, "stiftung/person_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_delete(request, pk):
|
||||
person = get_object_or_404(Person, pk=pk)
|
||||
if request.method == "POST":
|
||||
person.delete()
|
||||
messages.success(
|
||||
request, f'Person "{person.get_full_name()}" wurde erfolgreich gelöscht.'
|
||||
)
|
||||
return redirect("stiftung:person_list")
|
||||
|
||||
context = {"person": person}
|
||||
return render(request, "stiftung/person_confirm_delete.html", context)
|
||||
|
||||
|
||||
# Destinatär Views (Förderungsempfänger)
|
||||
@login_required
|
||||
def destinataer_list(request):
|
||||
search_query = request.GET.get("search", "")
|
||||
familienzweig_filter = request.GET.get("familienzweig", "")
|
||||
berufsgruppe_filter = request.GET.get("berufsgruppe", "")
|
||||
aktiv_filter = request.GET.get("aktiv", "")
|
||||
sort = request.GET.get("sort", "")
|
||||
direction = request.GET.get("dir", "asc")
|
||||
|
||||
destinataere = Destinataer.objects.all()
|
||||
|
||||
if search_query:
|
||||
destinataere = destinataere.filter(
|
||||
Q(nachname__icontains=search_query)
|
||||
| Q(vorname__icontains=search_query)
|
||||
| Q(email__icontains=search_query)
|
||||
| Q(institution__icontains=search_query)
|
||||
| Q(familienzweig__icontains=search_query)
|
||||
)
|
||||
|
||||
if familienzweig_filter:
|
||||
destinataere = destinataere.filter(familienzweig=familienzweig_filter)
|
||||
|
||||
if berufsgruppe_filter:
|
||||
destinataere = destinataere.filter(berufsgruppe=berufsgruppe_filter)
|
||||
|
||||
if aktiv_filter == "true":
|
||||
destinataere = destinataere.filter(aktiv=True)
|
||||
elif aktiv_filter == "false":
|
||||
destinataere = destinataere.filter(aktiv=False)
|
||||
|
||||
# Annotate with total funding (coalesce nulls to Decimal for stable sorting)
|
||||
destinataere = destinataere.annotate(
|
||||
total_foerderungen=Coalesce(
|
||||
Sum("foerderung__betrag"),
|
||||
Value(
|
||||
Decimal("0.00"),
|
||||
output_field=DecimalField(max_digits=12, decimal_places=2),
|
||||
),
|
||||
output_field=DecimalField(max_digits=12, decimal_places=2),
|
||||
)
|
||||
)
|
||||
|
||||
# Sorting
|
||||
sort_map = {
|
||||
"vorname": ["vorname"],
|
||||
"nachname": ["nachname"],
|
||||
"email": ["email"],
|
||||
"vierteljaehrlicher_betrag": ["vierteljaehrlicher_betrag"],
|
||||
"letzter_studiennachweis": ["letzter_studiennachweis"],
|
||||
"unterstuetzung_bestaetigt": ["unterstuetzung_bestaetigt"],
|
||||
# Keep old mappings for backward compatibility
|
||||
"name": ["nachname", "vorname"],
|
||||
"familienzweig": ["familienzweig"],
|
||||
"berufsgruppe": ["berufsgruppe"],
|
||||
"institution": ["institution"],
|
||||
"foerderungen": ["total_foerderungen"],
|
||||
"status": ["aktiv"],
|
||||
}
|
||||
if sort in sort_map:
|
||||
fields = sort_map[sort]
|
||||
if direction == "desc":
|
||||
order_fields = [f"-{f}" for f in fields]
|
||||
else:
|
||||
order_fields = fields
|
||||
destinataere = destinataere.order_by(*order_fields)
|
||||
else:
|
||||
# Default sorting by last name (nachname) ascending
|
||||
destinataere = destinataere.order_by("nachname", "vorname")
|
||||
|
||||
paginator = Paginator(destinataere, 50) # Increased from 20 to 50 entries per page
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Set default sort to nachname if no sort is specified
|
||||
effective_sort = sort if sort else "nachname"
|
||||
effective_direction = direction if sort else "asc"
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"search_query": search_query,
|
||||
"familienzweig_filter": familienzweig_filter,
|
||||
"berufsgruppe_filter": berufsgruppe_filter,
|
||||
"aktiv_filter": aktiv_filter,
|
||||
"familienzweig_choices": Destinataer.FAMILIENZWIG_CHOICES,
|
||||
"berufsgruppe_choices": Destinataer.BERUFSGRUPPE_CHOICES,
|
||||
"sort": effective_sort,
|
||||
"dir": effective_direction,
|
||||
}
|
||||
return render(request, "stiftung/destinataer_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_detail(request, pk):
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
|
||||
# Alle mit diesem Destinatär verknüpften Dokumente laden
|
||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
||||
destinataer_id=destinataer.pk
|
||||
).order_by("kontext", "titel")
|
||||
|
||||
# Förderungen für diesen Destinatär laden
|
||||
foerderungen = Foerderung.objects.filter(destinataer=destinataer).order_by(
|
||||
"-jahr", "-betrag"
|
||||
)
|
||||
|
||||
# Unterstützungen für diesen Destinatär laden
|
||||
unterstuetzungen = DestinataerUnterstuetzung.objects.filter(
|
||||
destinataer=destinataer
|
||||
).order_by("-faellig_am")
|
||||
|
||||
# Notizen laden
|
||||
notizen_eintraege = DestinataerNotiz.objects.filter(
|
||||
destinataer=destinataer
|
||||
).order_by("-erstellt_am")
|
||||
|
||||
# Quarterly confirmations - load for current and next year
|
||||
from datetime import date
|
||||
current_year = date.today().year
|
||||
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
||||
destinataer=destinataer,
|
||||
jahr__in=[current_year, current_year + 1]
|
||||
).order_by('-jahr', '-quartal')
|
||||
|
||||
# Create missing quarterly confirmations for current year
|
||||
# Quarterly tracking is now always available regardless of study proof requirements
|
||||
for quartal in range(1, 5): # Q1-Q4
|
||||
nachweis, created = VierteljahresNachweis.get_or_create_for_period(
|
||||
destinataer, current_year, quartal
|
||||
)
|
||||
|
||||
# Reload to get any newly created confirmations
|
||||
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
||||
destinataer=destinataer,
|
||||
jahr__in=[current_year, current_year + 1]
|
||||
).order_by('-jahr', '-quartal')
|
||||
|
||||
# Modal forms removed - only using full-screen editor now
|
||||
|
||||
# Generate available years for the add quarter dropdown (current year + next 5 years)
|
||||
available_years = list(range(current_year, current_year + 6))
|
||||
|
||||
# Alle verfügbaren StiftungsKonten für das Select-Feld laden
|
||||
stiftungskonten = StiftungsKonto.objects.all().order_by("kontoname")
|
||||
|
||||
context = {
|
||||
"destinataer": destinataer,
|
||||
"verknuepfte_dokumente": verknuepfte_dokumente,
|
||||
"foerderungen": foerderungen,
|
||||
"unterstuetzungen": unterstuetzungen,
|
||||
"notizen_eintraege": notizen_eintraege,
|
||||
"stiftungskonten": stiftungskonten,
|
||||
"quarterly_confirmations": quarterly_confirmations,
|
||||
"available_years": available_years,
|
||||
"current_year": current_year,
|
||||
}
|
||||
return render(request, "stiftung/destinataer_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_create(request):
|
||||
if request.method == "POST":
|
||||
form = DestinataerForm(request.POST)
|
||||
if form.is_valid():
|
||||
destinataer = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich erstellt.',
|
||||
)
|
||||
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
||||
else:
|
||||
form = DestinataerForm()
|
||||
|
||||
context = {"form": form, "title": "Neuen Destinatär erstellen"}
|
||||
return render(request, "stiftung/destinataer_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_update(request, pk):
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = DestinataerForm(request.POST, instance=destinataer)
|
||||
|
||||
# Handle AJAX requests
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
if form.is_valid():
|
||||
try:
|
||||
destinataer = form.save()
|
||||
|
||||
# Note: Support payments are now only created through quarterly confirmations
|
||||
# No automatic creation when unterstuetzung_bestaetigt is checked
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich aktualisiert.'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Speichern: {str(e)}'
|
||||
})
|
||||
else:
|
||||
# Return form errors for AJAX requests
|
||||
errors = []
|
||||
for field, field_errors in form.errors.items():
|
||||
for error in field_errors:
|
||||
errors.append(f'{form[field].label}: {error}')
|
||||
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Formular enthält Fehler: ' + '; '.join(errors)
|
||||
})
|
||||
|
||||
# Handle regular form submission
|
||||
if form.is_valid():
|
||||
destinataer = form.save()
|
||||
# Note: Support payments are now only created through quarterly confirmations
|
||||
# No automatic creation when unterstuetzung_bestaetigt is checked
|
||||
messages.success(
|
||||
request,
|
||||
f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich aktualisiert.',
|
||||
)
|
||||
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
||||
else:
|
||||
form = DestinataerForm(instance=destinataer)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"destinataer": destinataer,
|
||||
"title": f"Destinatär bearbeiten: {destinataer.get_full_name()}",
|
||||
}
|
||||
return render(request, "stiftung/destinataer_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_delete(request, pk):
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
if request.method == "POST":
|
||||
destinataer.delete()
|
||||
messages.success(
|
||||
request,
|
||||
f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich gelöscht.',
|
||||
)
|
||||
return redirect("stiftung:destinataer_list")
|
||||
|
||||
context = {"destinataer": destinataer}
|
||||
return render(request, "stiftung/destinataer_confirm_delete.html", context)
|
||||
|
||||
|
||||
# Paechter Views (Landpächter)
|
||||
@login_required
|
||||
def destinataer_notiz_create(request, pk):
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = DestinataerNotizForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
note = form.save(commit=False)
|
||||
note.destinataer = destinataer
|
||||
note.erstellt_von = request.user
|
||||
note.save()
|
||||
messages.success(request, "Notiz wurde gespeichert.")
|
||||
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
||||
else:
|
||||
# Debug: show what validation failed
|
||||
for field, errors in form.errors.items():
|
||||
messages.error(request, f'Fehler in {field}: {", ".join(errors)}')
|
||||
else:
|
||||
form = DestinataerNotizForm()
|
||||
return render(
|
||||
request,
|
||||
"stiftung/destinataer_notiz_form.html",
|
||||
{"form": form, "destinataer": destinataer, "title": "Notiz hinzufügen"},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_export(request, pk):
|
||||
"""Export complete Destinatär data as ZIP with documents"""
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
|
||||
# Create a temporary file for the ZIP
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(temp_file.name, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
# 1. Entity data as JSON
|
||||
entity_data = {
|
||||
"id": str(destinataer.id),
|
||||
"anrede": destinataer.get_anrede_display() if hasattr(destinataer, 'anrede') and destinataer.anrede else None,
|
||||
"titel": destinataer.titel if hasattr(destinataer, 'titel') else None,
|
||||
"vorname": destinataer.vorname,
|
||||
"nachname": destinataer.nachname,
|
||||
"geburtsdatum": (
|
||||
destinataer.geburtsdatum.isoformat()
|
||||
if destinataer.geburtsdatum
|
||||
else None
|
||||
),
|
||||
"email": destinataer.email,
|
||||
"telefon": destinataer.telefon,
|
||||
"mobil": destinataer.mobil if hasattr(destinataer, 'mobil') else None,
|
||||
"iban": destinataer.iban,
|
||||
"strasse": destinataer.strasse,
|
||||
"plz": destinataer.plz,
|
||||
"ort": destinataer.ort,
|
||||
"familienzweig": destinataer.get_familienzweig_display(),
|
||||
"berufsgruppe": destinataer.get_berufsgruppe_display(),
|
||||
"ausbildungsstand": destinataer.ausbildungsstand,
|
||||
"institution": destinataer.institution,
|
||||
"projekt_beschreibung": destinataer.projekt_beschreibung,
|
||||
"jaehrliches_einkommen": (
|
||||
str(destinataer.jaehrliches_einkommen)
|
||||
if destinataer.jaehrliches_einkommen
|
||||
else None
|
||||
),
|
||||
"finanzielle_notlage": destinataer.finanzielle_notlage,
|
||||
"ist_abkoemmling": destinataer.ist_abkoemmling,
|
||||
"haushaltsgroesse": destinataer.haushaltsgroesse,
|
||||
"monatliche_bezuege": (
|
||||
str(destinataer.monatliche_bezuege)
|
||||
if destinataer.monatliche_bezuege
|
||||
else None
|
||||
),
|
||||
"vermoegen": (
|
||||
str(destinataer.vermoegen) if destinataer.vermoegen else None
|
||||
),
|
||||
"unterstuetzung_bestaetigt": destinataer.unterstuetzung_bestaetigt,
|
||||
"vierteljaehrlicher_betrag": (
|
||||
str(destinataer.vierteljaehrlicher_betrag)
|
||||
if destinataer.vierteljaehrlicher_betrag
|
||||
else None
|
||||
),
|
||||
"standard_konto": (
|
||||
str(destinataer.standard_konto)
|
||||
if destinataer.standard_konto
|
||||
else None
|
||||
),
|
||||
"studiennachweis_erforderlich": destinataer.studiennachweis_erforderlich,
|
||||
"letzter_studiennachweis": (
|
||||
destinataer.letzter_studiennachweis.isoformat()
|
||||
if destinataer.letzter_studiennachweis
|
||||
else None
|
||||
),
|
||||
"notizen": destinataer.notizen,
|
||||
"aktiv": destinataer.aktiv,
|
||||
"export_datum": timezone.now().isoformat(),
|
||||
"export_user": request.user.username,
|
||||
}
|
||||
zipf.writestr(
|
||||
"destinataer_data.json",
|
||||
json.dumps(entity_data, indent=2, ensure_ascii=False),
|
||||
)
|
||||
|
||||
# 2. Notes with attachments
|
||||
notizen = DestinataerNotiz.objects.filter(destinataer=destinataer).order_by(
|
||||
"-erstellt_am"
|
||||
)
|
||||
notes_data = []
|
||||
for note in notizen:
|
||||
note_data = {
|
||||
"titel": note.titel,
|
||||
"text": note.text,
|
||||
"erstellt_am": note.erstellt_am.isoformat(),
|
||||
"erstellt_von": (
|
||||
note.erstellt_von.username if note.erstellt_von else None
|
||||
),
|
||||
"datei_name": note.datei.name if note.datei else None,
|
||||
}
|
||||
notes_data.append(note_data)
|
||||
|
||||
# Add attachment file if exists
|
||||
if note.datei and os.path.exists(note.datei.path):
|
||||
zipf.write(
|
||||
note.datei.path,
|
||||
f"notizen_anhaenge/{os.path.basename(note.datei.name)}",
|
||||
)
|
||||
|
||||
if notes_data:
|
||||
zipf.writestr(
|
||||
"notizen.json", json.dumps(notes_data, indent=2, ensure_ascii=False)
|
||||
)
|
||||
|
||||
# 3. Linked documents from Paperless
|
||||
dokumente = DokumentLink.objects.filter(destinataer_id=destinataer.pk)
|
||||
docs_data = []
|
||||
for doc in dokumente:
|
||||
doc_data = {
|
||||
"paperless_id": doc.paperless_document_id,
|
||||
"titel": doc.titel,
|
||||
"kontext": doc.get_kontext_display(),
|
||||
"beschreibung": doc.beschreibung,
|
||||
}
|
||||
docs_data.append(doc_data)
|
||||
|
||||
# Try to download document from Paperless
|
||||
try:
|
||||
if (
|
||||
hasattr(settings, "PAPERLESS_API_URL")
|
||||
and settings.PAPERLESS_API_URL
|
||||
):
|
||||
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
|
||||
headers = {}
|
||||
if (
|
||||
hasattr(settings, "PAPERLESS_API_TOKEN")
|
||||
and settings.PAPERLESS_API_TOKEN
|
||||
):
|
||||
headers["Authorization"] = (
|
||||
f"Token {settings.PAPERLESS_API_TOKEN}"
|
||||
)
|
||||
|
||||
response = requests.get(doc_url, headers=headers, timeout=30)
|
||||
if response.status_code == 200:
|
||||
# Determine file extension from Content-Type or use .pdf as fallback
|
||||
content_type = response.headers.get("content-type", "")
|
||||
if "pdf" in content_type:
|
||||
ext = ".pdf"
|
||||
elif "jpeg" in content_type or "jpg" in content_type:
|
||||
ext = ".jpg"
|
||||
elif "png" in content_type:
|
||||
ext = ".png"
|
||||
else:
|
||||
ext = ".pdf" # fallback
|
||||
|
||||
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
|
||||
zipf.writestr(
|
||||
f"dokumente/{safe_filename}", response.content
|
||||
)
|
||||
doc_data["downloaded"] = True
|
||||
else:
|
||||
doc_data["download_error"] = f"HTTP {response.status_code}"
|
||||
except Exception as e:
|
||||
doc_data["download_error"] = str(e)
|
||||
|
||||
if docs_data:
|
||||
zipf.writestr(
|
||||
"dokumente.json",
|
||||
json.dumps(docs_data, indent=2, ensure_ascii=False),
|
||||
)
|
||||
|
||||
# 4. Quarterly Confirmations with documents
|
||||
quarterly_confirmations = destinataer.quartalseinreichungen.all().order_by("-jahr", "-quartal")
|
||||
quarterly_data = []
|
||||
|
||||
for confirmation in quarterly_confirmations:
|
||||
confirmation_data = {
|
||||
"id": str(confirmation.id),
|
||||
"jahr": confirmation.jahr,
|
||||
"quartal": confirmation.quartal,
|
||||
"quartal_display": confirmation.get_quartal_display(),
|
||||
"status": confirmation.status,
|
||||
"status_display": confirmation.get_status_display(),
|
||||
"studiennachweis_erforderlich": confirmation.studiennachweis_erforderlich,
|
||||
"studiennachweis_eingereicht": confirmation.studiennachweis_eingereicht,
|
||||
"studiennachweis_bemerkung": confirmation.studiennachweis_bemerkung,
|
||||
"einkommenssituation_bestaetigt": confirmation.einkommenssituation_bestaetigt,
|
||||
"einkommenssituation_text": confirmation.einkommenssituation_text,
|
||||
"vermogenssituation_bestaetigt": confirmation.vermogenssituation_bestaetigt,
|
||||
"vermogenssituation_text": confirmation.vermogenssituation_text,
|
||||
"weitere_dokumente_beschreibung": confirmation.weitere_dokumente_beschreibung,
|
||||
"interne_notizen": confirmation.interne_notizen,
|
||||
"erstellt_am": confirmation.erstellt_am.isoformat(),
|
||||
"aktualisiert_am": confirmation.aktualisiert_am.isoformat(),
|
||||
"eingereicht_am": confirmation.eingereicht_am.isoformat() if confirmation.eingereicht_am else None,
|
||||
"geprueft_am": confirmation.geprueft_am.isoformat() if confirmation.geprueft_am else None,
|
||||
"geprueft_von": confirmation.geprueft_von.username if confirmation.geprueft_von else None,
|
||||
"faelligkeitsdatum": confirmation.faelligkeitsdatum.isoformat() if confirmation.faelligkeitsdatum else None,
|
||||
"completion_percentage": confirmation.get_completion_percentage(),
|
||||
"uploaded_files": []
|
||||
}
|
||||
|
||||
# Add uploaded files from quarterly confirmation
|
||||
quarterly_files = [
|
||||
("studiennachweis", confirmation.studiennachweis_datei),
|
||||
("einkommenssituation", confirmation.einkommenssituation_datei),
|
||||
("vermogenssituation", confirmation.vermogenssituation_datei),
|
||||
("weitere_dokumente", confirmation.weitere_dokumente),
|
||||
]
|
||||
|
||||
for file_type, file_field in quarterly_files:
|
||||
if file_field and os.path.exists(file_field.path):
|
||||
file_info = {
|
||||
"type": file_type,
|
||||
"name": os.path.basename(file_field.name),
|
||||
"path": file_field.name
|
||||
}
|
||||
confirmation_data["uploaded_files"].append(file_info)
|
||||
|
||||
# Add file to ZIP
|
||||
safe_filename = f"quarterly_{confirmation.jahr}_Q{confirmation.quartal}_{file_type}_{os.path.basename(file_field.name)}"
|
||||
zipf.write(
|
||||
file_field.path,
|
||||
f"vierteljahresnachweis/{safe_filename}"
|
||||
)
|
||||
|
||||
quarterly_data.append(confirmation_data)
|
||||
|
||||
if quarterly_data:
|
||||
zipf.writestr(
|
||||
"vierteljahresnachweis.json",
|
||||
json.dumps(quarterly_data, indent=2, ensure_ascii=False),
|
||||
)
|
||||
|
||||
# Prepare response
|
||||
with open(temp_file.name, "rb") as f:
|
||||
response = HttpResponse(f.read(), content_type="application/zip")
|
||||
filename = f"destinataer_{destinataer.nachname}_{destinataer.vorname}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
|
||||
finally:
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(temp_file.name)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
1453
app/stiftung/views/dokumente.py
Normal file
1453
app/stiftung/views/dokumente.py
Normal file
File diff suppressed because it is too large
Load Diff
743
app/stiftung/views/finanzen.py
Normal file
743
app/stiftung/views/finanzen.py
Normal file
@@ -0,0 +1,743 @@
|
||||
# views/finanzen.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def bericht_list(request):
|
||||
"""List available reports"""
|
||||
# Get available years from data
|
||||
jahre = sorted(
|
||||
set(
|
||||
list(Foerderung.objects.values_list("jahr", flat=True))
|
||||
+ list(LandVerpachtung.objects.values_list("pachtbeginn__year", flat=True))
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# Statistics for overview tiles (removed legacy Person and Verpachtung)
|
||||
total_destinataere = Destinataer.objects.count()
|
||||
total_laendereien = Land.objects.count()
|
||||
total_verpachtungen = LandVerpachtung.objects.count()
|
||||
total_foerderungen = Foerderung.objects.count()
|
||||
|
||||
context = {
|
||||
"jahre": jahre,
|
||||
"title": "Berichte",
|
||||
"total_destinataere": total_destinataere,
|
||||
"total_laendereien": total_laendereien,
|
||||
"total_verpachtungen": total_verpachtungen,
|
||||
"total_foerderungen": total_foerderungen,
|
||||
}
|
||||
return render(request, "stiftung/bericht_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def jahresbericht_generate(request, jahr):
|
||||
"""Generate annual report for a specific year"""
|
||||
# Get data for the year
|
||||
foerderungen = Foerderung.objects.filter(jahr=jahr).select_related("person")
|
||||
verpachtungen = LandVerpachtung.objects.filter(
|
||||
pachtbeginn__year__lte=jahr, pachtende__year__gte=jahr
|
||||
).select_related("land", "paechter")
|
||||
|
||||
# Calculate statistics
|
||||
total_foerderungen = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
|
||||
total_pachtzins = (
|
||||
verpachtungen.aggregate(total=Sum("pachtzins_pauschal"))["total"] or 0
|
||||
)
|
||||
|
||||
context = {
|
||||
"jahr": jahr,
|
||||
"foerderungen": foerderungen,
|
||||
"verpachtungen": verpachtungen,
|
||||
"total_foerderungen": total_foerderungen,
|
||||
"total_pachtzins": total_pachtzins,
|
||||
"title": f"Jahresbericht {jahr}",
|
||||
}
|
||||
return render(request, "stiftung/jahresbericht.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def jahresbericht_generate_redirect(request):
|
||||
"""Redirects the GET form without path param to the proper URL using the provided query param 'jahr'."""
|
||||
jahr = request.GET.get("jahr")
|
||||
if jahr and str(jahr).isdigit():
|
||||
return redirect("stiftung:jahresbericht_generate", jahr=int(jahr))
|
||||
messages.error(request, "Bitte wählen Sie ein gültiges Jahr aus.")
|
||||
return redirect("stiftung:bericht_list")
|
||||
|
||||
|
||||
@login_required
|
||||
def jahresbericht_pdf(request, jahr):
|
||||
"""Generate PDF version of annual report"""
|
||||
from django.http import HttpResponse
|
||||
from django.template.loader import render_to_string
|
||||
from weasyprint import HTML
|
||||
|
||||
# Get data for the year
|
||||
foerderungen = Foerderung.objects.filter(jahr=jahr).select_related("person")
|
||||
verpachtungen = LandVerpachtung.objects.filter(
|
||||
pachtbeginn__year__lte=jahr, pachtende__year__gte=jahr
|
||||
).select_related("land", "paechter")
|
||||
|
||||
# Calculate statistics
|
||||
total_foerderungen = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
|
||||
total_pachtzins = (
|
||||
verpachtungen.aggregate(total=Sum("pachtzins_pauschal"))["total"] or 0
|
||||
)
|
||||
|
||||
context = {
|
||||
"jahr": jahr,
|
||||
"foerderungen": foerderungen,
|
||||
"verpachtungen": verpachtungen,
|
||||
"total_foerderungen": total_foerderungen,
|
||||
"total_pachtzins": total_pachtzins,
|
||||
}
|
||||
|
||||
# Render HTML
|
||||
html_string = render_to_string("stiftung/jahresbericht.html", context)
|
||||
|
||||
# Generate PDF
|
||||
pdf = HTML(string=html_string).write_pdf()
|
||||
|
||||
# Create response
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response["Content-Disposition"] = f'attachment; filename="jahresbericht_{jahr}.pdf"'
|
||||
|
||||
return response
|
||||
|
||||
|
||||
# API Views for AJAX
|
||||
@login_required
|
||||
def geschaeftsfuehrung(request):
|
||||
"""Hauptansicht für die Geschäftsführung mit Übersicht"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.db.models import Count, Sum
|
||||
|
||||
from stiftung.models import Rentmeister, StiftungsKonto, Verwaltungskosten
|
||||
|
||||
# Rentmeister-Übersicht
|
||||
rentmeister = Rentmeister.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
||||
|
||||
# Konten-Übersicht
|
||||
konten = StiftungsKonto.objects.filter(aktiv=True).order_by(
|
||||
"bank_name", "kontoname"
|
||||
)
|
||||
gesamtsaldo = konten.aggregate(total=Sum("saldo"))["total"] or 0
|
||||
|
||||
# Aktuelle Kosten (letzten 30 Tage)
|
||||
heute = datetime.now().date()
|
||||
vor_30_tagen = heute - timedelta(days=30)
|
||||
|
||||
aktuelle_kosten = Verwaltungskosten.objects.filter(
|
||||
datum__gte=vor_30_tagen
|
||||
).order_by("-datum")[:10]
|
||||
|
||||
# Statistiken
|
||||
kosten_summe_monat = (
|
||||
Verwaltungskosten.objects.filter(datum__gte=vor_30_tagen).aggregate(
|
||||
total=Sum("betrag")
|
||||
)["total"]
|
||||
or 0
|
||||
)
|
||||
|
||||
kosten_statistik = (
|
||||
Verwaltungskosten.objects.filter(datum__gte=vor_30_tagen)
|
||||
.values("kategorie")
|
||||
.annotate(summe=Sum("betrag"), anzahl=Count("id"))
|
||||
.order_by("-summe")
|
||||
)
|
||||
|
||||
context = {
|
||||
"rentmeister": rentmeister,
|
||||
"konten": konten,
|
||||
"gesamtsaldo": gesamtsaldo,
|
||||
"aktuelle_kosten": aktuelle_kosten,
|
||||
"kosten_summe_monat": kosten_summe_monat,
|
||||
"kosten_statistik": kosten_statistik,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/geschaeftsfuehrung.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def konto_list(request):
|
||||
"""Liste aller Stiftungskonten"""
|
||||
from django.db.models import Sum
|
||||
|
||||
from stiftung.models import StiftungsKonto
|
||||
|
||||
konten = StiftungsKonto.objects.all().order_by("bank_name", "kontoname")
|
||||
gesamtsaldo = konten.aggregate(total=Sum("saldo"))["total"] or 0
|
||||
|
||||
context = {
|
||||
"konten": konten,
|
||||
"gesamtsaldo": gesamtsaldo,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/konto_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def verwaltungskosten_list(request):
|
||||
"""Liste aller Verwaltungskosten"""
|
||||
from django.core.paginator import Paginator
|
||||
|
||||
from stiftung.models import Verwaltungskosten
|
||||
|
||||
kosten = Verwaltungskosten.objects.all().order_by("-datum", "-erstellt_am")
|
||||
|
||||
# Filter nach Kategorie
|
||||
kategorie_filter = request.GET.get("kategorie")
|
||||
if kategorie_filter:
|
||||
kosten = kosten.filter(kategorie=kategorie_filter)
|
||||
|
||||
# Filter nach Status
|
||||
status_filter = request.GET.get("status")
|
||||
if status_filter:
|
||||
kosten = kosten.filter(status=status_filter)
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(kosten, 25)
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Für Filter-Dropdowns
|
||||
kategorien = Verwaltungskosten.KATEGORIE_CHOICES
|
||||
status_choices = Verwaltungskosten.STATUS_CHOICES
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"kategorien": kategorien,
|
||||
"status_choices": status_choices,
|
||||
"kategorie_filter": kategorie_filter,
|
||||
"status_filter": status_filter,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/verwaltungskosten_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_list(request):
|
||||
"""Liste aller Rentmeister"""
|
||||
from stiftung.models import Rentmeister
|
||||
|
||||
rentmeister = Rentmeister.objects.all().order_by("nachname", "vorname")
|
||||
|
||||
# Aktive/Inaktive aufteilen
|
||||
aktive_rentmeister = rentmeister.filter(aktiv=True)
|
||||
ehemalige_rentmeister = rentmeister.filter(aktiv=False)
|
||||
|
||||
context = {
|
||||
"aktive_rentmeister": aktive_rentmeister,
|
||||
"ehemalige_rentmeister": ehemalige_rentmeister,
|
||||
"total_count": rentmeister.count(),
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_detail(request, pk):
|
||||
"""Detailansicht eines Rentmeisters mit seinen Ausgaben"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.db.models import Count, Q, Sum
|
||||
|
||||
from stiftung.models import Rentmeister, Verwaltungskosten
|
||||
|
||||
rentmeister = get_object_or_404(Rentmeister, pk=pk)
|
||||
|
||||
# Ausgaben des Rentmeisters
|
||||
ausgaben = Verwaltungskosten.objects.filter(rentmeister=rentmeister).order_by(
|
||||
"-datum"
|
||||
)
|
||||
|
||||
# Statistiken
|
||||
heute = datetime.now().date()
|
||||
aktueller_monat = heute.replace(day=1)
|
||||
aktuelles_jahr = heute.replace(month=1, day=1)
|
||||
|
||||
stats = {
|
||||
"gesamt_ausgaben": ausgaben.aggregate(total=Sum("betrag"))["total"] or 0,
|
||||
"monat_ausgaben": ausgaben.filter(datum__gte=aktueller_monat).aggregate(
|
||||
total=Sum("betrag")
|
||||
)["total"]
|
||||
or 0,
|
||||
"jahr_ausgaben": ausgaben.filter(datum__gte=aktuelles_jahr).aggregate(
|
||||
total=Sum("betrag")
|
||||
)["total"]
|
||||
or 0,
|
||||
"anzahl_ausgaben": ausgaben.count(),
|
||||
"offene_ausgaben": ausgaben.exclude(status="bezahlt").count(),
|
||||
}
|
||||
|
||||
# Kategorie-Aufschlüsselung
|
||||
kategorie_stats = (
|
||||
ausgaben.values("kategorie")
|
||||
.annotate(summe=Sum("betrag"), anzahl=Count("id"))
|
||||
.order_by("-summe")
|
||||
)
|
||||
|
||||
# Aktuelle Ausgaben (letzten 30 Tage)
|
||||
vor_30_tagen = heute - timedelta(days=30)
|
||||
aktuelle_ausgaben = ausgaben.filter(datum__gte=vor_30_tagen)[:10]
|
||||
|
||||
# Verknüpfte Dokumente laden
|
||||
from stiftung.models import DokumentLink
|
||||
|
||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
||||
rentmeister_id=rentmeister.id
|
||||
).order_by("-id")[
|
||||
:10
|
||||
] # Neueste 10 Dokumente
|
||||
|
||||
context = {
|
||||
"rentmeister": rentmeister,
|
||||
"ausgaben": ausgaben[:20], # Nur erste 20 für Übersicht
|
||||
"stats": stats,
|
||||
"kategorie_stats": kategorie_stats,
|
||||
"aktuelle_ausgaben": aktuelle_ausgaben,
|
||||
"verknuepfte_dokumente": verknuepfte_dokumente,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_ausgaben(request, pk):
|
||||
"""Vollständige Ausgabenliste eines Rentmeisters mit PDF Export"""
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, Sum
|
||||
|
||||
from stiftung.models import Rentmeister, Verwaltungskosten
|
||||
|
||||
rentmeister = get_object_or_404(Rentmeister, pk=pk)
|
||||
|
||||
# Handle PDF export request
|
||||
if request.method == "POST" and "export_pdf" in request.POST:
|
||||
selected_ids = request.POST.getlist("selected_expenses")
|
||||
if selected_ids:
|
||||
# Update status to 'in_bearbeitung' and log each change
|
||||
from stiftung.audit import log_action
|
||||
|
||||
expenses_to_update = Verwaltungskosten.objects.filter(
|
||||
id__in=selected_ids, rentmeister=rentmeister
|
||||
)
|
||||
|
||||
updated_count = 0
|
||||
for expense in expenses_to_update:
|
||||
old_status = expense.status
|
||||
expense.status = "in_bearbeitung"
|
||||
expense.save()
|
||||
updated_count += 1
|
||||
|
||||
# Log the status change
|
||||
log_action(
|
||||
request=request,
|
||||
action="update",
|
||||
entity_type="verwaltungskosten",
|
||||
entity_id=str(expense.pk),
|
||||
entity_name=expense.bezeichnung,
|
||||
description=f'Ausgaben-Status für PDF-Export geändert von "{old_status}" zu "in_bearbeitung"',
|
||||
changes={"status": {"old": old_status, "new": "in_bearbeitung"}},
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"{updated_count} Ausgaben wurden zur Bearbeitung markiert und sind bereit für PDF Export.",
|
||||
)
|
||||
return redirect(
|
||||
"stiftung:rentmeister_ausgaben_pdf",
|
||||
pk=pk,
|
||||
expense_ids=",".join(selected_ids),
|
||||
)
|
||||
|
||||
# Get expenses grouped by status
|
||||
ausgaben_by_status = {}
|
||||
for status_code, status_name in Verwaltungskosten.STATUS_CHOICES:
|
||||
ausgaben_by_status[status_code] = {
|
||||
"name": status_name,
|
||||
"ausgaben": Verwaltungskosten.objects.filter(
|
||||
rentmeister=rentmeister, status=status_code
|
||||
).order_by("-datum", "-erstellt_am"),
|
||||
"total": Verwaltungskosten.objects.filter(
|
||||
rentmeister=rentmeister, status=status_code
|
||||
).aggregate(total=Sum("betrag"))["total"]
|
||||
or 0,
|
||||
}
|
||||
|
||||
# Get statistics
|
||||
stats = Verwaltungskosten.objects.filter(rentmeister=rentmeister).aggregate(
|
||||
total_count=Count("id"),
|
||||
total_amount=Sum("betrag"),
|
||||
geplant_count=Count("id", filter=Q(status="geplant")),
|
||||
geplant_amount=Sum("betrag", filter=Q(status="geplant")),
|
||||
in_bearbeitung_count=Count("id", filter=Q(status="in_bearbeitung")),
|
||||
in_bearbeitung_amount=Sum("betrag", filter=Q(status="in_bearbeitung")),
|
||||
bezahlt_count=Count("id", filter=Q(status="bezahlt")),
|
||||
bezahlt_amount=Sum("betrag", filter=Q(status="bezahlt")),
|
||||
)
|
||||
|
||||
context = {
|
||||
"rentmeister": rentmeister,
|
||||
"ausgaben_by_status": ausgaben_by_status,
|
||||
"stats": stats,
|
||||
"kategorien": Verwaltungskosten.KATEGORIE_CHOICES,
|
||||
"status_choices": Verwaltungskosten.STATUS_CHOICES,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_ausgaben.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_create(request):
|
||||
"""Erstelle einen neuen Rentmeister"""
|
||||
from stiftung.forms import RentmeisterForm
|
||||
|
||||
if request.method == "POST":
|
||||
form = RentmeisterForm(request.POST)
|
||||
if form.is_valid():
|
||||
rentmeister = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Rentmeister {rentmeister.get_full_name()} wurde erfolgreich angelegt.",
|
||||
)
|
||||
return redirect("stiftung:rentmeister_detail", pk=rentmeister.pk)
|
||||
else:
|
||||
form = RentmeisterForm()
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"title": "Neuen Rentmeister anlegen",
|
||||
"submit_text": "Rentmeister anlegen",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_edit(request, pk):
|
||||
"""Bearbeite einen bestehenden Rentmeister"""
|
||||
from stiftung.forms import RentmeisterForm
|
||||
from stiftung.models import Rentmeister
|
||||
|
||||
rentmeister = get_object_or_404(Rentmeister, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = RentmeisterForm(request.POST, instance=rentmeister)
|
||||
if form.is_valid():
|
||||
rentmeister = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Rentmeister {rentmeister.get_full_name()} wurde erfolgreich aktualisiert.",
|
||||
)
|
||||
return redirect("stiftung:rentmeister_detail", pk=rentmeister.pk)
|
||||
else:
|
||||
form = RentmeisterForm(instance=rentmeister)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"rentmeister": rentmeister,
|
||||
"title": f"{rentmeister.get_full_name()} bearbeiten",
|
||||
"submit_text": "Änderungen speichern",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def konto_create(request):
|
||||
"""Erstelle ein neues Stiftungskonto"""
|
||||
from stiftung.forms import StiftungsKontoForm
|
||||
|
||||
if request.method == "POST":
|
||||
form = StiftungsKontoForm(request.POST)
|
||||
if form.is_valid():
|
||||
konto = form.save()
|
||||
messages.success(
|
||||
request, f"Konto {konto.kontoname} wurde erfolgreich angelegt."
|
||||
)
|
||||
return redirect("stiftung:konto_list")
|
||||
else:
|
||||
form = StiftungsKontoForm()
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"title": "Neues Konto anlegen",
|
||||
"submit_text": "Konto anlegen",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/konto_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def konto_edit(request, pk):
|
||||
"""Bearbeite ein bestehendes Stiftungskonto"""
|
||||
from stiftung.forms import StiftungsKontoForm
|
||||
from stiftung.models import StiftungsKonto
|
||||
|
||||
konto = get_object_or_404(StiftungsKonto, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = StiftungsKontoForm(request.POST, instance=konto)
|
||||
if form.is_valid():
|
||||
konto = form.save()
|
||||
messages.success(
|
||||
request, f"Konto {konto.kontoname} wurde erfolgreich aktualisiert."
|
||||
)
|
||||
return redirect("stiftung:konto_list")
|
||||
else:
|
||||
form = StiftungsKontoForm(instance=konto)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"konto": konto,
|
||||
"title": f"Konto {konto.kontoname} bearbeiten",
|
||||
"submit_text": "Änderungen speichern",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/konto_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def konto_detail(request, pk):
|
||||
"""Zeige Details eines Stiftungskontos"""
|
||||
from django.db import models
|
||||
from django.db.models import Count, Max, Q, Sum
|
||||
|
||||
from stiftung.models import BankTransaction, StiftungsKonto
|
||||
|
||||
konto = get_object_or_404(StiftungsKonto, pk=pk)
|
||||
|
||||
# Get transaction statistics
|
||||
transactions = BankTransaction.objects.filter(konto=konto)
|
||||
transaction_stats = transactions.aggregate(
|
||||
total_count=Count("id"),
|
||||
total_eingang=Sum("betrag", filter=Q(betrag__gt=0)),
|
||||
total_ausgang=Sum("betrag", filter=Q(betrag__lt=0)),
|
||||
last_transaction_date=Max("datum"),
|
||||
)
|
||||
|
||||
# Recent transactions
|
||||
recent_transactions = transactions.order_by("-datum", "-importiert_am")[:10]
|
||||
|
||||
context = {
|
||||
"konto": konto,
|
||||
"transaction_stats": transaction_stats,
|
||||
"recent_transactions": recent_transactions,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/konto_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def verwaltungskosten_create(request):
|
||||
"""Erstelle neue Verwaltungskosten"""
|
||||
from stiftung.forms import VerwaltungskostenForm
|
||||
from stiftung.models import Rentmeister
|
||||
|
||||
# Check if we're coming from a specific Rentmeister
|
||||
rentmeister_id = request.GET.get("rentmeister")
|
||||
initial_data = {}
|
||||
redirect_url = "stiftung:verwaltungskosten_list"
|
||||
|
||||
if rentmeister_id:
|
||||
try:
|
||||
rentmeister = Rentmeister.objects.get(pk=rentmeister_id)
|
||||
initial_data["rentmeister"] = rentmeister
|
||||
redirect_url = "stiftung:rentmeister_detail"
|
||||
except Rentmeister.DoesNotExist:
|
||||
pass
|
||||
|
||||
if request.method == "POST":
|
||||
form = VerwaltungskostenForm(request.POST)
|
||||
if form.is_valid():
|
||||
kosten = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Verwaltungskosten "{kosten.bezeichnung}" wurden erfolgreich angelegt.',
|
||||
)
|
||||
if rentmeister_id:
|
||||
return redirect(redirect_url, pk=rentmeister_id)
|
||||
return redirect("stiftung:verwaltungskosten_list")
|
||||
else:
|
||||
form = VerwaltungskostenForm(initial=initial_data)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"title": "Neue Verwaltungskosten anlegen",
|
||||
"submit_text": "Kosten anlegen",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/verwaltungskosten_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def verwaltungskosten_edit(request, pk):
|
||||
"""Bearbeite bestehende Verwaltungskosten"""
|
||||
from stiftung.forms import VerwaltungskostenForm
|
||||
from stiftung.models import Verwaltungskosten
|
||||
|
||||
verwaltungskosten = get_object_or_404(Verwaltungskosten, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VerwaltungskostenForm(request.POST, instance=verwaltungskosten)
|
||||
if form.is_valid():
|
||||
verwaltungskosten = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Verwaltungskosten "{verwaltungskosten.bezeichnung}" wurden erfolgreich aktualisiert.',
|
||||
)
|
||||
return redirect("stiftung:verwaltungskosten_list")
|
||||
else:
|
||||
form = VerwaltungskostenForm(instance=verwaltungskosten)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"verwaltungskosten": verwaltungskosten,
|
||||
"title": f"Verwaltungskosten bearbeiten: {verwaltungskosten.bezeichnung}",
|
||||
"submit_text": "Änderungen speichern",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/verwaltungskosten_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def verwaltungskosten_delete(request, pk):
|
||||
"""Lösche Verwaltungskosten"""
|
||||
from stiftung.models import Verwaltungskosten
|
||||
|
||||
verwaltungskosten = get_object_or_404(Verwaltungskosten, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
bezeichnung = verwaltungskosten.bezeichnung
|
||||
|
||||
# Log the deletion
|
||||
from stiftung.audit import log_action
|
||||
log_action(
|
||||
request=request,
|
||||
action="delete",
|
||||
entity_type="verwaltungskosten",
|
||||
entity_id=str(verwaltungskosten.pk),
|
||||
entity_name=bezeichnung,
|
||||
description=f'Verwaltungskosten "{bezeichnung}" wurden gelöscht',
|
||||
)
|
||||
|
||||
verwaltungskosten.delete()
|
||||
messages.success(
|
||||
request,
|
||||
f'Verwaltungskosten "{bezeichnung}" wurden erfolgreich gelöscht.',
|
||||
)
|
||||
return redirect("stiftung:verwaltungskosten_list")
|
||||
|
||||
context = {
|
||||
"verwaltungskosten": verwaltungskosten,
|
||||
"title": f"Verwaltungskosten löschen: {verwaltungskosten.bezeichnung}",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/verwaltungskosten_delete.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def mark_expense_paid(request):
|
||||
"""Markiere eine Ausgabe als bezahlt"""
|
||||
if request.method == "POST":
|
||||
expense_id = request.POST.get("expense_id")
|
||||
if expense_id:
|
||||
try:
|
||||
from stiftung.models import Verwaltungskosten
|
||||
|
||||
expense = Verwaltungskosten.objects.get(pk=expense_id)
|
||||
old_status = expense.status
|
||||
expense.status = "bezahlt"
|
||||
expense.save()
|
||||
|
||||
# Log the status change
|
||||
from stiftung.audit import log_action
|
||||
|
||||
log_action(
|
||||
request=request,
|
||||
action="update",
|
||||
entity_type="verwaltungskosten",
|
||||
entity_id=str(expense.pk),
|
||||
entity_name=expense.bezeichnung,
|
||||
description=f'Ausgaben-Status geändert von "{old_status}" zu "bezahlt"',
|
||||
changes={"status": {"old": old_status, "new": "bezahlt"}},
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Ausgabe "{expense.bezeichnung}" wurde als bezahlt markiert.',
|
||||
)
|
||||
return redirect(
|
||||
"stiftung:rentmeister_ausgaben", pk=expense.rentmeister.pk
|
||||
)
|
||||
except Verwaltungskosten.DoesNotExist:
|
||||
messages.error(request, "Ausgabe nicht gefunden.")
|
||||
|
||||
return redirect("stiftung:verwaltungskosten_list")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ADMINISTRATION VIEWS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
236
app/stiftung/views/foerderung.py
Normal file
236
app/stiftung/views/foerderung.py
Normal file
@@ -0,0 +1,236 @@
|
||||
# views/foerderung.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_list(request):
|
||||
"""List all funding grants with filtering and pagination"""
|
||||
foerderungen = Foerderung.objects.select_related(
|
||||
"destinataer", "verwendungsnachweis"
|
||||
).all()
|
||||
|
||||
# Check for export request - handle both GET and POST
|
||||
export_format = (
|
||||
request.POST.get("format")
|
||||
if request.method == "POST"
|
||||
else request.GET.get("format", "")
|
||||
)
|
||||
selected_ids_param = (
|
||||
request.POST.get("selected_entries", "")
|
||||
if request.method == "POST"
|
||||
else request.GET.get("selected_entries", "")
|
||||
)
|
||||
selected_ids = (
|
||||
[id for id in selected_ids_param.split(",") if id] if selected_ids_param else []
|
||||
)
|
||||
|
||||
# Filtering
|
||||
jahr = request.GET.get("jahr")
|
||||
kategorie = request.GET.get("kategorie")
|
||||
status = request.GET.get("status")
|
||||
destinataer = request.GET.get("destinataer")
|
||||
|
||||
if jahr:
|
||||
foerderungen = foerderungen.filter(jahr=int(jahr))
|
||||
if kategorie:
|
||||
foerderungen = foerderungen.filter(kategorie=kategorie)
|
||||
if status:
|
||||
foerderungen = foerderungen.filter(status=status)
|
||||
if destinataer:
|
||||
foerderungen = foerderungen.filter(destinataer__nachname__icontains=destinataer)
|
||||
|
||||
# Handle exports
|
||||
if export_format == "csv":
|
||||
return export_foerderungen_csv(request, foerderungen, selected_ids)
|
||||
elif export_format == "pdf":
|
||||
return export_foerderungen_pdf(request, foerderungen, selected_ids)
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(foerderungen, 25)
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Statistics
|
||||
total_betrag = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
|
||||
avg_betrag = foerderungen.aggregate(avg=Avg("betrag"))["avg"] or 0
|
||||
|
||||
# Year choices for filters
|
||||
jahre = sorted(
|
||||
set(list(Foerderung.objects.values_list("jahr", flat=True))), reverse=True
|
||||
)
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"foerderungen": foerderungen, # Add for counting
|
||||
"total_betrag": total_betrag,
|
||||
"avg_betrag": avg_betrag,
|
||||
"kategorien": Foerderung.KATEGORIE_CHOICES,
|
||||
"status_choices": Foerderung.STATUS_CHOICES,
|
||||
"filter_jahr": jahr,
|
||||
"filter_kategorie": kategorie,
|
||||
"filter_status": status,
|
||||
"filter_person": destinataer,
|
||||
"jahre": jahre,
|
||||
}
|
||||
return render(request, "stiftung/foerderung_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_detail(request, pk):
|
||||
"""Show details of a specific funding grant"""
|
||||
foerderung = get_object_or_404(
|
||||
Foerderung.objects.select_related("person", "verwendungsnachweis"), pk=pk
|
||||
)
|
||||
|
||||
# Alle mit dieser Förderung verknüpften Dokumente laden
|
||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
||||
foerderung_id=foerderung.pk
|
||||
).order_by("kontext", "titel")
|
||||
|
||||
context = {
|
||||
"foerderung": foerderung,
|
||||
"verknuepfte_dokumente": verknuepfte_dokumente,
|
||||
"title": f"Förderung: {foerderung}",
|
||||
}
|
||||
return render(request, "stiftung/foerderung_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_create(request):
|
||||
"""Create a new funding grant"""
|
||||
# Get destinataer from URL parameter if provided
|
||||
destinataer_id = request.GET.get("destinataer")
|
||||
initial = {}
|
||||
if destinataer_id:
|
||||
initial["destinataer"] = destinataer_id
|
||||
|
||||
if request.method == "POST":
|
||||
form = FoerderungForm(request.POST)
|
||||
if form.is_valid():
|
||||
foerderung = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Förderung für {foerderung.destinataer} wurde erfolgreich erstellt.",
|
||||
)
|
||||
return redirect("stiftung:foerderung_detail", pk=foerderung.pk)
|
||||
else:
|
||||
form = FoerderungForm(initial=initial)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"title": "Neue Förderung erstellen",
|
||||
}
|
||||
return render(request, "stiftung/foerderung_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_update(request, pk):
|
||||
"""Update an existing funding grant"""
|
||||
foerderung = get_object_or_404(Foerderung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = FoerderungForm(request.POST, instance=foerderung)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Förderung für {foerderung.person} wurde erfolgreich aktualisiert.",
|
||||
)
|
||||
return redirect("stiftung:foerderung_detail", pk=foerderung.pk)
|
||||
else:
|
||||
form = FoerderungForm(instance=foerderung)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"foerderung": foerderung,
|
||||
"title": f"Förderung bearbeiten: {foerderung}",
|
||||
}
|
||||
return render(request, "stiftung/foerderung_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_delete(request, pk):
|
||||
"""Delete a funding grant"""
|
||||
foerderung = get_object_or_404(Foerderung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
# Get the recipient name before deletion
|
||||
recipient_name = (
|
||||
foerderung.destinataer.get_full_name()
|
||||
if foerderung.destinataer
|
||||
else (
|
||||
foerderung.person.get_full_name()
|
||||
if foerderung.person
|
||||
else "Unbekannter Empfänger"
|
||||
)
|
||||
)
|
||||
|
||||
foerderung.delete()
|
||||
messages.success(
|
||||
request, f"Förderung für {recipient_name} wurde erfolgreich gelöscht."
|
||||
)
|
||||
return redirect("stiftung:foerderung_list")
|
||||
|
||||
context = {
|
||||
"foerderung": foerderung,
|
||||
"title": f"Förderung löschen: {foerderung}",
|
||||
}
|
||||
return render(request, "stiftung/foerderung_confirm_delete.html", context)
|
||||
|
||||
|
||||
# DokumentLink Views
|
||||
710
app/stiftung/views/geschichte.py
Normal file
710
app/stiftung/views/geschichte.py
Normal file
@@ -0,0 +1,710 @@
|
||||
# views/geschichte.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_list(request):
|
||||
"""List all published history pages"""
|
||||
seiten = GeschichteSeite.objects.filter(ist_veroeffentlicht=True).order_by('sortierung', 'titel')
|
||||
|
||||
context = {
|
||||
'seiten': seiten,
|
||||
'title': 'Geschichte der Stiftung'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/liste.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_detail(request, slug):
|
||||
"""Display a specific history page"""
|
||||
seite = get_object_or_404(GeschichteSeite, slug=slug, ist_veroeffentlicht=True)
|
||||
bilder = seite.bilder.all().order_by('sortierung', 'titel')
|
||||
|
||||
context = {
|
||||
'seite': seite,
|
||||
'bilder': bilder,
|
||||
'title': seite.titel
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/detail.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_create(request):
|
||||
"""Create a new history page"""
|
||||
if not request.user.has_perm('stiftung.add_geschichteseite'):
|
||||
messages.error(request, 'Sie haben keine Berechtigung, neue Geschichtsseiten zu erstellen.')
|
||||
return redirect('stiftung:geschichte_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = GeschichteSeiteForm(request.POST)
|
||||
if form.is_valid():
|
||||
seite = form.save(commit=False)
|
||||
seite.erstellt_von = request.user
|
||||
seite.aktualisiert_von = request.user
|
||||
seite.save()
|
||||
|
||||
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich erstellt.')
|
||||
return redirect('stiftung:geschichte_detail', slug=seite.slug)
|
||||
else:
|
||||
form = GeschichteSeiteForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'title': 'Neue Geschichtsseite'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_edit(request, slug):
|
||||
"""Edit an existing history page"""
|
||||
seite = get_object_or_404(GeschichteSeite, slug=slug)
|
||||
|
||||
if not request.user.has_perm('stiftung.change_geschichteseite'):
|
||||
messages.error(request, 'Sie haben keine Berechtigung, diese Geschichtsseite zu bearbeiten.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = GeschichteSeiteForm(request.POST, instance=seite)
|
||||
if form.is_valid():
|
||||
seite = form.save(commit=False)
|
||||
seite.aktualisiert_von = request.user
|
||||
seite.save()
|
||||
|
||||
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich aktualisiert.')
|
||||
return redirect('stiftung:geschichte_detail', slug=seite.slug)
|
||||
else:
|
||||
form = GeschichteSeiteForm(instance=seite)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'seite': seite,
|
||||
'title': f'Bearbeiten: {seite.titel}'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_bild_upload(request, slug):
|
||||
"""Upload images to a history page"""
|
||||
seite = get_object_or_404(GeschichteSeite, slug=slug)
|
||||
|
||||
if not request.user.has_perm('stiftung.add_geschichtebild'):
|
||||
messages.error(request, 'Sie haben keine Berechtigung, Bilder hochzuladen.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = GeschichteBildForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
bild = form.save(commit=False)
|
||||
bild.seite = seite
|
||||
bild.hochgeladen_von = request.user
|
||||
bild.save()
|
||||
|
||||
messages.success(request, f'Bild "{bild.titel}" wurde erfolgreich hochgeladen.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
else:
|
||||
form = GeschichteBildForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'seite': seite,
|
||||
'title': f'Bild hochladen: {seite.titel}'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/bild_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_bild_delete(request, slug, bild_id):
|
||||
"""Delete an image from a history page"""
|
||||
seite = get_object_or_404(GeschichteSeite, slug=slug)
|
||||
bild = get_object_or_404(GeschichteBild, id=bild_id, seite=seite)
|
||||
|
||||
if not request.user.has_perm('stiftung.delete_geschichtebild'):
|
||||
messages.error(request, 'Sie haben keine Berechtigung, Bilder zu löschen.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
bild_titel = bild.titel
|
||||
bild.delete()
|
||||
messages.success(request, f'Bild "{bild_titel}" wurde erfolgreich gelöscht.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
context = {
|
||||
'bild': bild,
|
||||
'seite': seite,
|
||||
'title': f'Bild löschen: {bild.titel}'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/bild_delete.html', context)
|
||||
|
||||
|
||||
# Calendar Views
|
||||
@login_required
|
||||
def kalender_view(request):
|
||||
"""Main calendar view with different view types"""
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
import calendar as cal
|
||||
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get current date and view parameters
|
||||
today = timezone.now().date()
|
||||
view_type = request.GET.get('view', 'month') # month, week, list, agenda
|
||||
year = int(request.GET.get('year', today.year))
|
||||
month = int(request.GET.get('month', today.month))
|
||||
|
||||
# Calculate date ranges based on view type
|
||||
if view_type == 'month':
|
||||
# Get events for the entire month
|
||||
start_date = date(year, month, 1)
|
||||
_, last_day = cal.monthrange(year, month)
|
||||
end_date = date(year, month, last_day)
|
||||
title_suffix = f"{cal.month_name[month]} {year}"
|
||||
|
||||
elif view_type == 'week':
|
||||
# Get current week
|
||||
week_start = today - timedelta(days=today.weekday())
|
||||
start_date = week_start
|
||||
end_date = week_start + timedelta(days=6)
|
||||
title_suffix = f"Woche vom {start_date.strftime('%d.%m')} - {end_date.strftime('%d.%m.%Y')}"
|
||||
|
||||
elif view_type == 'agenda':
|
||||
# Next 30 days
|
||||
start_date = today
|
||||
end_date = today + timedelta(days=30)
|
||||
title_suffix = "Nächste 30 Tage"
|
||||
|
||||
else: # list view
|
||||
# Next 90 days
|
||||
start_date = today
|
||||
end_date = today + timedelta(days=90)
|
||||
title_suffix = "Liste (nächste 90 Tage)"
|
||||
|
||||
# Get events for the date range
|
||||
events = calendar_service.get_all_events(start_date, end_date)
|
||||
|
||||
# Generate calendar grid for month view
|
||||
calendar_grid = None
|
||||
if view_type == 'month':
|
||||
calendar_grid = []
|
||||
first_day = date(year, month, 1)
|
||||
month_cal = cal.monthcalendar(year, month)
|
||||
|
||||
for week in month_cal:
|
||||
week_data = []
|
||||
for day in week:
|
||||
if day == 0:
|
||||
week_data.append(None)
|
||||
else:
|
||||
day_date = date(year, month, day)
|
||||
day_events = [e for e in events if e.date == day_date]
|
||||
week_data.append({
|
||||
'day': day,
|
||||
'date': day_date,
|
||||
'is_today': day_date == today,
|
||||
'events': day_events[:3], # Show max 3 events per day
|
||||
'event_count': len(day_events)
|
||||
})
|
||||
calendar_grid.append(week_data)
|
||||
|
||||
# Navigation dates for month view
|
||||
if month > 1:
|
||||
prev_month = month - 1
|
||||
prev_year = year
|
||||
else:
|
||||
prev_month = 12
|
||||
prev_year = year - 1
|
||||
|
||||
if month < 12:
|
||||
next_month = month + 1
|
||||
next_year = year
|
||||
else:
|
||||
next_month = 1
|
||||
next_year = year + 1
|
||||
|
||||
context = {
|
||||
'title': f'Kalender - {title_suffix}',
|
||||
'events': events,
|
||||
'calendar_grid': calendar_grid,
|
||||
'view_type': view_type,
|
||||
'year': year,
|
||||
'month': month,
|
||||
'today': today,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'prev_year': prev_year,
|
||||
'prev_month': prev_month,
|
||||
'next_year': next_year,
|
||||
'next_month': next_month,
|
||||
'month_name': cal.month_name[month],
|
||||
'weekdays': ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'],
|
||||
}
|
||||
|
||||
# Choose template based on view type
|
||||
if view_type == 'month':
|
||||
template = 'stiftung/kalender/month_view.html'
|
||||
elif view_type == 'week':
|
||||
template = 'stiftung/kalender/week_view.html'
|
||||
elif view_type == 'agenda':
|
||||
template = 'stiftung/kalender/agenda_view.html'
|
||||
else:
|
||||
template = 'stiftung/kalender/list_view.html'
|
||||
|
||||
return render(request, template, context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_create(request):
|
||||
"""Create new calendar event"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
if request.method == 'POST':
|
||||
# Simple form handling - you can enhance this with Django forms
|
||||
titel = request.POST.get('titel')
|
||||
beschreibung = request.POST.get('beschreibung', '')
|
||||
datum = request.POST.get('datum')
|
||||
kategorie = request.POST.get('kategorie', 'termin')
|
||||
prioritaet = request.POST.get('prioritaet', 'normal')
|
||||
|
||||
if titel and datum:
|
||||
zeit_str = request.POST.get('zeit')
|
||||
uhrzeit = zeit_str if zeit_str else None
|
||||
ganztags = not bool(zeit_str)
|
||||
|
||||
StiftungsKalenderEintrag.objects.create(
|
||||
titel=titel,
|
||||
beschreibung=beschreibung,
|
||||
datum=datum,
|
||||
uhrzeit=uhrzeit,
|
||||
ganztags=ganztags,
|
||||
kategorie=kategorie,
|
||||
prioritaet=prioritaet,
|
||||
erstellt_von=request.user.username
|
||||
)
|
||||
messages.success(request, 'Kalendereintrag wurde erfolgreich erstellt.')
|
||||
return redirect('stiftung:kalender')
|
||||
else:
|
||||
messages.error(request, 'Titel und Datum sind erforderlich.')
|
||||
|
||||
context = {
|
||||
'title': 'Neuer Kalendereintrag',
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/create.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_detail(request, pk):
|
||||
"""Calendar event detail view"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
|
||||
|
||||
context = {
|
||||
'title': f'Kalendereintrag: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/detail.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_edit(request, pk):
|
||||
"""Edit calendar event"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
event.titel = request.POST.get('titel', event.titel)
|
||||
event.beschreibung = request.POST.get('beschreibung', event.beschreibung)
|
||||
event.datum = request.POST.get('datum', event.datum)
|
||||
zeit_str = request.POST.get('zeit')
|
||||
if zeit_str:
|
||||
event.uhrzeit = zeit_str
|
||||
event.ganztags = False
|
||||
else:
|
||||
event.uhrzeit = None
|
||||
event.ganztags = True
|
||||
event.kategorie = request.POST.get('kategorie', event.kategorie)
|
||||
event.prioritaet = request.POST.get('prioritaet', event.prioritaet)
|
||||
event.erledigt = 'erledigt' in request.POST
|
||||
|
||||
event.save()
|
||||
messages.success(request, 'Kalendereintrag wurde aktualisiert.')
|
||||
return redirect('stiftung:kalender_detail', pk=pk)
|
||||
|
||||
context = {
|
||||
'title': f'Bearbeiten: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/edit.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_delete(request, pk):
|
||||
"""Delete calendar event"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
event_titel = event.titel
|
||||
event.delete()
|
||||
messages.success(request, f'Kalendereintrag "{event_titel}" wurde gelöscht.')
|
||||
return redirect('stiftung:kalender')
|
||||
|
||||
context = {
|
||||
'title': f'Löschen: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/delete_confirm.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_admin(request):
|
||||
"""Calendar administration with event sources and management"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
|
||||
# Get filter parameters
|
||||
show_custom = request.GET.get('show_custom', 'true') == 'true'
|
||||
show_payments = request.GET.get('show_payments', 'true') == 'true'
|
||||
show_leases = request.GET.get('show_leases', 'true') == 'true'
|
||||
show_birthdays = request.GET.get('show_birthdays', 'true') == 'true'
|
||||
category_filter = request.GET.get('category', '')
|
||||
priority_filter = request.GET.get('priority', '')
|
||||
|
||||
# Initialize calendar service
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get events based on filters
|
||||
from datetime import date, timedelta
|
||||
start_date = date.today() - timedelta(days=30)
|
||||
end_date = date.today() + timedelta(days=90)
|
||||
|
||||
all_events = []
|
||||
|
||||
# Custom calendar entries
|
||||
if show_custom:
|
||||
custom_events = calendar_service.get_calendar_events(start_date, end_date)
|
||||
all_events.extend(custom_events)
|
||||
|
||||
# Payment events
|
||||
if show_payments:
|
||||
payment_events = calendar_service.get_support_payment_events(start_date, end_date)
|
||||
all_events.extend(payment_events)
|
||||
|
||||
# Lease events
|
||||
if show_leases:
|
||||
lease_events = calendar_service.get_lease_events(start_date, end_date)
|
||||
all_events.extend(lease_events)
|
||||
|
||||
# Birthday events
|
||||
if show_birthdays:
|
||||
birthday_events = calendar_service.get_birthday_events(start_date, end_date)
|
||||
all_events.extend(birthday_events)
|
||||
|
||||
# Filter by category and priority if specified
|
||||
if category_filter:
|
||||
all_events = [e for e in all_events if getattr(e, 'category', '') == category_filter]
|
||||
|
||||
if priority_filter:
|
||||
all_events = [e for e in all_events if getattr(e, 'priority', '') == priority_filter]
|
||||
|
||||
# Sort events by date
|
||||
all_events.sort(key=lambda x: x.date)
|
||||
|
||||
# Get statistics
|
||||
custom_count = StiftungsKalenderEintrag.objects.count()
|
||||
total_events = len(all_events)
|
||||
|
||||
# Event source statistics
|
||||
stats = {
|
||||
'custom_events': len([e for e in all_events if getattr(e, 'source', '') == 'custom']),
|
||||
'payment_events': len([e for e in all_events if getattr(e, 'source', '') == 'payment']),
|
||||
'lease_events': len([e for e in all_events if getattr(e, 'source', '') == 'lease']),
|
||||
'birthday_events': len([e for e in all_events if getattr(e, 'source', '') == 'birthday']),
|
||||
'total_events': total_events,
|
||||
'custom_count': custom_count,
|
||||
}
|
||||
|
||||
context = {
|
||||
'title': 'Kalender Administration',
|
||||
'events': all_events,
|
||||
'stats': stats,
|
||||
'show_custom': show_custom,
|
||||
'show_payments': show_payments,
|
||||
'show_leases': show_leases,
|
||||
'show_birthdays': show_birthdays,
|
||||
'category_filter': category_filter,
|
||||
'priority_filter': priority_filter,
|
||||
'categories': StiftungsKalenderEintrag.KATEGORIE_CHOICES,
|
||||
'priorities': StiftungsKalenderEintrag.PRIORITAET_CHOICES,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/admin.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_api_events(request):
|
||||
"""API endpoint for calendar events (JSON)"""
|
||||
from django.http import JsonResponse
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
from datetime import datetime
|
||||
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get date range from request
|
||||
start_date = request.GET.get('start')
|
||||
end_date = request.GET.get('end')
|
||||
|
||||
if start_date and end_date:
|
||||
try:
|
||||
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||||
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return JsonResponse({'error': 'Invalid date format'}, status=400)
|
||||
|
||||
events = calendar_service.get_all_events(start_date, end_date)
|
||||
else:
|
||||
events = calendar_service.get_all_events()
|
||||
|
||||
# Convert to FullCalendar format
|
||||
calendar_events = []
|
||||
for event in events:
|
||||
calendar_events.append({
|
||||
'id': getattr(event, 'id', str(event.title)),
|
||||
'title': event.title,
|
||||
'start': event.date.strftime('%Y-%m-%d'),
|
||||
'description': getattr(event, 'description', ''),
|
||||
'className': f"event-{event.category}",
|
||||
'backgroundColor': f"var(--bs-{event.color})",
|
||||
'borderColor': f"var(--bs-{event.color})",
|
||||
})
|
||||
|
||||
return JsonResponse(calendar_events, safe=False)
|
||||
|
||||
|
||||
# Calendar Views
|
||||
@login_required
|
||||
def kalender_view(request):
|
||||
"""Full calendar view with all events"""
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get current month events by default
|
||||
today = timezone.now().date()
|
||||
events = calendar_service.get_events_for_month(today.year, today.month)
|
||||
|
||||
context = {
|
||||
'events': events,
|
||||
'title': 'Stiftungskalender',
|
||||
'current_month': today.strftime('%B %Y'),
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/kalender.html', context)
|
||||
|
||||
|
||||
context = {
|
||||
'title': 'Kalendereintrag löschen'
|
||||
}
|
||||
return render(request, 'stiftung/kalender/delete.html', context)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# E-Mail-Eingang – Destinatäre
|
||||
# =============================================================================
|
||||
|
||||
@login_required
|
||||
def email_eingang_list(request):
|
||||
"""
|
||||
Übersicht aller eingegangenen E-Mails von Destinatären.
|
||||
Zeigt ungeklärte Absender zuerst, dann chronologisch absteigend.
|
||||
"""
|
||||
status_filter = request.GET.get("status", "")
|
||||
search = request.GET.get("q", "").strip()
|
||||
|
||||
qs = DestinataerEmailEingang.objects.select_related("destinataer", "quartalsnachweis")
|
||||
|
||||
if status_filter:
|
||||
qs = qs.filter(status=status_filter)
|
||||
if search:
|
||||
qs = qs.filter(
|
||||
Q(absender_email__icontains=search)
|
||||
| Q(absender_name__icontains=search)
|
||||
| Q(betreff__icontains=search)
|
||||
| Q(destinataer__vorname__icontains=search)
|
||||
| Q(destinataer__nachname__icontains=search)
|
||||
)
|
||||
|
||||
# Unbekannte Absender zuerst, dann nach Datum absteigend
|
||||
qs = qs.order_by(
|
||||
"status", # "unbekannt" kommt alphabetisch vor "verarbeitet" / "zugewiesen"
|
||||
"-eingangsdatum",
|
||||
)
|
||||
|
||||
paginator = Paginator(qs, 30)
|
||||
page_obj = paginator.get_page(request.GET.get("page"))
|
||||
|
||||
context = {
|
||||
"title": "E-Mail-Eingang (Destinatäre)",
|
||||
"page_obj": page_obj,
|
||||
"status_filter": status_filter,
|
||||
"search": search,
|
||||
"status_choices": DestinataerEmailEingang.STATUS_CHOICES,
|
||||
"counts": {
|
||||
"gesamt": DestinataerEmailEingang.objects.count(),
|
||||
"neu": DestinataerEmailEingang.objects.filter(status="neu").count(),
|
||||
"unbekannt": DestinataerEmailEingang.objects.filter(status="unbekannt").count(),
|
||||
"fehler": DestinataerEmailEingang.objects.filter(status="fehler").count(),
|
||||
},
|
||||
}
|
||||
return render(request, "stiftung/email_eingang/list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def email_eingang_detail(request, pk):
|
||||
"""Detailansicht einer eingegangenen E-Mail mit Möglichkeit zur manuellen Zuordnung."""
|
||||
eingang = get_object_or_404(DestinataerEmailEingang, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
action = request.POST.get("action")
|
||||
|
||||
if action == "assign_destinataer":
|
||||
dest_id = request.POST.get("destinataer_id")
|
||||
if dest_id:
|
||||
try:
|
||||
destinataer = Destinataer.objects.get(pk=dest_id)
|
||||
eingang.destinataer = destinataer
|
||||
eingang.status = "zugewiesen"
|
||||
eingang.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"E-Mail wurde {destinataer} zugeordnet.",
|
||||
)
|
||||
except Destinataer.DoesNotExist:
|
||||
messages.error(request, "Destinatär nicht gefunden.")
|
||||
return redirect("email_eingang_detail", pk=pk)
|
||||
|
||||
elif action == "mark_verarbeitet":
|
||||
eingang.status = "verarbeitet"
|
||||
eingang.notizen = request.POST.get("notizen", eingang.notizen)
|
||||
eingang.save()
|
||||
messages.success(request, "E-Mail als verarbeitet markiert.")
|
||||
return redirect("email_eingang_list")
|
||||
|
||||
elif action == "save_notizen":
|
||||
eingang.notizen = request.POST.get("notizen", "")
|
||||
eingang.save()
|
||||
messages.success(request, "Notizen gespeichert.")
|
||||
return redirect("email_eingang_detail", pk=pk)
|
||||
|
||||
# Paperless-Links zusammenstellen
|
||||
paperless_links = eingang.get_paperless_links()
|
||||
|
||||
# DokumentLinks für diese E-Mail (über paperless_dokument_ids)
|
||||
dokument_links = []
|
||||
if eingang.paperless_dokument_ids:
|
||||
dokument_links = DokumentLink.objects.filter(
|
||||
paperless_document_id__in=eingang.paperless_dokument_ids
|
||||
)
|
||||
|
||||
# Alle aktiven Destinatäre für manuelle Zuordnung
|
||||
alle_destinataere = Destinataer.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
||||
|
||||
context = {
|
||||
"title": f"E-Mail-Eingang: {eingang}",
|
||||
"eingang": eingang,
|
||||
"paperless_links": paperless_links,
|
||||
"dokument_links": dokument_links,
|
||||
"alle_destinataere": alle_destinataere,
|
||||
}
|
||||
return render(request, "stiftung/email_eingang/detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def email_eingang_poll_trigger(request):
|
||||
"""Löst den IMAP-Poll-Task manuell aus (für Tests und manuelle Verarbeitung)."""
|
||||
if request.method == "POST":
|
||||
from stiftung.tasks import poll_destinataer_emails
|
||||
try:
|
||||
task = poll_destinataer_emails.delay()
|
||||
messages.success(
|
||||
request,
|
||||
f"E-Mail-Abruf wurde gestartet (Task-ID: {task.id}). "
|
||||
"Bitte Seite in ca. 30 Sekunden neu laden.",
|
||||
)
|
||||
except Exception as exc:
|
||||
messages.error(request, f"Fehler beim Starten des Tasks: {exc}")
|
||||
return redirect("email_eingang_list")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Veranstaltungsmodul
|
||||
# ============================================================
|
||||
|
||||
1553
app/stiftung/views/land.py
Normal file
1553
app/stiftung/views/land.py
Normal file
File diff suppressed because it is too large
Load Diff
2139
app/stiftung/views/system.py
Normal file
2139
app/stiftung/views/system.py
Normal file
File diff suppressed because it is too large
Load Diff
1495
app/stiftung/views/unterstuetzungen.py
Normal file
1495
app/stiftung/views/unterstuetzungen.py
Normal file
File diff suppressed because it is too large
Load Diff
254
app/stiftung/views/veranstaltung.py
Normal file
254
app/stiftung/views/veranstaltung.py
Normal file
@@ -0,0 +1,254 @@
|
||||
# views/veranstaltung.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_list(request):
|
||||
"""Liste aller Veranstaltungen"""
|
||||
veranstaltungen = Veranstaltung.objects.all()
|
||||
return render(request, "stiftung/veranstaltung/list.html", {"veranstaltungen": veranstaltungen})
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_detail(request, pk):
|
||||
"""Detail-Ansicht einer Veranstaltung mit RSVP-Übersicht"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
teilnehmer = veranstaltung.teilnehmer.all()
|
||||
context = {
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
"zugesagte": teilnehmer.filter(rsvp_status="zugesagt"),
|
||||
"abgesagte": teilnehmer.filter(rsvp_status="abgesagt"),
|
||||
"keine_rueckmeldung": teilnehmer.filter(rsvp_status="keine_rueckmeldung"),
|
||||
"eingeladen": teilnehmer.filter(rsvp_status="eingeladen"),
|
||||
}
|
||||
return render(request, "stiftung/veranstaltung/detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_serienbrief_pdf(request, pk):
|
||||
"""Generiert Serienbrief-PDF für alle Teilnehmer einer Veranstaltung"""
|
||||
from weasyprint import HTML
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
teilnehmer = veranstaltung.teilnehmer.all().order_by("nachname", "vorname")
|
||||
|
||||
# Render HTML for all letters
|
||||
html_string = render_to_string(
|
||||
"stiftung/veranstaltung/serienbrief_pdf.html",
|
||||
{
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
},
|
||||
)
|
||||
pdf = HTML(string=html_string).write_pdf()
|
||||
filename = f"einladungen_{veranstaltung.datum}_{veranstaltung.titel[:30].replace(' ', '_')}.pdf"
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_serienbrief_vorschau(request, pk):
|
||||
"""HTML-Vorschau des Serienbriefs im Browser (kein PDF-Download)"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
teilnehmer = veranstaltung.teilnehmer.all().order_by("nachname", "vorname")
|
||||
return render(
|
||||
request,
|
||||
"stiftung/veranstaltung/serienbrief_vorschau.html",
|
||||
{
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_create(request):
|
||||
"""Neue Veranstaltung erstellen"""
|
||||
from stiftung.forms import VeranstaltungForm
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungForm(request.POST)
|
||||
if form.is_valid():
|
||||
veranstaltung = form.save()
|
||||
messages.success(request, f'Veranstaltung "{veranstaltung.titel}" wurde erstellt.')
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungForm()
|
||||
|
||||
return render(request, "stiftung/veranstaltung/form.html", {
|
||||
"form": form,
|
||||
"title": "Neue Veranstaltung erstellen",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_update(request, pk):
|
||||
"""Veranstaltung bearbeiten (inkl. Serienbrief-Felder)"""
|
||||
from stiftung.forms import VeranstaltungForm
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungForm(request.POST, instance=veranstaltung)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, f'Veranstaltung "{veranstaltung.titel}" wurde aktualisiert.')
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungForm(instance=veranstaltung)
|
||||
|
||||
return render(request, "stiftung/veranstaltung/form.html", {
|
||||
"form": form,
|
||||
"veranstaltung": veranstaltung,
|
||||
"title": f"Veranstaltung bearbeiten: {veranstaltung.titel}",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_delete(request, pk):
|
||||
"""Veranstaltung löschen"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
titel = veranstaltung.titel
|
||||
veranstaltung.delete()
|
||||
messages.success(request, f'Veranstaltung "{titel}" wurde gelöscht.')
|
||||
return redirect("stiftung:veranstaltung_list")
|
||||
|
||||
return render(request, "stiftung/veranstaltung/delete.html", {
|
||||
"veranstaltung": veranstaltung,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def teilnehmer_create(request, veranstaltung_pk):
|
||||
"""Teilnehmer zu einer Veranstaltung hinzufügen"""
|
||||
from stiftung.forms import VeranstaltungsteilnehmerForm
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungsteilnehmerForm(request.POST)
|
||||
if form.is_valid():
|
||||
teilnehmer = form.save(commit=False)
|
||||
teilnehmer.veranstaltung = veranstaltung
|
||||
teilnehmer.save()
|
||||
messages.success(request, f"{teilnehmer.vorname} {teilnehmer.nachname} wurde hinzugefügt.")
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungsteilnehmerForm()
|
||||
|
||||
return render(request, "stiftung/veranstaltung/teilnehmer_form.html", {
|
||||
"form": form,
|
||||
"veranstaltung": veranstaltung,
|
||||
"title": "Teilnehmer hinzufügen",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def teilnehmer_update(request, veranstaltung_pk, pk):
|
||||
"""Teilnehmer bearbeiten"""
|
||||
from stiftung.forms import VeranstaltungsteilnehmerForm
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||||
teilnehmer = get_object_or_404(Veranstaltungsteilnehmer, pk=pk, veranstaltung=veranstaltung)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungsteilnehmerForm(request.POST, instance=teilnehmer)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, f"{teilnehmer.vorname} {teilnehmer.nachname} wurde aktualisiert.")
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungsteilnehmerForm(instance=teilnehmer)
|
||||
|
||||
return render(request, "stiftung/veranstaltung/teilnehmer_form.html", {
|
||||
"form": form,
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
"title": f"Teilnehmer bearbeiten: {teilnehmer.vorname} {teilnehmer.nachname}",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def teilnehmer_delete(request, veranstaltung_pk, pk):
|
||||
"""Teilnehmer aus Veranstaltung entfernen"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||||
teilnehmer = get_object_or_404(Veranstaltungsteilnehmer, pk=pk, veranstaltung=veranstaltung)
|
||||
|
||||
if request.method == "POST":
|
||||
name = f"{teilnehmer.vorname} {teilnehmer.nachname}"
|
||||
teilnehmer.delete()
|
||||
messages.success(request, f"{name} wurde aus der Teilnehmerliste entfernt.")
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
|
||||
return render(request, "stiftung/veranstaltung/teilnehmer_delete.html", {
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
})
|
||||
Reference in New Issue
Block a user