Files
stiftung-management-system/app/stiftung/views/finanzen.py
SysAdmin Agent faeb7c1073
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled
Implement modular report system with 6 report types and composer UI
Refactors the Berichte section from a single hardcoded Jahresbericht into
a modular report-building system. Jahresbericht now uses PDFGenerator for
corporate identity (logo, colors, headers/footers, cover page). 8 reusable
section templates can be freely combined. 6 predefined report templates
(Jahres-, Destinatär-, Grundstücks-, Finanz-, Förder-, Pachtbericht) with
HTML preview and PDF export. New Bericht-Baukasten UI lets users compose
custom reports from individual sections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 20:55:31 +00:00

1091 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

# 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 with modular report builder"""
# 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
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,
"bericht_vorlagen": BERICHT_VORLAGEN,
"bericht_sektionen": BERICHT_SEKTIONEN,
}
return render(request, "stiftung/bericht_list.html", context)
def _get_corporate_context():
"""Holt Corporate-Identity-Einstellungen und CSS für Berichte."""
from stiftung.utils.pdf_generator import pdf_generator
corporate_settings = pdf_generator.get_corporate_settings()
logo_base64 = pdf_generator.get_logo_base64(corporate_settings.get("logo_path", ""))
css_content = pdf_generator.get_base_css(corporate_settings)
return {
"corporate_settings": corporate_settings,
"logo_base64": logo_base64,
"css_content": css_content,
}
def _jahresbericht_context(jahr):
"""Phase 4: Aggregiert alle Daten für den Jahresbericht."""
from stiftung.models import (
DestinataerUnterstuetzung, LandAbrechnung, Verwaltungskosten,
)
# Förderungen (legacy)
foerderungen = Foerderung.objects.filter(jahr=jahr).select_related("destinataer", "person")
total_foerderungen_legacy = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
# Unterstützungen (Phase 2 neue Pipeline)
unterstuetzungen = DestinataerUnterstuetzung.objects.filter(
faellig_am__year=jahr
).exclude(status="storniert").select_related("destinataer", "konto")
unterstuetzungen_ausgezahlt = unterstuetzungen.filter(
status__in=["ausgezahlt", "abgeschlossen"]
)
total_unterstuetzungen = unterstuetzungen_ausgezahlt.aggregate(total=Sum("betrag"))["total"] or 0
# Gesamtausgaben Förderung
total_ausgaben_foerderung = total_foerderungen_legacy + total_unterstuetzungen
# Verpachtungen
verpachtungen = LandVerpachtung.objects.filter(
pachtbeginn__year__lte=jahr
).filter(
Q(pachtende__isnull=True) | Q(pachtende__year__gte=jahr)
).select_related("land", "paechter")
total_pachtzins = verpachtungen.aggregate(total=Sum("pachtzins_pauschal"))["total"] or 0
# Landabrechnungen für das Jahr
landabrechnungen = LandAbrechnung.objects.filter(
abrechnungsjahr=jahr
).select_related("land")
pacht_vereinnahmt = landabrechnungen.aggregate(total=Sum("pacht_vereinnahmt"))["total"] or 0
grundsteuer_gesamt = landabrechnungen.aggregate(total=Sum("grundsteuer_betrag"))["total"] or 0
# Verwaltungskosten
verwaltungskosten_qs = Verwaltungskosten.objects.filter(datum__year=jahr)
total_verwaltungskosten = verwaltungskosten_qs.aggregate(total=Sum("betrag"))["total"] or 0
verwaltungskosten_nach_kategorie = (
verwaltungskosten_qs
.values("kategorie")
.annotate(summe=Sum("betrag"), anzahl=Count("id"))
.order_by("-summe")
)
# Gesamtbilanz
total_einnahmen = pacht_vereinnahmt if pacht_vereinnahmt else total_pachtzins
total_ausgaben = total_ausgaben_foerderung + total_verwaltungskosten
netto = total_einnahmen - total_ausgaben
context = {
"jahr": jahr,
"title": f"Jahresbericht {jahr}",
"foerderungen": foerderungen,
"total_foerderungen_legacy": total_foerderungen_legacy,
"unterstuetzungen": unterstuetzungen,
"unterstuetzungen_ausgezahlt": unterstuetzungen_ausgezahlt,
"total_unterstuetzungen": total_unterstuetzungen,
"total_ausgaben_foerderung": total_ausgaben_foerderung,
"verpachtungen": verpachtungen,
"total_pachtzins": total_pachtzins,
"landabrechnungen": landabrechnungen,
"pacht_vereinnahmt": pacht_vereinnahmt,
"grundsteuer_gesamt": grundsteuer_gesamt,
"verwaltungskosten_nach_kategorie": verwaltungskosten_nach_kategorie,
"total_verwaltungskosten": total_verwaltungskosten,
"total_einnahmen": total_einnahmen,
"total_ausgaben": total_ausgaben,
"netto": netto,
"total_foerderungen": total_ausgaben_foerderung,
"show_cover": True,
"bericht_titel": f"Jahresbericht {jahr}",
"bericht_untertitel": "Gesamtübersicht des Geschäftsjahres",
"berichtszeitraum": str(jahr),
}
context.update(_get_corporate_context())
return context
@login_required
def jahresbericht_generate(request, jahr):
"""Phase 4: Jahresbericht mit aggregierten Finanzdaten."""
context = _jahresbericht_context(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):
"""Phase 4: PDF-Export des Jahresberichts via PDFGenerator."""
from django.template.loader import render_to_string
from stiftung.utils.pdf_generator import pdf_generator
context = _jahresbericht_context(jahr)
html_string = render_to_string("stiftung/jahresbericht.html", context)
return pdf_generator.generate_pdf_response(
html_string, f"jahresbericht_{jahr}.pdf", context.get("css_content")
)
# =============================================================================
# MODULARE BERICHTE Berichts-Baukasten
# =============================================================================
# Verfügbare Sektionen mit Metadaten
BERICHT_SEKTIONEN = {
"bilanz": {"label": "Jahresbilanz", "icon": "fa-balance-scale", "needs_jahr": True},
"unterstuetzungen": {"label": "Unterstützungszahlungen", "icon": "fa-hand-holding-heart", "needs_jahr": True},
"foerderungen": {"label": "Förderungen", "icon": "fa-gift", "needs_jahr": True},
"grundstuecke": {"label": "Grundstücksverwaltung", "icon": "fa-map", "needs_jahr": True},
"verwaltungskosten": {"label": "Verwaltungskosten", "icon": "fa-file-invoice-dollar", "needs_jahr": True},
"destinataere_uebersicht": {"label": "Destinatär-Übersicht", "icon": "fa-users", "needs_jahr": False},
"konten_uebersicht": {"label": "Kontenübersicht", "icon": "fa-university", "needs_jahr": False},
"verpachtungen": {"label": "Pachtbericht", "icon": "fa-handshake", "needs_jahr": False},
}
# Vordefinierte Berichtstypen
BERICHT_VORLAGEN = {
"jahresbericht": {
"label": "Jahresbericht",
"beschreibung": "Vollständige Übersicht eines Geschäftsjahres",
"sektionen": ["bilanz", "unterstuetzungen", "foerderungen", "grundstuecke", "verwaltungskosten"],
"needs_jahr": True,
"icon": "fa-calendar-alt",
},
"destinataerbericht": {
"label": "Destinatärbericht",
"beschreibung": "Übersicht aller Destinatäre mit Förderstatus",
"sektionen": ["destinataere_uebersicht", "unterstuetzungen", "foerderungen"],
"needs_jahr": True,
"icon": "fa-users",
},
"grundstuecksbericht": {
"label": "Grundstücksbericht",
"beschreibung": "Liegenschaftsübersicht mit Pachtverträgen",
"sektionen": ["grundstuecke", "verpachtungen"],
"needs_jahr": True,
"icon": "fa-map",
},
"finanzbericht": {
"label": "Finanzbericht",
"beschreibung": "Einnahmen/Ausgaben und Kontenübersicht",
"sektionen": ["bilanz", "konten_uebersicht", "verwaltungskosten"],
"needs_jahr": True,
"icon": "fa-euro-sign",
},
"foerderbericht": {
"label": "Förderbericht",
"beschreibung": "Detailansicht aller Förderungen",
"sektionen": ["foerderungen", "unterstuetzungen"],
"needs_jahr": True,
"icon": "fa-gift",
},
"pachtbericht": {
"label": "Pachtbericht",
"beschreibung": "Pachtzinseinnahmen und Vertragsübersicht",
"sektionen": ["verpachtungen", "grundstuecke"],
"needs_jahr": True,
"icon": "fa-handshake",
},
}
def _build_section_context(sektionen, jahr=None):
"""Baut den Context für die gewählten Sektionen zusammen."""
from stiftung.models import (
DestinataerUnterstuetzung, LandAbrechnung, Verwaltungskosten,
)
context = {}
if jahr:
context["jahr"] = jahr
needs_jahresbericht = any(s in sektionen for s in [
"bilanz", "unterstuetzungen", "foerderungen", "grundstuecke", "verwaltungskosten"
])
if needs_jahresbericht and jahr:
jb = _jahresbericht_context(jahr)
context.update(jb)
if "destinataere_uebersicht" in sektionen:
from django.db.models import Count, Sum as DSum
qs = Destinataer.objects.all()
context["destinataere_aktiv"] = qs.filter(aktiv=True).count()
context["destinataere_gesamt"] = qs.count()
# Annotate with support stats
if jahr:
dest_qs = qs.annotate(
unterstuetzung_count=Count(
"unterstuetzungen",
filter=Q(unterstuetzungen__faellig_am__year=jahr),
),
unterstuetzung_summe=Coalesce(
DSum(
"unterstuetzungen__betrag",
filter=Q(
unterstuetzungen__faellig_am__year=jahr,
unterstuetzungen__status__in=["ausgezahlt", "abgeschlossen"],
),
),
Decimal("0"),
),
)
else:
dest_qs = qs.annotate(
unterstuetzung_count=Count("unterstuetzungen"),
unterstuetzung_summe=Coalesce(
DSum(
"unterstuetzungen__betrag",
filter=Q(unterstuetzungen__status__in=["ausgezahlt", "abgeschlossen"]),
),
Decimal("0"),
),
)
context["destinataere_liste"] = dest_qs.order_by("nachname", "vorname")
context["destinataere_total_unterstuetzung"] = (
dest_qs.aggregate(total=DSum("unterstuetzung_summe"))["total"] or 0
)
if "konten_uebersicht" in sektionen:
konten = StiftungsKonto.objects.filter(aktiv=True).order_by("bank_name", "kontoname")
context["konten_liste"] = konten
context["konten_anzahl"] = konten.count()
context["konten_gesamtsaldo"] = konten.aggregate(total=Sum("saldo"))["total"] or 0
if "verpachtungen" in sektionen:
from datetime import timedelta
heute = date.today()
in_12_monaten = heute + timedelta(days=365)
aktive = LandVerpachtung.objects.filter(
status="aktiv"
).select_related("land", "paechter")
auslaufend = aktive.filter(
pachtende__isnull=False,
pachtende__lte=in_12_monaten,
pachtende__gte=heute,
).order_by("pachtende")
total_flaeche = aktive.aggregate(total=Sum("verpachtete_flaeche"))["total"] or 0
total_pz = aktive.aggregate(total=Sum("pachtzins_pauschal"))["total"] or 0
context["pacht_statistik"] = {
"aktive_vertraege": aktive.count(),
"total_pachtzins": total_pz,
"total_flaeche": total_flaeche,
"auslaufend_12m": auslaufend.count(),
}
context["pacht_auslaufend"] = auslaufend
# Also provide full list if not already from jahresbericht
if "verpachtungen" not in context or not context.get("verpachtungen"):
if jahr:
context["verpachtungen"] = LandVerpachtung.objects.filter(
pachtbeginn__year__lte=jahr
).filter(
Q(pachtende__isnull=True) | Q(pachtende__year__gte=jahr)
).select_related("land", "paechter")
else:
context["verpachtungen"] = aktive
return context
@login_required
def bericht_zusammenstellen(request):
"""Modularer Bericht: Sektionen auswählen und zusammenstellen."""
if request.method == "POST":
sektionen = request.POST.getlist("sektionen")
jahr_str = request.POST.get("jahr", "")
show_cover = request.POST.get("show_cover") == "on"
output_format = request.POST.get("format", "html")
vorlage = request.POST.get("vorlage", "")
# Vorlage anwenden falls gewählt
if vorlage and vorlage in BERICHT_VORLAGEN and not sektionen:
sektionen = BERICHT_VORLAGEN[vorlage]["sektionen"]
if not sektionen:
messages.error(request, "Bitte wählen Sie mindestens eine Sektion aus.")
return redirect("stiftung:bericht_list")
jahr = int(jahr_str) if jahr_str and jahr_str.isdigit() else None
# Build context
context = _build_section_context(sektionen, jahr)
context["sektionen"] = sektionen
context["show_cover"] = show_cover
# Set titles
if vorlage and vorlage in BERICHT_VORLAGEN:
titel = BERICHT_VORLAGEN[vorlage]["label"]
else:
titel = "Bericht"
if jahr:
context["bericht_titel"] = f"{titel} {jahr}"
context["berichtszeitraum"] = str(jahr)
else:
context["bericht_titel"] = titel
context["berichtszeitraum"] = "Aktuell"
context["bericht_untertitel"] = BERICHT_VORLAGEN.get(vorlage, {}).get("beschreibung", "")
# Add corporate context if not already present
if "corporate_settings" not in context:
context.update(_get_corporate_context())
if output_format == "pdf":
from django.template.loader import render_to_string
from stiftung.utils.pdf_generator import pdf_generator
html_string = render_to_string("berichte/bericht_modular.html", context)
filename = f"bericht_{vorlage or 'custom'}_{jahr or 'aktuell'}.pdf"
return pdf_generator.generate_pdf_response(
html_string, filename, context.get("css_content")
)
else:
return render(request, "berichte/bericht_modular.html", context)
# GET: Redirect to bericht_list
return redirect("stiftung:bericht_list")
@login_required
def bericht_vorlage(request, vorlage_key):
"""Schnellzugriff: Vordefinierte Berichtsvorlage generieren."""
if vorlage_key not in BERICHT_VORLAGEN:
messages.error(request, f"Unbekannter Berichtstyp: {vorlage_key}")
return redirect("stiftung:bericht_list")
vorlage = BERICHT_VORLAGEN[vorlage_key]
jahr_str = request.GET.get("jahr", "")
jahr = int(jahr_str) if jahr_str and jahr_str.isdigit() else None
output_format = request.GET.get("format", "html")
if vorlage["needs_jahr"] and not jahr:
messages.error(request, "Bitte wählen Sie ein Jahr für diesen Bericht.")
return redirect("stiftung:bericht_list")
context = _build_section_context(vorlage["sektionen"], jahr)
context["sektionen"] = vorlage["sektionen"]
context["show_cover"] = True
context["bericht_titel"] = f"{vorlage['label']}" + (f" {jahr}" if jahr else "")
context["bericht_untertitel"] = vorlage["beschreibung"]
context["berichtszeitraum"] = str(jahr) if jahr else "Aktuell"
if "corporate_settings" not in context:
context.update(_get_corporate_context())
if output_format == "pdf":
from django.template.loader import render_to_string
from stiftung.utils.pdf_generator import pdf_generator
html_string = render_to_string("berichte/bericht_modular.html", context)
filename = f"{vorlage_key}_{jahr or 'aktuell'}.pdf"
return pdf_generator.generate_pdf_response(
html_string, filename, context.get("css_content")
)
else:
return render(request, "berichte/bericht_modular.html", context)
# 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 verwaltungskosten_detail(request, pk):
"""Detailansicht einer Verwaltungskosten-Position mit verknüpften Dokumenten und E-Mails."""
from stiftung.models import DokumentDatei, EmailEingang, Verwaltungskosten
vk = get_object_or_404(Verwaltungskosten, pk=pk)
if request.method == "POST":
action = request.POST.get("action")
if action == "set_status":
new_status = request.POST.get("status", "")
if new_status in dict(Verwaltungskosten.STATUS_CHOICES):
vk.status = new_status
vk.save()
messages.success(request, f"Status auf '{vk.get_status_display()}' gesetzt.")
return redirect("stiftung:verwaltungskosten_detail", pk=pk)
# Verknüpfte DMS-Dokumente
dms_dokumente = DokumentDatei.objects.filter(verwaltungskosten=vk).order_by("erstellt_am")
# Verknüpfte E-Mails
email_eingaenge = EmailEingang.objects.filter(verwaltungskosten=vk).order_by("-eingangsdatum")
context = {
"vk": vk,
"dms_dokumente": dms_dokumente,
"email_eingaenge": email_eingaenge,
"status_choices": Verwaltungskosten.STATUS_CHOICES,
}
return render(request, "stiftung/verwaltungskosten_detail.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 DokumentDatei, 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_detail", pk=pk)
else:
form = VerwaltungskostenForm(instance=verwaltungskosten)
# Verknüpfte DMS-Dokumente
dms_dokumente = DokumentDatei.objects.filter(verwaltungskosten=verwaltungskosten).order_by("erstellt_am")
context = {
"form": form,
"verwaltungskosten": verwaltungskosten,
"dms_dokumente": dms_dokumente,
"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
# =============================================================================