""" Schreib-Tools für den MCP Server der Stiftungsverwaltung. Alle Tools: - Prüfen die Rolle (editor oder admin erforderlich) - Schreiben Audit-Log-Einträge - Validieren Pflichtfelder vor dem Speichern """ from __future__ import annotations from mcp_server.audit import log_mcp_create, log_mcp_update from mcp_server.auth import can_write, require_role from mcp_server.tools.helpers import format_result def _require_write_role() -> str: from mcp_server.auth import get_current_role role = get_current_role() require_role(role) if not can_write(role): raise PermissionError( f"Rolle '{role}' hat keine Schreibrechte. " "editor- oder admin-Rolle erforderlich." ) return role # ────────────────────────────────────────────────────────────────────────────── # Destinatäre # ────────────────────────────────────────────────────────────────────────────── def destinataer_anlegen( vorname: str, nachname: str, familienzweig: str = "", email: str = "", telefon: str = "", geburtsdatum: str = "", ort: str = "", plz: str = "", strasse: str = "", berufsgruppe: str = "", notizen: str = "", ) -> str: """ Legt einen neuen Destinatär an. Args: vorname: Vorname (Pflichtfeld) nachname: Nachname (Pflichtfeld) familienzweig: hauptzweig/nebenzweig/verwandt/anderer email: E-Mail-Adresse telefon: Telefonnummer geburtsdatum: Geburtsdatum YYYY-MM-DD ort: Wohnort plz: Postleitzahl strasse: Straße und Hausnummer berufsgruppe: student/wissenschaftler/künstler/sozialarbeiter/umweltschützer/andere notizen: Freitext-Notizen """ from stiftung.models import Destinataer role = _require_write_role() kwargs = { "vorname": vorname.strip(), "nachname": nachname.strip(), "aktiv": True, } if familienzweig: kwargs["familienzweig"] = familienzweig if email: kwargs["email"] = email if telefon: kwargs["telefon"] = telefon if geburtsdatum: kwargs["geburtsdatum"] = geburtsdatum if ort: kwargs["ort"] = ort if plz: kwargs["plz"] = plz if strasse: kwargs["strasse"] = strasse if berufsgruppe: kwargs["berufsgruppe"] = berufsgruppe if notizen: kwargs["notizen"] = notizen obj = Destinataer.objects.create(**kwargs) log_mcp_create(role, "destinataer", str(obj.id), f"{vorname} {nachname}") return format_result({"erfolg": True, "id": str(obj.id), "name": f"{vorname} {nachname}"}) def destinataer_aktualisieren( destinataer_id: str, vorname: str = "", nachname: str = "", email: str = "", telefon: str = "", ort: str = "", plz: str = "", strasse: str = "", aktiv: bool | None = None, notizen: str = "", familienzweig: str = "", ) -> str: """ Aktualisiert einen bestehenden Destinatär. Args: destinataer_id: UUID des Destinatärs (Pflichtfeld) vorname: Neuer Vorname (optional) nachname: Neuer Nachname (optional) email: Neue E-Mail (optional) telefon: Neue Telefonnummer (optional) ort: Neuer Ort (optional) plz: Neue PLZ (optional) strasse: Neue Straße (optional) aktiv: Aktivstatus (optional) notizen: Neue Notizen (optional) familienzweig: Neuer Familienzweig (optional) """ from stiftung.models import Destinataer role = _require_write_role() try: obj = Destinataer.objects.get(id=destinataer_id) except Destinataer.DoesNotExist: return format_result({"fehler": f"Destinatär {destinataer_id} nicht gefunden"}) changes = {} update_fields = [] def _set(field, value): if value != "" and value is not None: old = getattr(obj, field) if str(old) != str(value): changes[field] = {"alt": str(old), "neu": str(value)} setattr(obj, field, value) update_fields.append(field) _set("vorname", vorname) _set("nachname", nachname) _set("email", email) _set("telefon", telefon) _set("ort", ort) _set("plz", plz) _set("strasse", strasse) _set("notizen", notizen) _set("familienzweig", familienzweig) if aktiv is not None: _set("aktiv", aktiv) if not update_fields: return format_result({"erfolg": True, "hinweis": "Keine Änderungen"}) obj.save(update_fields=update_fields) name = f"{obj.vorname} {obj.nachname}" log_mcp_update(role, "destinataer", str(obj.id), name, changes) return format_result({"erfolg": True, "id": str(obj.id), "geaenderte_felder": list(changes.keys())}) # ────────────────────────────────────────────────────────────────────────────── # Förderungen & Unterstützungen # ────────────────────────────────────────────────────────────────────────────── def foerderung_anlegen( destinataer_id: str, jahr: int, betrag: float, kategorie: str = "anderes", bemerkungen: str = "", ) -> str: """ Legt eine neue Förderung für einen Destinatär an. Args: destinataer_id: UUID des Destinatärs (Pflichtfeld) jahr: Förderjahr (Pflichtfeld) betrag: Förderbetrag in EUR (Pflichtfeld) kategorie: bildung/forschung/kultur/soziales/umwelt/anderes bemerkungen: Freitext """ from datetime import date from stiftung.models import Destinataer, Foerderung role = _require_write_role() try: destinataer = Destinataer.objects.get(id=destinataer_id) except Destinataer.DoesNotExist: return format_result({"fehler": f"Destinatär {destinataer_id} nicht gefunden"}) foerderung = Foerderung.objects.create( destinataer=destinataer, jahr=jahr, betrag=betrag, kategorie=kategorie, bemerkungen=bemerkungen, status="beantragt", antragsdatum=date.today(), ) name = f"{destinataer.vorname} {destinataer.nachname} – {jahr}" log_mcp_create(role, "foerderung", str(foerderung.id), name) return format_result({"erfolg": True, "id": str(foerderung.id), "foerderung": name}) def unterstuetzung_anlegen( destinataer_id: str, konto_id: str, betrag: float, faellig_am: str, beschreibung: str = "", verwendungszweck: str = "", ) -> str: """ Legt eine neue Unterstützungszahlung für einen Destinatär an. Args: destinataer_id: UUID des Destinatärs (Pflichtfeld) konto_id: UUID des Zahlungskontos (Pflichtfeld) betrag: Betrag in EUR (Pflichtfeld) faellig_am: Fälligkeitsdatum YYYY-MM-DD (Pflichtfeld) beschreibung: Kurzbeschreibung (optional) verwendungszweck: Verwendungszweck für Überweisung (optional) """ from stiftung.models import Destinataer, DestinataerUnterstuetzung, StiftungsKonto role = _require_write_role() try: destinataer = Destinataer.objects.get(id=destinataer_id) except Destinataer.DoesNotExist: return format_result({"fehler": f"Destinatär {destinataer_id} nicht gefunden"}) try: konto = StiftungsKonto.objects.get(id=konto_id) except StiftungsKonto.DoesNotExist: return format_result({"fehler": f"Konto {konto_id} nicht gefunden"}) unterstuetzung = DestinataerUnterstuetzung.objects.create( destinataer=destinataer, konto=konto, betrag=betrag, faellig_am=faellig_am, beschreibung=beschreibung, verwendungszweck=verwendungszweck, status="geplant", ) name = f"{destinataer.vorname} {destinataer.nachname} – {faellig_am}" log_mcp_create(role, "destinataer", str(unterstuetzung.id), name) return format_result({"erfolg": True, "id": str(unterstuetzung.id)}) # ────────────────────────────────────────────────────────────────────────────── # Ländereien & Verpachtungen # ────────────────────────────────────────────────────────────────────────────── def land_anlegen( lfd_nr: str, amtsgericht: str, gemeinde: str, gemarkung: str, flur: str, flurstueck: str, groesse_qm: float, verpachtete_gesamtflaeche: float = 0.0, adresse: str = "", ) -> str: """ Legt eine neue Länderei an. Args: lfd_nr: Laufende Nummer (Pflichtfeld, eindeutig) amtsgericht: Zuständiges Amtsgericht (Pflichtfeld) gemeinde: Gemeinde (Pflichtfeld) gemarkung: Gemarkung (Pflichtfeld) flur: Flur (Pflichtfeld) flurstueck: Flurstück (Pflichtfeld) groesse_qm: Gesamtgröße in Quadratmetern (Pflichtfeld) verpachtete_gesamtflaeche: Verpachtete Fläche in qm (Standard: 0) adresse: Adresse/Ortsangabe (optional) """ from stiftung.models import Land role = _require_write_role() if Land.objects.filter(lfd_nr=lfd_nr).exists(): return format_result({"fehler": f"Länderei mit lfd_nr '{lfd_nr}' existiert bereits"}) land = Land.objects.create( lfd_nr=lfd_nr, amtsgericht=amtsgericht, gemeinde=gemeinde, gemarkung=gemarkung, flur=flur, flurstueck=flurstueck, groesse_qm=groesse_qm, verpachtete_gesamtflaeche=verpachtete_gesamtflaeche, adresse=adresse, ) log_mcp_create(role, "land", str(land.id), str(land)) return format_result({"erfolg": True, "id": str(land.id), "bezeichnung": str(land)}) def verpachtung_anlegen( land_id: str, paechter_id: str, vertragsnummer: str, pachtbeginn: str, verpachtete_flaeche: float, pachtzins_pauschal: float, zahlungsweise: str = "jaehrlich", pachtende: str = "", ) -> str: """ Legt einen neuen Pachtvertrag für eine Länderei an. Args: land_id: UUID der Länderei (Pflichtfeld) paechter_id: UUID des Pächters (Pflichtfeld) vertragsnummer: Eindeutige Vertragsnummer (Pflichtfeld) pachtbeginn: Datum YYYY-MM-DD (Pflichtfeld) verpachtete_flaeche: Fläche in qm (Pflichtfeld) pachtzins_pauschal: Jährlicher Pachtzins in EUR (Pflichtfeld) zahlungsweise: jaehrlich/halbjaehrlich/vierteljaehrlich/monatlich pachtende: Datum YYYY-MM-DD (optional) """ from stiftung.models import Land, LandVerpachtung, Paechter role = _require_write_role() try: land = Land.objects.get(id=land_id) except Land.DoesNotExist: return format_result({"fehler": f"Länderei {land_id} nicht gefunden"}) try: paechter = Paechter.objects.get(id=paechter_id) except Paechter.DoesNotExist: return format_result({"fehler": f"Pächter {paechter_id} nicht gefunden"}) if LandVerpachtung.objects.filter(vertragsnummer=vertragsnummer).exists(): return format_result({"fehler": f"Vertragsnummer '{vertragsnummer}' existiert bereits"}) kwargs = { "land": land, "paechter": paechter, "vertragsnummer": vertragsnummer, "pachtbeginn": pachtbeginn, "verpachtete_flaeche": verpachtete_flaeche, "pachtzins_pauschal": pachtzins_pauschal, "zahlungsweise": zahlungsweise, "status": "aktiv", } if pachtende: kwargs["pachtende"] = pachtende verpachtung = LandVerpachtung.objects.create(**kwargs) name = f"{land} – {paechter}" log_mcp_create(role, "verpachtung", str(verpachtung.id), name) return format_result({"erfolg": True, "id": str(verpachtung.id)}) def paechter_anlegen( vorname: str, nachname: str, email: str = "", telefon: str = "", ort: str = "", plz: str = "", strasse: str = "", personentyp: str = "natuerlich", ) -> str: """ Legt einen neuen Pächter an. Args: vorname: Vorname (Pflichtfeld) nachname: Nachname (Pflichtfeld) email: E-Mail (optional) telefon: Telefon (optional) ort: Ort (optional) plz: Postleitzahl (optional) strasse: Straße (optional) personentyp: natuerlich/gesellschaft """ from stiftung.models import Paechter role = _require_write_role() kwargs = { "vorname": vorname.strip(), "nachname": nachname.strip(), "personentyp": personentyp, } for field, value in [("email", email), ("telefon", telefon), ("ort", ort), ("plz", plz), ("strasse", strasse)]: if value: kwargs[field] = value paechter = Paechter.objects.create(**kwargs) name = f"{vorname} {nachname}" log_mcp_create(role, "paechter", str(paechter.id), name) return format_result({"erfolg": True, "id": str(paechter.id), "name": name}) # ────────────────────────────────────────────────────────────────────────────── # Verwaltungskosten # ────────────────────────────────────────────────────────────────────────────── def verwaltungskosten_erfassen( bezeichnung: str, kategorie: str, betrag: float, datum: str, lieferant_firma: str = "", rechnungsnummer: str = "", status: str = "geplant", ) -> str: """ Erfasst eine neue Verwaltungskosten-Position. Args: bezeichnung: Bezeichnung (Pflichtfeld) kategorie: rechnung_intern/bueroausstattung/fahrtkosten/porto/telefon_internet/ software/beratung/versicherung/steuerberatung/bankgebuehren/sonstiges betrag: Betrag in EUR (Pflichtfeld) datum: Datum YYYY-MM-DD (Pflichtfeld) lieferant_firma: Lieferant oder Firma (optional) rechnungsnummer: Rechnungsnummer (optional) status: geplant/bestellt/erhalten/in_bearbeitung/bezahlt/storniert """ from stiftung.models import Verwaltungskosten role = _require_write_role() vk = Verwaltungskosten.objects.create( bezeichnung=bezeichnung, kategorie=kategorie, betrag=betrag, datum=datum, lieferant_firma=lieferant_firma, rechnungsnummer=rechnungsnummer, status=status, ) log_mcp_create(role, "verwaltungskosten", str(vk.id), bezeichnung) return format_result({"erfolg": True, "id": str(vk.id), "bezeichnung": bezeichnung}) # ────────────────────────────────────────────────────────────────────────────── # Termine # ────────────────────────────────────────────────────────────────────────────── def termin_anlegen( titel: str, datum: str, kategorie: str = "termin", prioritaet: str = "normal", beschreibung: str = "", uhrzeit: str = "", ganztags: bool = True, destinataer_id: str = "", ) -> str: """ Legt einen neuen Kalendertermin an. Args: titel: Titel des Termins (Pflichtfeld) datum: Datum YYYY-MM-DD (Pflichtfeld) kategorie: termin/zahlung/deadline/geburtstag/vertrag/pruefung/sonstiges prioritaet: niedrig/normal/hoch/kritisch beschreibung: Beschreibung (optional) uhrzeit: Uhrzeit HH:MM (optional) ganztags: Ganztägig (Standard: True) destinataer_id: UUID eines zugehörigen Destinatärs (optional) """ from stiftung.models import Destinataer, StiftungsKalenderEintrag role = _require_write_role() kwargs = { "titel": titel, "datum": datum, "kategorie": kategorie, "prioritaet": prioritaet, "beschreibung": beschreibung, "ganztags": ganztags, } if uhrzeit: kwargs["uhrzeit"] = uhrzeit kwargs["ganztags"] = False if destinataer_id: try: kwargs["destinataer"] = Destinataer.objects.get(id=destinataer_id) except Destinataer.DoesNotExist: return format_result({"fehler": f"Destinatär {destinataer_id} nicht gefunden"}) termin = StiftungsKalenderEintrag.objects.create(**kwargs) log_mcp_create(role, "system", str(termin.id), titel) return format_result({"erfolg": True, "id": str(termin.id), "titel": titel, "datum": datum}) # ────────────────────────────────────────────────────────────────────────────── # Dokument verknüpfen # ────────────────────────────────────────────────────────────────────────────── def dokument_verknuepfen( dokument_id: str, land_id: str = "", paechter_id: str = "", destinataer_id: str = "", ) -> str: """ Verknüpft ein vorhandenes Dokument mit einer Länderei, einem Pächter oder Destinatär. Args: dokument_id: UUID des Dokuments (Pflichtfeld) land_id: UUID der Länderei (optional) paechter_id: UUID des Pächters (optional) destinataer_id: UUID des Destinatärs (optional) """ from stiftung.models import DokumentDatei role = _require_write_role() try: dokument = DokumentDatei.objects.get(id=dokument_id) except DokumentDatei.DoesNotExist: return format_result({"fehler": f"Dokument {dokument_id} nicht gefunden"}) changes = {} update_fields = [] if land_id: from stiftung.models import Land try: land = Land.objects.get(id=land_id) dokument.land = land update_fields.append("land") changes["land"] = {"neu": str(land)} except Land.DoesNotExist: return format_result({"fehler": f"Länderei {land_id} nicht gefunden"}) if paechter_id: from stiftung.models import Paechter try: paechter = Paechter.objects.get(id=paechter_id) dokument.paechter = paechter update_fields.append("paechter") changes["paechter"] = {"neu": str(paechter)} except Paechter.DoesNotExist: return format_result({"fehler": f"Pächter {paechter_id} nicht gefunden"}) if destinataer_id: from stiftung.models import Destinataer try: dest = Destinataer.objects.get(id=destinataer_id) dokument.destinataer = dest update_fields.append("destinataer") changes["destinataer"] = {"neu": f"{dest.vorname} {dest.nachname}"} except Destinataer.DoesNotExist: return format_result({"fehler": f"Destinatär {destinataer_id} nicht gefunden"}) if not update_fields: return format_result({"fehler": "Keine Verknüpfung angegeben (land_id, paechter_id oder destinataer_id)"}) dokument.save(update_fields=update_fields) log_mcp_update(role, "dokumentlink", str(dokument.id), dokument.titel, changes) return format_result({"erfolg": True, "id": str(dokument.id), "verknuepft_mit": list(changes.keys())})