- Save cover email body as DMS document with new 'email' context type - Show email body separately from attachments in email detail view - Add per-category DMS document assignment in quarterly confirmation (Studiennachweis, Einkommenssituation, Vermögenssituation) - Add VERSION file and context processor for automatic version display - Add MCP server, agent system, import/export, and new migrations - Update compose files and production environment template Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
756 lines
27 KiB
Python
756 lines
27 KiB
Python
"""
|
|
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})
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# 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,
|
|
},
|
|
})
|