gateway/modules/features/trustee/mainTrustee.py

975 lines
43 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.

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Trustee Feature Container - Main Module.
Handles feature initialization and RBAC catalog registration.
"""
import logging
from typing import Dict, List, Any
from modules.shared.i18nRegistry import t
logger = logging.getLogger(__name__)
# Feature metadata
FEATURE_CODE = "trustee"
FEATURE_LABEL = t("Treuhand", context="UI")
FEATURE_ICON = "mdi-briefcase"
# UI Objects for RBAC catalog
# Note: organisations and contracts removed - feature instance = organisation
UI_OBJECTS = [
{
"objectKey": "ui.feature.trustee.dashboard",
"label": t("Dashboard", context="UI"),
"meta": {"area": "dashboard"}
},
# Note: ui.feature.trustee.positions and .documents removed.
# Positionen and Dokumente are now consolidated tabs inside the
# ui.feature.trustee.data-tables view (TrusteeDataTablesView).
# Data-level RBAC (data.feature.trustee.TrusteePosition / .TrusteeDocument)
# remains and continues to gate per-row access.
{
"objectKey": "ui.feature.trustee.data-tables",
"label": t("Daten-Tabellen", context="UI"),
"meta": {"area": "data-tables"}
},
{
"objectKey": "ui.feature.trustee.import-process",
"label": t("Import & Verarbeitung", context="UI"),
"meta": {"area": "import-process"}
},
{
"objectKey": "ui.feature.trustee.analyse",
"label": t("Analyse & Reporting", context="UI"),
"meta": {"area": "analyse"}
},
{
"objectKey": "ui.feature.trustee.abschluss",
"label": t("Abschluss & Prüfung", context="UI"),
"meta": {"area": "abschluss"}
},
{
"objectKey": "ui.feature.trustee.settings",
"label": t("Buchhaltungs-Einstellungen", context="UI"),
"meta": {"area": "settings", "admin_only": True}
},
{
"objectKey": "ui.feature.trustee.instance-roles",
"label": t("Instanz-Rollen & Berechtigungen", context="UI"),
"meta": {"area": "admin", "admin_only": True}
},
]
# DATA Objects for RBAC catalog (tables/entities)
# Used for AccessRules on data-level permissions.
# Architecture note: a feature instance IS the organisation. There is no
# TrusteeOrganisation parent grouping in the UDB — all tables are scoped
# to the feature instance via featureInstanceId.
DATA_OBJECTS = [
# ── Categorical Groups (UDB folders) ─────────────────────────────────────
{
"objectKey": "data.feature.trustee.localData",
"label": t("Lokale Daten", context="UI"),
"meta": {"isGroup": True}
},
{
"objectKey": "data.feature.trustee.config",
"label": t("Konfiguration", context="UI"),
"meta": {"isGroup": True}
},
{
"objectKey": "data.feature.trustee.accountingData",
"label": t("Daten aus Buchhaltungssystem", context="UI"),
"meta": {"isGroup": True}
},
# ── Lokale Daten ─────────────────────────────────────────────────────────
{
"objectKey": "data.feature.trustee.TrusteePosition",
"label": t("Position", context="UI"),
"meta": {
"table": "TrusteePosition",
"group": "data.feature.trustee.localData",
"fields": ["id", "valuta", "company", "desc", "bookingAmount", "bookingCurrency", "debitAccountNumber", "creditAccountNumber"],
}
},
{
"objectKey": "data.feature.trustee.TrusteeDocument",
"label": t("Dokument", context="UI"),
"meta": {
"table": "TrusteeDocument",
"group": "data.feature.trustee.localData",
"fields": ["id", "documentName", "documentMimeType", "documentType", "sourceType"],
}
},
# ── Konfiguration ────────────────────────────────────────────────────────
{
"objectKey": "data.feature.trustee.TrusteeAccountingConfig",
"label": t("Buchhaltungs-Verbindung", context="UI"),
"meta": {
"table": "TrusteeAccountingConfig",
"group": "data.feature.trustee.config",
"fields": ["id", "connectorType", "displayLabel", "isActive", "lastSyncAt", "lastSyncStatus"],
}
},
{
"objectKey": "data.feature.trustee.TrusteeAccountingSync",
"label": t("Sync-Protokoll", context="UI"),
"meta": {
"table": "TrusteeAccountingSync",
"group": "data.feature.trustee.config",
"fields": ["id", "positionId", "syncStatus", "externalId"],
}
},
# ── Daten aus Buchhaltungssystem ─────────────────────────────────────────
{
"objectKey": "data.feature.trustee.TrusteeDataAccount",
"label": t("Kontenplan", context="UI"),
"meta": {
"table": "TrusteeDataAccount",
"group": "data.feature.trustee.accountingData",
"fields": ["id", "accountNumber", "label", "accountType", "accountGroup", "currency", "isActive"],
}
},
{
"objectKey": "data.feature.trustee.TrusteeDataJournalEntry",
"label": t("Buchungen", context="UI"),
"meta": {
"table": "TrusteeDataJournalEntry",
"group": "data.feature.trustee.accountingData",
"fields": ["id", "externalId", "bookingDate", "reference", "description", "currency", "totalAmount"],
}
},
{
"objectKey": "data.feature.trustee.TrusteeDataJournalLine",
"label": t("Buchungszeilen", context="UI"),
"meta": {
"table": "TrusteeDataJournalLine",
"group": "data.feature.trustee.accountingData",
"fields": ["id", "journalEntryId", "accountNumber", "debitAmount", "creditAmount", "currency", "taxCode", "costCenter", "description"],
}
},
{
"objectKey": "data.feature.trustee.TrusteeDataContact",
"label": t("Kontakte", context="UI"),
"meta": {
"table": "TrusteeDataContact",
"group": "data.feature.trustee.accountingData",
"fields": ["id", "externalId", "contactType", "contactNumber", "name", "address", "zip", "city", "country", "email", "phone", "vatNumber"],
}
},
{
"objectKey": "data.feature.trustee.TrusteeDataAccountBalance",
"label": t("Kontosalden", context="UI"),
"meta": {
"table": "TrusteeDataAccountBalance",
"group": "data.feature.trustee.accountingData",
"fields": ["id", "accountNumber", "periodYear", "periodMonth", "openingBalance", "debitTotal", "creditTotal", "closingBalance", "currency"],
}
},
{
"objectKey": "data.feature.trustee.*",
"label": t("Alle Treuhand-Daten", context="UI"),
"meta": {"wildcard": True, "description": "Wildcard for all trustee data tables"}
},
]
# Resource Objects for RBAC catalog
# Note: organisations and contracts removed - feature instance = organisation
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.trustee.documents.create",
"label": t("Dokument hochladen", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/documents", "method": "POST"}
},
{
"objectKey": "resource.feature.trustee.documents.update",
"label": t("Dokument aktualisieren", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "PUT"}
},
{
"objectKey": "resource.feature.trustee.documents.delete",
"label": t("Dokument löschen", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "DELETE"}
},
{
"objectKey": "resource.feature.trustee.positions.create",
"label": t("Position erstellen", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/positions", "method": "POST"}
},
{
"objectKey": "resource.feature.trustee.positions.update",
"label": t("Position aktualisieren", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "PUT"}
},
{
"objectKey": "resource.feature.trustee.positions.delete",
"label": t("Position löschen", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "DELETE"}
},
{
"objectKey": "resource.feature.trustee.instance-roles.manage",
"label": t("Instanz-Rollen verwalten", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/instance-roles", "method": "ALL", "admin_only": True}
},
{
"objectKey": "resource.feature.trustee.accounting.manage",
"label": t("Buchhaltungs-Integration verwalten", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/config", "method": "ALL", "admin_only": True}
},
{
"objectKey": "resource.feature.trustee.accounting.sync",
"label": t("Buchhaltung synchronisieren", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync", "method": "POST"}
},
{
"objectKey": "resource.feature.trustee.accounting.view",
"label": t("Sync-Status einsehen", context="UI"),
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync-status", "method": "GET"}
},
{
"objectKey": "resource.feature.trustee.workflows.view",
"label": t("Workflows einsehen", context="UI"),
"meta": {"endpoint": "/api/workflows/{instanceId}/workflows", "method": "GET"}
},
{
"objectKey": "resource.feature.trustee.workflows.execute",
"label": t("Workflows ausführen", context="UI"),
"meta": {"endpoint": "/api/workflows/{instanceId}/execute", "method": "POST"}
},
{
"objectKey": "resource.feature.trustee.workflows.manage",
"label": t("Workflows verwalten", context="UI"),
"meta": {"endpoint": "/api/workflows/{instanceId}/workflows", "method": "ALL", "admin_only": True}
},
]
# Template roles for this feature with AccessRules
# Each role defines default UI and DATA permissions
# Note: UI item=None means ALL views, specific items restrict to named views
# IMPORTANT: item uses vollqualifizierte ObjectKeys (gemäss Navigation-API-Konzept)
QUICK_ACTION_CATEGORIES = [
{"id": "import", "label": "Import & Verarbeitung", "sortOrder": 1},
{"id": "analyse", "label": "Analyse & Reporting", "sortOrder": 2},
{"id": "abschluss", "label": "Abschluss & Prüfung", "sortOrder": 3},
]
QUICK_ACTIONS = [
{
"id": "trustee-process-receipts",
"label": t("Belege verarbeiten", context="UI"),
"description": "Belege aus SharePoint importieren, klassifizieren und verbuchen",
"icon": "mdi-file-document-check-outline",
"color": "#4CAF50",
"category": "import",
"actionType": "link",
"config": {"targetView": "import-process", "tab": "receipts"},
"requiredRoles": ["trustee-user", "trustee-accountant", "trustee-admin"],
"sortOrder": 1,
},
{
"id": "trustee-upload-receipt",
"label": t("Beleg hochladen", context="UI"),
"description": "Beleg scannen oder als Datei hochladen",
"icon": "mdi-camera-document-outline",
"color": "#607D8B",
"category": "import",
"actionType": "link",
"config": {"targetView": "import-process", "tab": "upload"},
"requiredRoles": ["trustee-user", "trustee-client", "trustee-accountant", "trustee-admin"],
"sortOrder": 2,
},
{
"id": "trustee-sync-accounting",
"label": t("Daten einlesen", context="UI"),
"description": "Buchhaltungsdaten aus dem externen System aktualisieren",
"icon": "mdi-sync",
"color": "#FF9800",
"category": "import",
"actionType": "link",
"config": {"targetView": "settings", "tab": "import-data"},
"requiredRoles": ["trustee-accountant", "trustee-admin"],
"sortOrder": 3,
},
{
"id": "trustee-budget-comparison",
"label": t("Budget-Vergleich", context="UI"),
"description": "Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel",
"icon": "mdi-chart-bar",
"color": "#2196F3",
"category": "analyse",
"actionType": "link",
"config": {"targetView": "analyse", "tab": "budget"},
"requiredRoles": ["trustee-accountant", "trustee-admin"],
"sortOrder": 4,
},
{
"id": "trustee-kpi-dashboard",
"label": t("KPI-Dashboard", context="UI"),
"description": "Kennzahlen berechnen und visualisieren",
"icon": "mdi-view-dashboard-outline",
"color": "#9C27B0",
"category": "analyse",
"actionType": "link",
"config": {"targetView": "analyse", "tab": "kpi"},
"requiredRoles": ["trustee-accountant", "trustee-admin"],
"sortOrder": 5,
},
{
"id": "trustee-cashflow",
"label": t("Cashflow-Rechnung", context="UI"),
"description": "Cashflow berechnen und analysieren",
"icon": "mdi-cash-multiple",
"color": "#009688",
"category": "analyse",
"actionType": "link",
"config": {"targetView": "analyse", "tab": "cashflow"},
"requiredRoles": ["trustee-accountant", "trustee-admin"],
"sortOrder": 6,
},
{
"id": "trustee-forecast",
"label": t("Prognose erstellen", context="UI"),
"description": "Trend-Analyse und Prognose der nächsten Monate",
"icon": "mdi-chart-timeline-variant",
"color": "#E91E63",
"category": "analyse",
"actionType": "link",
"config": {"targetView": "analyse", "tab": "forecast"},
"requiredRoles": ["trustee-accountant", "trustee-admin"],
"sortOrder": 7,
},
{
"id": "trustee-year-end-check",
"label": t("Jahresabschluss prüfen", context="UI"),
"description": "Automatische Prüfungen für den Jahresabschluss",
"icon": "mdi-clipboard-check-outline",
"color": "#795548",
"category": "abschluss",
"actionType": "link",
"config": {"targetView": "abschluss", "tab": "year-end"},
"requiredRoles": ["trustee-accountant", "trustee-admin"],
"sortOrder": 8,
},
]
# ---------------------------------------------------------------------------
# Template Workflows — bootstrapped into each new feature instance.
# Graphs use existing nodes: trigger.manual, trustee.refreshAccountingData, ai.prompt.
# The placeholder {{featureInstanceId}} is replaced by _copyTemplateWorkflows.
# ---------------------------------------------------------------------------
def _buildAnalysisWorkflowGraph(prompt: str) -> Dict[str, Any]:
"""Build a standard analysis graph: trigger -> refreshAccountingData -> ai.prompt."""
return {
"nodes": [
{"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
{"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten laden", "_method": "trustee", "_action": "refreshAccountingData",
"parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": False}, "position": {"x": 250, "y": 0}},
{"id": "analyse", "type": "ai.prompt", "label": "Analyse", "_method": "ai", "_action": "process",
"parameters": {
"aiPrompt": prompt,
"context": {"type": "ref", "nodeId": "refresh", "path": ["data", "accountingData"]},
"simpleMode": False,
}, "position": {"x": 500, "y": 0}},
],
"connections": [
{"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0},
{"source": "refresh", "sourcePort": 0, "target": "analyse", "targetPort": 0},
],
}
TEMPLATE_WORKFLOWS = [
{
"id": "trustee-receipt-import",
"label": t("Beleg-Import Pipeline", context="UI"),
"description": "Belege extrahieren, verarbeiten und in Buchhaltung synchronisieren",
"tags": ["feature:trustee", "template:trustee-receipt-import"],
"graph": {
"nodes": [
{"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
{"id": "extract", "type": "trustee.extractFromFiles", "label": "Dokumente extrahieren", "_method": "trustee", "_action": "extractFromFiles",
"parameters": {"featureInstanceId": "{{featureInstanceId}}", "prompt": ""}, "position": {"x": 250, "y": 0}},
{"id": "process", "type": "trustee.processDocuments", "label": "Verarbeiten", "_method": "trustee", "_action": "processDocuments",
"parameters": {
"documentList": {"type": "ref", "nodeId": "extract", "path": ["documents"]},
"featureInstanceId": "{{featureInstanceId}}",
}, "position": {"x": 500, "y": 0}},
{"id": "sync", "type": "trustee.syncToAccounting", "label": "Synchronisieren", "_method": "trustee", "_action": "syncToAccounting",
"parameters": {
"documentList": {"type": "ref", "nodeId": "process", "path": ["documents"]},
"featureInstanceId": "{{featureInstanceId}}",
}, "position": {"x": 750, "y": 0}},
],
"connections": [
{"source": "trigger", "sourcePort": 0, "target": "extract", "targetPort": 0},
{"source": "extract", "sourcePort": 0, "target": "process", "targetPort": 0},
{"source": "process", "sourcePort": 0, "target": "sync", "targetPort": 0},
],
},
},
{
"id": "trustee-sync-accounting",
"label": t("Buchhaltung synchronisieren", context="UI"),
"description": "Buchhaltungsdaten aus dem externen System aktualisieren",
"tags": ["feature:trustee", "template:trustee-sync-accounting"],
"graph": {
"nodes": [
{"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
{"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten aktualisieren", "_method": "trustee", "_action": "refreshAccountingData",
"parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": True}, "position": {"x": 250, "y": 0}},
],
"connections": [
{"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0},
],
},
},
{
"id": "trustee-budget-comparison",
"label": t("Budget-Vergleich", context="UI"),
"description": "Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel",
"tags": ["feature:trustee", "template:trustee-budget-comparison"],
"graph": {
"nodes": [
{"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
{"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten laden", "_method": "trustee", "_action": "refreshAccountingData",
"parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": False}, "position": {"x": 250, "y": 0}},
{"id": "analyse", "type": "ai.prompt", "label": "Budget-Analyse", "_method": "ai", "_action": "process",
"parameters": {
"aiPrompt": (
"Fuehre einen Budget-Soll/Ist-Vergleich durch und liefere EIN Excel-Dokument "
"mit folgender Struktur:\n\n"
"1. Tabelle \"Konten-Vergleich\" -- EINE Tabelle, EINE Zeile pro Konto:\n"
" Spalten: Konto-Nr | Konto-Name | Soll | Ist | Abweichung absolut | "
"Abweichung % | Status (OK / Warnung / Kritisch).\n"
"2. EINE Visualisierung \"Soll vs. Ist gesamt\" -- ein einziges "
"Balkendiagramm UNTER der Tabelle, das ALLE Konten in einer Grafik "
"gegenueberstellt (gruppierte Balken: Soll und Ist je Konto).\n"
"3. Kurzer Management-Summary-Absatz (3-5 Saetze) UNTER dem Chart "
"mit den 3 groessten Abweichungen (>10%) und einer fachlichen "
"Einschaetzung.\n\n"
"Verwende die uebergebene Budget-Datei als Soll-Quelle und die im "
"Kontext bereitgestellten Buchhaltungsdaten als Ist-Quelle.\n"
"WICHTIG: Erstelle KEINEN separaten Chart pro Konto. Nur EIN "
"Uebersichts-Chart ueber alle Konten ist gewuenscht."
),
"resultType": "xlsx",
"documentTheme": "finance",
"documentList": {"type": "ref", "nodeId": "trigger", "path": ["payload", "documentList"]},
"context": {"type": "ref", "nodeId": "refresh", "path": ["data", "accountingData"]},
"simpleMode": False,
}, "position": {"x": 500, "y": 0}},
],
"connections": [
{"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0},
{"source": "refresh", "sourcePort": 0, "target": "analyse", "targetPort": 0},
],
},
},
{
"id": "trustee-kpi-dashboard",
"label": t("KPI-Dashboard", context="UI"),
"description": "Kennzahlen berechnen und visualisieren",
"tags": ["feature:trustee", "template:trustee-kpi-dashboard"],
"graph": _buildAnalysisWorkflowGraph(
"Erstelle ein KPI-Dashboard basierend auf den aktuellen Buchhaltungsdaten. Berechne und visualisiere:\n"
"1. Bruttogewinn und Bruttogewinnmarge\n"
"2. EBIT (Betriebsergebnis)\n"
"3. Gewinnmarge (Reingewinn / Umsatz)\n"
"4. Eigenkapitalquote und Check auf hälftigen Kapitalverlust (OR Art. 725)\n"
"5. Liquiditätsgrad 1-3 (Cash Ratio, Quick Ratio, Current Ratio)\n"
"6. Überschuldungs-Check\n\n"
"Erstelle für jede Kennzahl einen kurzen Kommentar (gut/kritisch/Handlungsbedarf). "
"Erstelle mindestens 2 Charts: ein Übersichts-Chart der Margen und ein Liquiditäts-Chart."
),
},
{
"id": "trustee-cashflow",
"label": t("Cashflow-Rechnung", context="UI"),
"description": "Cashflow berechnen und analysieren",
"tags": ["feature:trustee", "template:trustee-cashflow"],
"graph": _buildAnalysisWorkflowGraph(
"Erstelle eine Cashflow-Rechnung basierend auf den aktuellen Buchhaltungsdaten:\n"
"1. Operativer Cashflow: Starte vom Reingewinn, bereinige um nicht-cash-wirksame Positionen\n"
"2. Investitions-Cashflow: Investitionen in Sachanlagen, Finanzanlagen\n"
"3. Finanzierungs-Cashflow: Darlehensaufnahmen/-rückzahlungen, Dividenden, Kapitalerhöhungen\n"
"4. Netto-Cashflow und Veränderung der liquiden Mittel\n\n"
"Warne bei kritischen Werten. Erstelle ein Wasserfall-Chart oder gestapeltes Balkendiagramm."
),
},
{
"id": "trustee-forecast",
"label": t("Prognose erstellen", context="UI"),
"description": "Trend-Analyse und Prognose der nächsten Monate",
"tags": ["feature:trustee", "template:trustee-forecast"],
"graph": _buildAnalysisWorkflowGraph(
"Erstelle eine Finanzprognose basierend auf den historischen Buchhaltungsdaten:\n"
"1. Analysiere die Umsatz- und Aufwandsentwicklung der letzten 6 Monate\n"
"2. Identifiziere Trends und Saisonalitäten\n"
"3. Prognostiziere Umsatz, Aufwand und Gewinn für die nächsten 3 Monate\n"
"4. Erstelle ein Chart mit Ist-Werten und Prognose-Korridor\n"
"5. Markiere Risiken\n\n"
"Nutze eine einfache lineare Extrapolation mit Saisonalitätskorrektur wo sinnvoll."
),
},
{
"id": "trustee-year-end-check",
"label": t("Jahresabschluss prüfen", context="UI"),
"description": "Automatische Prüfungen für den Jahresabschluss",
"tags": ["feature:trustee", "template:trustee-year-end-check"],
"graph": _buildAnalysisWorkflowGraph(
"Führe eine automatische Jahresabschluss-Prüfung durch:\n"
"1. Saldovalidierung: Prüfe alle Bilanzkonten auf Plausibilität\n"
"2. Vorjahresvergleich: Vergleiche Bilanz- und ER-Positionen mit dem Vorjahr, markiere Abweichungen >20%\n"
"3. Abgrenzungen: Identifiziere potenzielle transitorische Aktiven/Passiven\n"
"4. Gesetzliche Prüfungen: Hälftiger Kapitalverlust (OR 725), Überschuldung, Mindestkapital\n"
"5. MWST-Plausibilisierung: Vorsteuer vs. geschätzter Aufwand, Umsatzsteuer vs. Umsatz\n\n"
"Erstelle eine Checkliste mit Status (OK / Warnung / Kritisch) pro Prüfpunkt."
),
},
]
TEMPLATE_ROLES = [
{
"roleLabel": "trustee-viewer",
"description": "Treuhand-Betrachter - Treuhand-Daten einsehen (nur lesen)",
"accessRules": [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.data-tables", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
],
},
{
"roleLabel": "trustee-user",
"description": "Treuhand-Benutzer - Eigene Treuhand-Daten erstellen und verwalten",
"accessRules": [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.data-tables", "view": True},
{"context": "UI", "item": "ui.feature.trustee.import-process", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
],
},
{
"roleLabel": "trustee-admin",
"description": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen",
"accessRules": [
{"context": "UI", "item": None, "view": True},
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
{"context": "RESOURCE", "item": "resource.feature.trustee.instance-roles.manage", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.manage", "view": True},
],
},
{
"roleLabel": "trustee-accountant",
"description": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten",
"accessRules": [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.data-tables", "view": True},
{"context": "UI", "item": "ui.feature.trustee.analyse", "view": True},
{"context": "UI", "item": "ui.feature.trustee.abschluss", "view": True},
{"context": "UI", "item": "ui.feature.trustee.settings", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
{"context": "RESOURCE", "item": "resource.feature.trustee.accounting.sync", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.accounting.view", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True},
],
},
{
"roleLabel": "trustee-client",
"description": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen",
"accessRules": [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.data-tables", "view": True},
{"context": "UI", "item": "ui.feature.trustee.import-process", "view": True},
{"context": "DATA", "item": "data.feature.trustee.TrusteePosition", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
{"context": "DATA", "item": "data.feature.trustee.TrusteeDocument", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
],
},
]
def getFeatureDefinition() -> Dict[str, Any]:
"""Return the feature definition for registration."""
return {
"code": FEATURE_CODE,
"label": FEATURE_LABEL,
"icon": FEATURE_ICON
}
def getUiObjects() -> List[Dict[str, Any]]:
"""Return UI objects for RBAC catalog registration."""
return UI_OBJECTS
def getResourceObjects() -> List[Dict[str, Any]]:
"""Return resource objects for RBAC catalog registration."""
return RESOURCE_OBJECTS
def getTemplateRoles() -> List[Dict[str, Any]]:
"""Return template roles for this feature."""
return TEMPLATE_ROLES
def getTemplateWorkflows() -> List[Dict[str, Any]]:
"""Return template workflow definitions for bootstrap on instance creation."""
return TEMPLATE_WORKFLOWS
def getQuickActions() -> List[Dict[str, Any]]:
"""Return quick action definitions for the Trustee dashboard."""
return QUICK_ACTIONS
def getQuickActionCategories() -> List[Dict[str, Any]]:
"""Return quick action category definitions."""
return QUICK_ACTION_CATEGORIES
def getDataObjects() -> List[Dict[str, Any]]:
"""Return DATA objects for RBAC catalog registration."""
return DATA_OBJECTS
# ---------------------------------------------------------------------------
# Feature Data Sub-Agent — domain hints
# ---------------------------------------------------------------------------
# Appended to the sub-agent's system prompt so it understands the Swiss
# KMU chart-of-accounts conventions, the period-month semantics of
# TrusteeDataAccountBalance, and the canonical query patterns. Without this
# the agent invents wrong patterns: e.g. SUMming closingBalance across
# years/months (closingBalance is already a balance) or guessing that 5400
# (Materialaufwand) is a bank account.
_AGENT_DOMAIN_HINTS = """\
TRUSTEE DOMAIN HINTS (Swiss KMU accounting):
CHART OF ACCOUNTS — account number prefixes (KMU-Kontenrahmen):
1xxx = Aktiven (assets)
100x Kasse / cash on hand (e.g. 1000 Hauptkasse)
102x Bank / Post (e.g. 1020 ZKB, 1021 PostFinance, 1025 UBS)
105x Wertschriften
11xx Forderungen aus Lieferungen und Leistungen (receivables)
14xx Anlagevermögen (fixed assets)
2xxx = Passiven (liabilities + equity)
20xx Verbindlichkeiten (payables)
28xx Eigenkapital (equity)
3xxx = Ertrag (revenue)
4xxx = Material-/Warenaufwand
5xxx = Personalaufwand
6xxx = übriger betrieblicher Aufwand
8xxx = ausserordentliches / betriebsfremdes Ergebnis
9xxx = Abschluss
TrusteeDataAccount.accountType is one of: asset / liability / revenue /
expense / closing (derived from the first digit when the source system
doesn't provide it).
"Bankkonten" = accountNumber LIKE '102%'.
"Kassenkonten" = accountNumber LIKE '100%'.
"Liquide Mittel / Cash" = accountNumber LIKE '10%'.
PERIOD CONVENTION on TrusteeDataAccountBalance:
periodMonth = 0 → annual total for periodYear (use this for
"Saldo per 31.12.YYYY" / "Stand Jahresende")
periodMonth = 1..12 → monthly snapshot (use the month the question
refers to, e.g. "per Ende März" → periodMonth=3)
closingBalance is the balance AT THE END of the period; openingBalance
is the balance AT THE START.
CANONICAL QUERY PATTERNS:
1) "Banksaldo per 31.12.2025" (single tool call, no aggregate):
queryTable(
tableName="TrusteeDataAccountBalance",
filters=[
{"field": "periodYear", "op": "=", "value": 2025},
{"field": "periodMonth", "op": "=", "value": 0},
{"field": "accountNumber", "op": "LIKE", "value": "102%"}
],
fields=["accountNumber", "closingBalance", "currency"]
)
→ return the rows as-is. Sum them ONLY in the final answer if the user
asked for a total across banks; otherwise list per account.
2) "Saldo Konto 1020 per Ende 2024":
queryTable(
tableName="TrusteeDataAccountBalance",
filters=[
{"field": "accountNumber", "op": "=", "value": "1020"},
{"field": "periodYear", "op": "=", "value": 2024},
{"field": "periodMonth", "op": "=", "value": 0}
],
fields=["closingBalance", "currency"]
)
3) "Welche Konten gehören zu welchem Typ?" — query TrusteeDataAccount.
4) "Buchungen im März 2025" — bookingDate is a UTC unix-seconds float on
TrusteeDataJournalEntry. Convert: 2025-03-01 → 1740787200.0,
2025-04-01 → 1743465600.0, then filter '>= 1740787200.0' AND
'< 1743465600.0'. NEVER compare bookingDate against ISO strings.
ANTI-PATTERNS (do NOT do this):
- aggregateTable(SUM, closingBalance, GROUP BY accountNumber) without
period filters — closingBalance is already a balance per period; summing
multiple periods produces nonsense (e.g. 7 years × 13 periods = ~90×
the real saldo).
- Using debitTotal/creditTotal as a balance — those are turnovers, not
balances. The balance is closingBalance.
- Picking the top-N accounts by SUM and assuming they are "the most
important". Account 5400 (Materialaufwand) and 3434 (Erlöskorrekturen)
are NOT bank accounts.
"""
def getAgentDomainHints() -> str:
"""Return Trustee-specific guidance for the Feature Data Sub-Agent.
The text is appended verbatim to the sub-agent's system prompt by
``featureDataAgent._buildSchemaContext``. Keep it concise and
pattern-driven — every line costs tokens on every sub-agent call.
"""
return _AGENT_DOMAIN_HINTS
def registerFeature(catalogService) -> bool:
"""
Register this feature's RBAC objects in the catalog.
Args:
catalogService: The RBAC catalog service instance
Returns:
True if registration was successful
"""
try:
# Register UI objects
for uiObj in UI_OBJECTS:
catalogService.registerUiObject(
featureCode=FEATURE_CODE,
objectKey=uiObj["objectKey"],
label=uiObj["label"],
meta=uiObj.get("meta")
)
# Register Resource objects
for resObj in RESOURCE_OBJECTS:
catalogService.registerResourceObject(
featureCode=FEATURE_CODE,
objectKey=resObj["objectKey"],
label=resObj["label"],
meta=resObj.get("meta")
)
# Register DATA objects (tables/entities)
for dataObj in DATA_OBJECTS:
catalogService.registerDataObject(
featureCode=FEATURE_CODE,
objectKey=dataObj["objectKey"],
label=dataObj["label"],
meta=dataObj.get("meta")
)
# Sync template roles to database (with AccessRules)
_syncTemplateRolesToDb()
logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI, {len(RESOURCE_OBJECTS)} resource, {len(DATA_OBJECTS)} data objects")
return True
except Exception as e:
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
return False
def _syncTemplateRolesToDb() -> int:
"""
Sync template roles and their AccessRules to the database.
Creates global template roles (mandateId=None) if they don't exist.
Returns:
Number of roles created/updated
"""
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
from modules.datamodels.datamodelUtils import coerce_text_multilingual
rootInterface = getRootInterface()
# Get existing template roles for this feature (Pydantic models)
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
templateRoles = [r for r in existingRoles if r.mandateId is None]
existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
createdCount = 0
for roleTemplate in TEMPLATE_ROLES:
roleLabel = roleTemplate["roleLabel"]
if roleLabel in existingRoleLabels:
roleId = existingRoleLabels[roleLabel]
# Ensure AccessRules exist for this role
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
else:
# Create new template role
newRole = Role(
roleLabel=roleLabel,
description=coerce_text_multilingual(roleTemplate.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None, # Global template
featureInstanceId=None,
isSystemRole=False
)
createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
roleId = createdRole.get("id")
# Create AccessRules for this role
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
createdCount += 1
if createdCount > 0:
logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
# Repair instance-specific roles that are missing AccessRules
_repairInstanceRolesAccessRules(rootInterface, existingRoleLabels)
return createdCount
except Exception as e:
logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
return 0
def _repairInstanceRolesAccessRules(rootInterface, templateRoleLabels: Dict[str, str]) -> int:
"""
Repair instance-specific roles by copying AccessRules from their template roles.
This ensures instance roles created before AccessRules were defined get updated.
Args:
rootInterface: Root interface instance
templateRoleLabels: Dict mapping roleLabel to template role ID
Returns:
Number of instance roles repaired
"""
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
repairedCount = 0
# Get all instance-specific roles for this feature (Pydantic models)
allRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
instanceRoles = [r for r in allRoles if r.mandateId is not None]
for instanceRole in instanceRoles:
roleLabel = instanceRole.roleLabel
instanceRoleId = str(instanceRole.id)
# Find matching template role
templateRoleId = templateRoleLabels.get(roleLabel)
if not templateRoleId:
continue
# Check if instance role has AccessRules (Pydantic models)
existingRules = rootInterface.getAccessRulesByRole(instanceRoleId)
if existingRules:
continue # Already has rules, skip
# Copy AccessRules from template role (Pydantic models)
templateRules = rootInterface.getAccessRulesByRole(templateRoleId)
if not templateRules:
continue # Template has no rules
for rule in templateRules:
newRule = AccessRule(
roleId=instanceRoleId,
context=rule.context,
item=rule.item,
view=rule.view if rule.view else False,
read=rule.read,
create=rule.create,
update=rule.update,
delete=rule.delete,
)
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
logger.info(f"Repaired instance role '{roleLabel}' (ID: {instanceRoleId}): copied {len(templateRules)} AccessRules from template")
repairedCount += 1
if repairedCount > 0:
logger.info(f"Feature '{FEATURE_CODE}': Repaired {repairedCount} instance roles with missing AccessRules")
return repairedCount
def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
"""
Ensure AccessRules exist for a role based on templates.
Args:
rootInterface: Root interface instance
roleId: Role ID
ruleTemplates: List of rule templates
Returns:
Number of rules created
"""
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
# Get existing rules for this role (Pydantic models)
existingRules = rootInterface.getAccessRulesByRole(roleId)
# Create a set of existing rule signatures to avoid duplicates
# IMPORTANT: Use .value for enum comparison, not str() which gives "AccessRuleContext.DATA" in Python 3.11+
existingSignatures = set()
for rule in existingRules:
sig = (rule.context.value if rule.context else None, rule.item)
existingSignatures.add(sig)
createdCount = 0
for template in ruleTemplates:
context = template.get("context", "UI")
item = template.get("item")
sig = (context, item)
if sig in existingSignatures:
continue
# Map context string to enum
if context == "UI":
contextEnum = AccessRuleContext.UI
elif context == "DATA":
contextEnum = AccessRuleContext.DATA
elif context == "RESOURCE":
contextEnum = AccessRuleContext.RESOURCE
else:
contextEnum = context
newRule = AccessRule(
roleId=roleId,
context=contextEnum,
item=item,
view=template.get("view", False),
read=template.get("read"),
create=template.get("create"),
update=template.get("update"),
delete=template.get("delete"),
)
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
createdCount += 1
if createdCount > 0:
logger.debug(f"Created {createdCount} AccessRules for role {roleId}")
return createdCount