# 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 # =============================================================================