""" Lese-Tools für den MCP Server der Stiftungsverwaltung. Alle Tools: - Prüfen die Rolle (readonly/editor/admin erforderlich) - Wenden PII-Maskierung an (außer bei admin) - Schreiben Audit-Log-Einträge """ from __future__ import annotations from django.db.models import Q, Sum from mcp_server.audit import log_mcp_read from mcp_server.auth import require_role from mcp_server.privacy import apply_privacy_filter, apply_privacy_filter_list from mcp_server.tools.helpers import format_result, model_to_dict def _get_role() -> str: from mcp_server.auth import get_current_role, require_role as _require role = get_current_role() _require(role) return role # ────────────────────────────────────────────────────────────────────────────── # Destinatäre # ────────────────────────────────────────────────────────────────────────────── def destinataer_suchen( suchbegriff: str = "", aktiv: bool | None = None, familienzweig: str = "", limit: int = 20, ) -> str: """ Sucht Destinatäre nach Name, Familienzweig oder Aktivstatus. Args: suchbegriff: Freitext (Vor-/Nachname, Institution) aktiv: True=nur Aktive, False=nur Inaktive, None=alle familienzweig: Filtert nach Familienzweig (hauptzweig/nebenzweig/verwandt/anderer) limit: Maximale Anzahl Ergebnisse (max. 100) """ from stiftung.models import Destinataer role = _get_role() limit = min(limit, 100) qs = Destinataer.objects.all() if suchbegriff: qs = qs.filter( Q(vorname__icontains=suchbegriff) | Q(nachname__icontains=suchbegriff) | Q(institution__icontains=suchbegriff) ) if aktiv is not None: qs = qs.filter(aktiv=aktiv) if familienzweig: qs = qs.filter(familienzweig=familienzweig) qs = qs.order_by("nachname", "vorname")[:limit] # Reduzierte Felder für Listen-Ausgabe results = [] for obj in qs: item = { "id": str(obj.id), "vorname": obj.vorname, "nachname": obj.nachname, "familienzweig": obj.familienzweig, "aktiv": obj.aktiv, "berufsgruppe": obj.berufsgruppe, "ort": obj.ort, "email": obj.email, } results.append(apply_privacy_filter(item, "destinataer", role)) log_mcp_read(role, "destinataer", "Destinatär-Suche", f"Suche: '{suchbegriff}', {len(results)} Ergebnisse") return format_result({"anzahl": len(results), "destinataere": results}) def destinataer_details(destinataer_id: str) -> str: """ Gibt vollständige Details eines Destinatärs zurück. Args: destinataer_id: UUID des Destinatärs """ from stiftung.models import Destinataer, DestinataerUnterstuetzung, Foerderung role = _get_role() try: obj = Destinataer.objects.get(id=destinataer_id) except Destinataer.DoesNotExist: return format_result({"fehler": f"Destinatär {destinataer_id} nicht gefunden"}) data = model_to_dict(obj) data = apply_privacy_filter(data, "destinataer", role) # Aktuelle Unterstützungen unterstuetzungen = list( DestinataerUnterstuetzung.objects.filter(destinataer=obj) .exclude(status="storniert") .order_by("-faellig_am")[:10] .values("id", "betrag", "faellig_am", "status", "beschreibung") ) for u in unterstuetzungen: for k, v in u.items(): from mcp_server.tools.helpers import serialize_value u[k] = serialize_value(v) # Förderungen foerderungen = list( Foerderung.objects.filter(destinataer=obj) .order_by("-jahr")[:10] .values("id", "jahr", "betrag", "kategorie", "status") ) for f in foerderungen: for k, v in f.items(): from mcp_server.tools.helpers import serialize_value f[k] = serialize_value(v) data["aktuelle_unterstuetzungen"] = unterstuetzungen data["foerderungen"] = foerderungen name = f"{obj.vorname} {obj.nachname}" log_mcp_read(role, "destinataer", name, f"Details abgerufen für {name}") return format_result(data) # ────────────────────────────────────────────────────────────────────────────── # Ländereien # ────────────────────────────────────────────────────────────────────────────── def land_suchen( suchbegriff: str = "", gemeinde: str = "", limit: int = 20, ) -> str: """ Sucht Ländereien nach Bezeichnung, Gemarkung oder Gemeinde. Args: suchbegriff: Freitext (Bezeichnung, Gemarkung) gemeinde: Filtert nach Gemeinde limit: Maximale Anzahl Ergebnisse (max. 100) """ from stiftung.models import Land role = _get_role() limit = min(limit, 100) qs = Land.objects.all() if suchbegriff: qs = qs.filter( Q(gemeinde__icontains=suchbegriff) | Q(gemarkung__icontains=suchbegriff) | Q(flur__icontains=suchbegriff) | Q(lfd_nr__icontains=suchbegriff) | Q(adresse__icontains=suchbegriff) ) if gemeinde: qs = qs.filter(gemeinde__icontains=gemeinde) qs = qs.order_by("gemeinde", "gemarkung")[:limit] results = [] for obj in qs: results.append({ "id": str(obj.id), "bezeichnung": str(obj), "lfd_nr": obj.lfd_nr, "gemeinde": obj.gemeinde, "gemarkung": obj.gemarkung, "flur": obj.flur, "flurstueck": obj.flurstueck, "groesse_qm": float(obj.groesse_qm) if obj.groesse_qm else None, "aktiv_verpachtet": obj.neue_verpachtungen.filter(status="aktiv").exists(), }) log_mcp_read(role, "land", "Länderei-Suche", f"Suche: '{suchbegriff}', {len(results)} Ergebnisse") return format_result({"anzahl": len(results), "laendereien": results}) def land_details(land_id: str) -> str: """ Gibt vollständige Details einer Länderei zurück. Args: land_id: UUID der Länderei """ from stiftung.models import Land, LandVerpachtung role = _get_role() try: obj = Land.objects.get(id=land_id) except Land.DoesNotExist: return format_result({"fehler": f"Länderei {land_id} nicht gefunden"}) data = model_to_dict(obj) # Aktive Verpachtungen verpachtungen = [] for v in obj.neue_verpachtungen.all().order_by("-pachtbeginn")[:5]: verpachtungen.append({ "id": str(v.id), "paechter": str(v.paechter) if v.paechter else None, "pachtbeginn": v.pachtbeginn.isoformat() if v.pachtbeginn else None, "pachtende": v.pachtende.isoformat() if v.pachtende else None, "pachtzins_pauschal": float(v.pachtzins_pauschal) if v.pachtzins_pauschal else None, "status": v.status, }) data["verpachtungen"] = verpachtungen log_mcp_read(role, "land", str(obj), f"Land-Details abgerufen: {obj}") return format_result(data) # ────────────────────────────────────────────────────────────────────────────── # Pächter # ────────────────────────────────────────────────────────────────────────────── def paechter_suchen( suchbegriff: str = "", limit: int = 20, ) -> str: """ Sucht Pächter nach Name. Args: suchbegriff: Freitext (Vor-/Nachname) limit: Maximale Anzahl Ergebnisse (max. 100) """ from stiftung.models import Paechter role = _get_role() limit = min(limit, 100) qs = Paechter.objects.all() if suchbegriff: qs = qs.filter( Q(vorname__icontains=suchbegriff) | Q(nachname__icontains=suchbegriff) ) qs = qs.order_by("nachname", "vorname")[:limit] results = [] for obj in qs: item = { "id": str(obj.id), "vorname": obj.vorname, "nachname": obj.nachname, "personentyp": obj.personentyp, "ort": obj.ort, "email": obj.email, "telefon": obj.telefon, "aktive_verpachtungen": obj.neue_verpachtungen.filter(status="aktiv").count() if hasattr(obj, "neue_verpachtungen") else 0, } results.append(apply_privacy_filter(item, "paechter", role)) log_mcp_read(role, "paechter", "Pächter-Suche", f"Suche: '{suchbegriff}', {len(results)} Ergebnisse") return format_result({"anzahl": len(results), "paechter": results}) # ────────────────────────────────────────────────────────────────────────────── # Konten # ────────────────────────────────────────────────────────────────────────────── def konten_uebersicht() -> str: """ Gibt eine Übersicht aller Stiftungskonten mit aktuellem Saldo zurück. """ from stiftung.models import StiftungsKonto role = _get_role() konten = [] gesamt_saldo = 0.0 for konto in StiftungsKonto.objects.filter(aktiv=True).order_by("bank_name"): saldo = float(konto.saldo) if konto.saldo else 0.0 gesamt_saldo += saldo konten.append({ "id": str(konto.id), "kontoname": konto.kontoname, "bank_name": konto.bank_name, "konto_typ": konto.konto_typ, "saldo": saldo, "saldo_datum": konto.saldo_datum.isoformat() if konto.saldo_datum else None, "zinssatz": float(konto.zinssatz) if konto.zinssatz else None, # IBAN nur für Admin "iban": konto.iban if role == "admin" else "****" + konto.iban[-4:] if konto.iban and len(konto.iban) > 4 else "****", }) log_mcp_read(role, "stiftungskonto", "Kontenübersicht", f"{len(konten)} Konten abgerufen") return format_result({ "konten": konten, "gesamt_saldo": round(gesamt_saldo, 2), "anzahl_konten": len(konten), }) # ────────────────────────────────────────────────────────────────────────────── # Verwaltungskosten # ────────────────────────────────────────────────────────────────────────────── def verwaltungskosten( jahr: int | None = None, kategorie: str = "", status: str = "", limit: int = 50, ) -> str: """ Listet Verwaltungskosten auf. Args: jahr: Filtert nach Jahr (z.B. 2024) kategorie: Filtert nach Kategorie (rechnung_intern, bueroausstattung, ...) status: Filtert nach Status (geplant, bezahlt, ...) limit: Maximale Anzahl (max. 200) """ from stiftung.models import Verwaltungskosten role = _get_role() limit = min(limit, 200) qs = Verwaltungskosten.objects.all() if jahr: qs = qs.filter(datum__year=jahr) if kategorie: qs = qs.filter(kategorie=kategorie) if status: qs = qs.filter(status=status) qs = qs.order_by("-datum")[:limit] results = [] gesamt = 0.0 for obj in qs: betrag = float(obj.betrag) if obj.betrag else 0.0 gesamt += betrag results.append({ "id": str(obj.id), "bezeichnung": obj.bezeichnung, "kategorie": obj.kategorie, "betrag": betrag, "datum": obj.datum.isoformat(), "lieferant_firma": obj.lieferant_firma, "status": obj.status, "rechnungsnummer": obj.rechnungsnummer, }) log_mcp_read(role, "verwaltungskosten", "Verwaltungskosten", f"{len(results)} Einträge") return format_result({ "anzahl": len(results), "gesamt_betrag": round(gesamt, 2), "verwaltungskosten": results, }) # ────────────────────────────────────────────────────────────────────────────── # Transaktionen # ────────────────────────────────────────────────────────────────────────────── def transaktionen_suchen( suchbegriff: str = "", konto_id: str = "", von_datum: str = "", bis_datum: str = "", transaction_type: str = "", limit: int = 50, ) -> str: """ Sucht Banktransaktionen. Args: suchbegriff: Freitext in Verwendungszweck oder Empfänger konto_id: UUID des Kontos (optional) von_datum: Startdatum YYYY-MM-DD (optional) bis_datum: Enddatum YYYY-MM-DD (optional) transaction_type: eingang/ausgang/dauerauftrag/... (optional) limit: Maximale Anzahl (max. 200) """ from stiftung.models import BankTransaction role = _get_role() limit = min(limit, 200) qs = BankTransaction.objects.select_related("konto").all() if suchbegriff: qs = qs.filter( Q(verwendungszweck__icontains=suchbegriff) | Q(empfaenger_zahlungspflichtiger__icontains=suchbegriff) ) if konto_id: qs = qs.filter(konto_id=konto_id) if von_datum: qs = qs.filter(datum__gte=von_datum) if bis_datum: qs = qs.filter(datum__lte=bis_datum) if transaction_type: qs = qs.filter(transaction_type=transaction_type) qs = qs.order_by("-datum")[:limit] results = [] for obj in qs: results.append({ "id": str(obj.id), "datum": obj.datum.isoformat(), "betrag": float(obj.betrag), "waehrung": obj.waehrung, "verwendungszweck": obj.verwendungszweck[:200], "empfaenger_zahlungspflichtiger": obj.empfaenger_zahlungspflichtiger, "transaction_type": obj.transaction_type, "status": obj.status, "konto": obj.konto.kontoname if obj.konto else None, }) log_mcp_read(role, "banktransaction", "Transaktionssuche", f"Suche: '{suchbegriff}', {len(results)} Ergebnisse") return format_result({"anzahl": len(results), "transaktionen": results}) # ────────────────────────────────────────────────────────────────────────────── # Dokumente # ────────────────────────────────────────────────────────────────────────────── def dokument_suchen( suchbegriff: str = "", kontext: str = "", limit: int = 30, ) -> str: """ Sucht Dokumente im DMS nach Titel, Beschreibung oder Kontext. Args: suchbegriff: Freitext (Titel, Beschreibung, Volltext) kontext: Dokumententyp (pachtvertrag, antrag, rechnung, ...) limit: Maximale Anzahl (max. 100) """ from stiftung.models import DokumentDatei role = _get_role() limit = min(limit, 100) qs = DokumentDatei.objects.all() if suchbegriff: qs = qs.filter( Q(titel__icontains=suchbegriff) | Q(beschreibung__icontains=suchbegriff) | Q(inhaltstext__icontains=suchbegriff) ) if kontext: qs = qs.filter(kontext=kontext) qs = qs.order_by("-erstellt_am")[:limit] results = [] for obj in qs: results.append({ "id": str(obj.id), "titel": obj.titel, "kontext": obj.kontext, "beschreibung": obj.beschreibung[:200] if obj.beschreibung else "", "dateityp": obj.dateityp, "dateigroesse": obj.dateigroesse, "dateiname_original": obj.dateiname_original, }) log_mcp_read(role, "dokumentlink", "Dokumentsuche", f"Suche: '{suchbegriff}', {len(results)} Ergebnisse") return format_result({"anzahl": len(results), "dokumente": results}) def dokument_details(dokument_id: str) -> str: """ Gibt Details eines Dokuments zurück (ohne Dateiinhalt). Args: dokument_id: UUID des Dokuments """ from stiftung.models import DokumentDatei role = _get_role() try: obj = DokumentDatei.objects.get(id=dokument_id) except DokumentDatei.DoesNotExist: return format_result({"fehler": f"Dokument {dokument_id} nicht gefunden"}) data = { "id": str(obj.id), "titel": obj.titel, "kontext": obj.kontext, "beschreibung": obj.beschreibung, "dateityp": obj.dateityp, "dateigroesse": obj.dateigroesse, "dateiname_original": obj.dateiname_original, # Verknüpfungen "land_id": str(obj.land_id) if obj.land_id else None, "paechter_id": str(obj.paechter_id) if obj.paechter_id else None, } # Inhaltstext nur für Nicht-Binary-Dokumente und wenn vorhanden if obj.inhaltstext: data["inhaltsvorschau"] = obj.inhaltstext[:500] log_mcp_read(role, "dokumentlink", obj.titel, f"Dokumentdetails abgerufen: {obj.titel}") return format_result(data) # ────────────────────────────────────────────────────────────────────────────── # Termine # ────────────────────────────────────────────────────────────────────────────── def termine_anzeigen( von_datum: str = "", bis_datum: str = "", kategorie: str = "", prioritaet: str = "", limit: int = 50, ) -> str: """ Zeigt Kalendereinträge und Termine an. Args: von_datum: Startdatum YYYY-MM-DD (optional, Standard: heute) bis_datum: Enddatum YYYY-MM-DD (optional) kategorie: termin/zahlung/deadline/geburtstag/vertrag/pruefung/sonstiges prioritaet: niedrig/normal/hoch/kritisch limit: Maximale Anzahl (max. 200) """ from datetime import date as date_type from stiftung.models import StiftungsKalenderEintrag role = _get_role() limit = min(limit, 200) qs = StiftungsKalenderEintrag.objects.all() if von_datum: qs = qs.filter(datum__gte=von_datum) else: qs = qs.filter(datum__gte=date_type.today()) if bis_datum: qs = qs.filter(datum__lte=bis_datum) if kategorie: qs = qs.filter(kategorie=kategorie) if prioritaet: qs = qs.filter(prioritaet=prioritaet) qs = qs.order_by("datum", "uhrzeit")[:limit] results = [] for obj in qs: results.append({ "id": str(obj.id), "titel": obj.titel, "datum": obj.datum.isoformat(), "uhrzeit": obj.uhrzeit.isoformat() if obj.uhrzeit else None, "ganztags": obj.ganztags, "kategorie": obj.kategorie, "prioritaet": obj.prioritaet, "beschreibung": obj.beschreibung[:300] if obj.beschreibung else "", "destinataer_id": str(obj.destinataer_id) if obj.destinataer_id else None, }) log_mcp_read(role, "system", "Terminübersicht", f"{len(results)} Termine abgerufen") return format_result({"anzahl": len(results), "termine": results}) # ────────────────────────────────────────────────────────────────────────────── # Veranstaltungen # ────────────────────────────────────────────────────────────────────────────── def veranstaltungen_anzeigen( status: str = "", limit: int = 20, ) -> str: """ Zeigt Veranstaltungen der Stiftung an. Args: status: geplant/einladungen_versendet/abgeschlossen/abgesagt (optional, leer = alle) limit: Maximale Anzahl (max. 50) """ from stiftung.models.veranstaltungen import Veranstaltung role = _get_role() limit = min(limit, 50) qs = Veranstaltung.objects.all() if status: qs = qs.filter(status=status) qs = qs.order_by("-datum")[:limit] results = [] for v in qs: results.append({ "id": str(v.id), "titel": v.titel, "datum": v.datum.isoformat(), "uhrzeit": v.uhrzeit.isoformat() if v.uhrzeit else None, "ort": v.ort, "status": v.status, "teilnehmer_gesamt": v.get_teilnehmer_count(), "zugesagt": v.get_zugesagte_count(), "abgesagt": v.get_abgesagte_count(), }) log_mcp_read(role, "veranstaltung", "Veranstaltungsübersicht", f"{len(results)} Veranstaltungen") return format_result({"anzahl": len(results), "veranstaltungen": results}) def veranstaltung_teilnehmer_anzeigen( veranstaltung_id: str, rsvp_status: str = "", ) -> str: """ Zeigt die Teilnehmer einer Veranstaltung an. Args: veranstaltung_id: UUID der Veranstaltung (Pflichtfeld) rsvp_status: eingeladen/zugesagt/abgesagt/keine_rueckmeldung (optional, leer = alle) """ from stiftung.models.veranstaltungen import Veranstaltung role = _get_role() try: veranstaltung = Veranstaltung.objects.get(id=veranstaltung_id) except Veranstaltung.DoesNotExist: return format_result({"fehler": f"Veranstaltung {veranstaltung_id} nicht gefunden"}) qs = veranstaltung.teilnehmer.all() if rsvp_status: qs = qs.filter(rsvp_status=rsvp_status) results = [] for t in qs: results.append({ "id": str(t.id), "anrede": t.anrede, "vorname": t.vorname, "nachname": t.nachname, "strasse": t.strasse, "plz": t.plz, "ort": t.ort, "email": t.email, "rsvp_status": t.rsvp_status, "destinataer_id": str(t.destinataer_id) if t.destinataer_id else None, }) log_mcp_read( role, "veranstaltung", str(veranstaltung.id), f"{len(results)} Teilnehmer von '{veranstaltung.titel}'", ) return format_result({ "veranstaltung": str(veranstaltung), "anzahl": len(results), "teilnehmer": results, }) # ────────────────────────────────────────────────────────────────────────────── # Globale Suche & Dashboard # ────────────────────────────────────────────────────────────────────────────── def globale_suche(suchbegriff: str, limit_pro_typ: int = 5) -> str: """ Sucht über alle Entitätstypen gleichzeitig. Args: suchbegriff: Suchbegriff (mindestens 2 Zeichen) limit_pro_typ: Ergebnisse pro Entitätstyp (max. 20) """ from stiftung.models import ( BankTransaction, Destinataer, Land, Paechter, StiftungsKalenderEintrag, Verwaltungskosten, ) role = _get_role() if len(suchbegriff) < 2: return format_result({"fehler": "Suchbegriff muss mindestens 2 Zeichen lang sein"}) limit_pro_typ = min(limit_pro_typ, 20) ergebnisse = {} # Destinatäre dest = Destinataer.objects.filter( Q(vorname__icontains=suchbegriff) | Q(nachname__icontains=suchbegriff) )[:limit_pro_typ] ergebnisse["destinataere"] = [ {"id": str(d.id), "name": f"{d.vorname} {d.nachname}", "typ": "destinataer"} for d in dest ] # Ländereien laender = Land.objects.filter( Q(bezeichnung__icontains=suchbegriff) | Q(gemarkung__icontains=suchbegriff) )[:limit_pro_typ] ergebnisse["laendereien"] = [ {"id": str(l.id), "name": str(l), "typ": "land"} for l in laender ] # Pächter paechter = Paechter.objects.filter( Q(vorname__icontains=suchbegriff) | Q(nachname__icontains=suchbegriff) )[:limit_pro_typ] ergebnisse["paechter"] = [ {"id": str(p.id), "name": f"{p.vorname} {p.nachname}", "typ": "paechter"} for p in paechter ] # Transaktionen transaktionen = BankTransaction.objects.filter( Q(verwendungszweck__icontains=suchbegriff) | Q(empfaenger_zahlungspflichtiger__icontains=suchbegriff) )[:limit_pro_typ] ergebnisse["transaktionen"] = [ {"id": str(t.id), "verwendungszweck": t.verwendungszweck[:100], "betrag": float(t.betrag), "datum": t.datum.isoformat(), "typ": "transaktion"} for t in transaktionen ] log_mcp_read(role, "system", "Globale Suche", f"Suche: '{suchbegriff}'") return format_result(ergebnisse) def dashboard() -> str: """ Gibt eine Übersicht der wichtigsten Stiftungsdaten zurück. """ from datetime import date as date_type from stiftung.models import ( BankTransaction, Destinataer, DestinataerUnterstuetzung, Land, LandVerpachtung, StiftungsKalenderEintrag, StiftungsKonto, ) role = _get_role() heute = date_type.today() # Konten-Gesamtsaldo konten = StiftungsKonto.objects.filter(aktiv=True) gesamt_saldo = sum(float(k.saldo or 0) for k in konten) # Destinatäre aktive_dest = Destinataer.objects.filter(aktiv=True).count() # Offene Zahlungen offene_zahlungen = DestinataerUnterstuetzung.objects.filter( status__in=["geplant", "faellig", "nachweis_eingereicht", "freigegeben"] ).aggregate(anzahl=Sum("betrag")) offene_zahlungen_betrag = float(offene_zahlungen["anzahl"] or 0) offene_zahlungen_anzahl = DestinataerUnterstuetzung.objects.filter( status__in=["geplant", "faellig", "nachweis_eingereicht", "freigegeben"] ).count() # Fällige Termine (nächste 30 Tage) from datetime import timedelta naechste_termine = StiftungsKalenderEintrag.objects.filter( datum__gte=heute, datum__lte=heute + timedelta(days=30), ).order_by("datum")[:5] termine_liste = [ {"titel": t.titel, "datum": t.datum.isoformat(), "prioritaet": t.prioritaet, "kategorie": t.kategorie} for t in naechste_termine ] # Aktive Verpachtungen aktive_verpachtungen = LandVerpachtung.objects.filter(status="aktiv").count() log_mcp_read(role, "system", "Dashboard", "Dashboard abgerufen") return format_result({ "stand": heute.isoformat(), "finanzen": { "gesamt_saldo_eur": round(gesamt_saldo, 2), "anzahl_konten": konten.count(), }, "destinataere": { "aktiv": aktive_dest, }, "zahlungen": { "offen_anzahl": offene_zahlungen_anzahl, "offen_betrag_eur": round(offene_zahlungen_betrag, 2), }, "verpachtungen": { "aktiv": aktive_verpachtungen, }, "naechste_termine": termine_liste, }) def statistiken() -> str: """ Gibt detaillierte Statistiken der Stiftungsverwaltung zurück. """ from datetime import date as date_type from stiftung.models import ( BankTransaction, Destinataer, Foerderung, Land, LandVerpachtung, Verwaltungskosten, ) role = _get_role() aktuelles_jahr = date_type.today().year # Förderungen dieses Jahr foerderungen_jahr = Foerderung.objects.filter(jahr=aktuelles_jahr) foerderungen_gesamt = foerderungen_jahr.aggregate(summe=Sum("betrag")) # Destinatäre nach Familienzweig from django.db.models import Count dest_zweige = list( Destinataer.objects.filter(aktiv=True) .values("familienzweig") .annotate(anzahl=Count("id")) .order_by("-anzahl") ) # Verwaltungskosten dieses Jahr vk_jahr = Verwaltungskosten.objects.filter(datum__year=aktuelles_jahr) vk_gesamt = vk_jahr.aggregate(summe=Sum("betrag")) # Ländereien laender_ges = Land.objects.count() laender_verpachtet = Land.objects.filter( neue_verpachtungen__status="aktiv" ).distinct().count() log_mcp_read(role, "system", "Statistiken", f"Statistiken für {aktuelles_jahr} abgerufen") return format_result({ "jahr": aktuelles_jahr, "foerderungen": { "anzahl": foerderungen_jahr.count(), "gesamt_betrag_eur": float(foerderungen_gesamt["summe"] or 0), }, "verwaltungskosten": { "anzahl": vk_jahr.count(), "gesamt_betrag_eur": float(vk_gesamt["summe"] or 0), }, "destinataere_nach_zweig": dest_zweige, "laendereien": { "gesamt": laender_ges, "aktiv_verpachtet": laender_verpachtet, }, })