Files
stiftung-management-system/app/stiftung/views/finanzen.py
SysAdmin Agent 2be72c3990 Phase 4: SEPA-Validierung (schwifty), Globale Suche (Cmd+K) & Jahresbericht-Modul
- SEPA-Export: IBAN/BIC-Validierung via schwifty, Schuldner-Konto aus StiftungsKonto
- Globale Suche: Cmd+K Modal über Destinatäre, Pächter, Ländereien, Förderungen, Dokumente
- Jahresbericht: Vollständige Jahresbilanz mit Einnahmen/Ausgaben/Netto, Unterstützungen,
  Landabrechnungen, Verwaltungskosten nach Kategorie, PDF-Export

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 12:57:36 +00:00

783 lines
26 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"""
# 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)
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
return {
"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,
# Rückwärtskompatibilität
"total_foerderungen": total_ausgaben_foerderung,
}
@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."""
from django.http import HttpResponse
from django.template.loader import render_to_string
from weasyprint import HTML
context = _jahresbericht_context(jahr)
# 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
# =============================================================================