v4.1.0: DMS email documents, category-specific Nachweis linking, version system
- 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>
This commit is contained in:
755
app/mcp_server/tools/lesen.py
Normal file
755
app/mcp_server/tools/lesen.py
Normal file
@@ -0,0 +1,755 @@
|
||||
"""
|
||||
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,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user