# 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