wiki/c-work/4-done/2026-04-trustee-account-balances-import.md
2026-06-02 09:42:12 +02:00

16 KiB

Trustee: Echte Schlusssalden aus Buchhaltungssystem importieren

Beschreibung und Kontext

Bug-Report Kunde (PROD-Mandant BuHa SoHa, Connector RMA): Fragt man Nyla nach dem Banksaldo per 31.12.2025 (Konto 1020), antwortet sie mit CHF 79'939.86. Der echte Schlusssaldo ist CHF 48'507.41. Auch alle daraus abgeleiteten Auswertungen (Geldflussrechnung, Umsatz-Diagramme, Plausichecks) sind dadurch falsch.

Root cause: Die Tabelle TrusteeDataAccountBalance wird nicht aus dem Buchhaltungssystem importiert, sondern in accountingDataSync._persistBalances lokal aus den Journalzeilen aggregiert -- mit drei eigenstaendigen Fehlern:

  1. closingBalance = debit - credit nur aus den Buchungen der jeweiligen Periode (Monat oder Jahres-Bucket). Das ist die Periodenbewegung, kein Schlusssaldo. Schon innerhalb desselben Jahres summieren sich die Vormonate nicht auf den Stichtag.
  2. openingBalance ist hart auf 0.0 kodiert -- der Saldovortrag aus den Vorjahren / vor dem Importfenster fehlt komplett.
  3. Auch der Jahres-Bucket (accNo, year, 0) laeuft durch dieselbe Formel, d.h. der "Jahres-Schlusssaldo" ist eigentlich nur die Jahres-Bewegung.

Architekturloch: BaseAccountingConnector kennt schlicht keine getAccountBalances-Methode. Der Defekt trifft alle drei aktuell registrierten Connectoren (RMA / Bexio / Abacus).

Warum jetzt: Das Trustee-Feature steht im aktiven Roll-out beim Pilot-Kunden (BuHa SoHa, Customer Trustee Demo). Falsche Saldenzahlen unterminieren das Vertrauen in die ganze Plattform und blockieren saemtliche AI-Auswertungen, die auf Salden aufbauen (Geldfluss, Plausichecks, Bilanz-/ER-Vergleiche).

Risiko bei Verzicht: Jede Frage an den Agenten zu Salden / Bilanzpositionen liefert subtile Falschwerte (kein offensichtlicher Crash, sondern stillschweigend falsche Zahlen). Auditor-Risiko.

Fokus und kritische Details

  • RMA hat einen dedizierten Saldo-Endpunkt (GET /gl/saldo mit accno, from, to, bookkeeping_main_curr, exclude_yearend_bookings). Liefert pro Konto den echten, durch RMA berechneten Schlusssaldo unter Beruecksichtigung von Vorjahres-Vortraegen und Jahresabschluss-Buchungen. Quelle der Wahrheit ist immer der Connector, nicht eine lokale Berechnung -- Letztere kann Vortraege und Jahresabschluss-Offsets nicht rekonstruieren, wenn das Importfenster nur einen Teil der Historie enthaelt.
  • Bexio + Abacus haben keinen entsprechenden Endpunkt in dem Detailgrad. Bexio liefert GET /3.0/accounting/journal (alle Journal-Eintraege mit debit_account_id, credit_account_id, amount, date); Abacus GET GeneralJournalEntries (OData V4). Dort muessen wir lokal kumulativ rechnen und den Vorjahres-Vortrag explizit aus den Journal-Daten ableiten (Eroeffnungsbuchungen am Geschaeftsjahres-Anfang bzw. Abschluss-Buchungen am Vorjahres-Ende).
  • Datenmodell TrusteeDataAccountBalance bleibt unveraendert -- die Felder openingBalance, debitTotal, creditTotal, closingBalance sind bereits vorhanden. Der Bug ist rein in der Befuellung; kein DB-Migration noetig (_bulkClear + _bulkCreate ueberschreibt jeden Sync ohnehin).
  • Konsumenten der Tabelle: mainTrustee.DATA_OBJECTS, routeFeatureTrustee (data-tables UI), methodTrustee/actions/queryData (entity=balances, mode=raw/aggregate), AI-Agent ueber /api/automation2/catalog. Keine Schema-Aenderung, also kein Frontend-Impact.
  • Persistenz-Performance: Wir reden von 200--500 Konten x 13 Perioden (Annual + 12 Monate) = max ~6500 Rows pro Sync. Der bestehende _bulkCreate-Pfad ist dafuer dimensioniert (vgl. accountingDataSync.py Header-Kommentar zur Asyncio-Architektur).
  • Connector-Fallback-Vertrag: Wenn getAccountBalances [] zurueckgibt und keine Exception wirft, gilt das als "Connector kann das nicht / hat nichts geliefert" -- in dem Fall faellt der Sync auf die korrigierte lokale Aggregation zurueck. Wirft der Connector eine Exception, wird sie in summary["errors"] aufgenommen und der Sync laeuft weiter (gleiches Pattern wie bei den uebrigen Phasen).
  • i18n: Keine neuen User-facing Strings.
  • Naming: Funktionen _-Prefix fuer intern (_buildBalanceUrl, _aggregateBalancesFromLines); camelCase ueberall (Python + TS).
  • Logging: Keine Emojis (vgl. .cursor/rules/python-coding.mdc).

Ziel und Nicht-Ziele

  • Ziel: TrusteeDataAccountBalance enthaelt echte Salden aus dem Buchhaltungssystem (RMA, soweit verfuegbar) bzw. eine korrekt kumulierte lokale Berechnung (Bexio, Abacus). closingBalance = Stichtagsbestand, openingBalance = Stand zu Periodenbeginn, inkl. Vortraege.
  • Ziel: Erweiterung von BaseAccountingConnector um optionale Methode getAccountBalances(...). Default-Implementierung gibt [] zurueck (keine externe Quelle) -- Sync faellt auf den korrigierten Fallback zurueck.
  • Ziel: RMA-Connector implementiert echte Saldenabfrage; Bexio + Abacus bekommen entweder eine voll funktionsfaehige getAccountBalances- Implementierung oder, falls nicht moeglich, einen detaillierten Code- Kommentar mit Endpunkt-Recherche und Skizze des fehlenden Stuecks (User-Direktive: "besser gleich umsetzen bei allen 3").
  • Ziel: Korrekter Fallback in _persistBalances: Wenn der Connector nichts liefert, kumuliere Journal-Lines pro Konto chronologisch und fuelle openingBalance aus den Vorjahresbuchungen (oder 0, wenn das Importfenster den Account-Lifecycle abdeckt).
  • Ziel: Unit-Test pro Connector + Integrationstest, der den BuHa-SoHa- Fall reproduziert (Konto mit Vortrag, mehreren Monaten Bewegungen, korrekter Schlusssaldo Ende Dezember).
  • Explizit NICHT: Schema- oder DB-Migration. Felder existieren bereits; jeder Sync schreibt die Tabelle ohnehin neu (_bulkClear).
  • Explizit NICHT: Aenderung am Frontend (data-tables-Seite). Die Tabelle bekommt nur korrekte Werte -- die Spalten sind bereits da.
  • Explizit NICHT: Aenderung am AI-Agent / queryData-Action. Die Action liefert weiter entity="balances" aus derselben Tabelle.
  • Explizit NICHT: Saldo-Logik ausserhalb von Trustee (Sanctions-Plattform etc.).

Betroffene Module

  • Gateway:
    • platform-core/modules/features/trustee/accounting/accountingConnectorBase.py -- neue Datenklasse AccountingPeriodBalance, neue optionale Methode getAccountBalances.
    • platform-core/modules/features/trustee/accounting/connectors/accountingConnectorRma.py -- getAccountBalances via GET /gl/saldo. Annual-Bucket + 12 Monats- Buckets, jeweils kumulativ bis Periodenende. Opening-Balance = Saldo per Periodenstart - 1 Tag (zweiter API-Call) oder via vorheriger Periode.
    • platform-core/modules/features/trustee/accounting/connectors/accountingConnectorBexio.py -- getAccountBalances aggregiert lokal aus GET /3.0/accounting/journal (filtert per from/to-Param) -- Bexio bietet keine Saldoliste.
    • platform-core/modules/features/trustee/accounting/connectors/accountingConnectorAbacus.py -- getAccountBalances aggregiert lokal aus GET GeneralJournalEntries (OData-Filter JournalDate ge ... and le ...). Mit Schemahinweis im Code, falls Abacus-Instanz die Entity AccountBalances (offiziell vorhanden, aber Verfuegbarkeit instanzabhaengig) ausliefern kann.
    • platform-core/modules/features/trustee/accounting/accountingDataSync.py -- Phase 4 (_persistBalances) umbauen: zuerst Connector fragen; bei leerer / fehlerhafter Antwort den korrigierten lokalen Fallback nutzen.
    • platform-core/modules/workflows/methods/methodTrustee/actions/refreshAccountingData.py -- nur Doc-String anpassen (verweist auf neuen Datenfluss).
  • Frontend: keine Aenderung.
  • DB-Migration: nein.
  • Andere Komponenten: keine.

Entscheidungen

Datum Entscheidung Begruendung
2026-04-25 Connector-Werte haben Vorrang, lokale Aggregation ist Fallback. Vorjahres-Vortraege und Jahresabschluss-Offsets kann nur das Buchhaltungssystem zuverlaessig liefern; lokale Aggregation aus dem Importfenster bleibt naehrungsweise.
2026-04-25 Per Konto + Periode 13 Buckets (Annual + 12 Monate). Jeder Bucket = Schlusssaldo per Periodenende. Konsistent mit bestehendem Datenmodell und der BuHa SoHa-Tabelle (Monat 12 = Dezember-Stichtag). Annual-Bucket = Schlusssaldo per Geschaeftsjahres-Ende.
2026-04-25 RMA: Echte Saldenabfrage via /gl/saldo pro Periode (Annual + 12 Monate). 13 sequenzielle GETs pro Sync. RMA-API bietet pro Aufruf nur einen Stichtag; mehrere Aufrufe sind unkritisch (Sync laeuft im Hintergrund, schon heute 30+s).
2026-04-25 Bexio: Aggregation aus /3.0/accounting/journal (kein dedizierter Saldo-Endpunkt vorhanden). Bexio Journal-API liefert alle benoetigten Felder; lokale Kumulation ist hier exakt, weil der Bexio-Mandant typischerweise im Importfenster komplett liegt.
2026-04-25 Abacus: Aggregation aus GeneralJournalEntries. Falls Instanz AccountBalances-Entity hat, kann sie spaeter bevorzugt werden (Code-Kommentar fuer Folge-Iteration). OData-Schema variiert pro Abacus-Instanz; sichere Default-Strategie ist Aggregation.
2026-04-25 AccountingPeriodBalance-Pydantic-Modell (statt dict) fuer Connector-Output. Konsistent mit AccountingChart, AccountingBooking; Typsicherheit + Pydantic-Validierung.
2026-04-25 getAccountBalances ist OPTIONAL (Default: []). Erlaubt graduelles Rollout pro Connector ohne Breaking Change; bestehender Fallback bleibt verfuegbar.
2026-04-25 Lokaler Fallback rechnet jetzt korrekt kumulativ pro Konto, sortiert Journal-Lines nach Buchungsdatum, und propagiert den Saldo Monat-fuer-Monat. openingBalance der ersten Periode = 0 (besser nichts als ein erfundener Wert). Vortrags-Berechnung ohne Connector-Daten ist nicht zuverlaessig; explizites openingBalance=0 + Kommentar im UI/Doku ist ehrlicher.

Umsetzungs-Checkliste

Backend

  • AccountingPeriodBalance (Pydantic) in accountingConnectorBase.py: Felder accountNumber, periodYear, periodMonth, openingBalance, debitTotal, creditTotal, closingBalance, currency, asOfDate.
  • BaseAccountingConnector.getAccountBalances(config, year, accountNumbers=None) mit Default-Implementierung return []. Doc-String dokumentiert Vertrag (Annual-Bucket = periodMonth=0).
  • RMA: getAccountBalances via GET /gl/saldo. Pro Konto-Filter accno (z.B. xxxx) und Datum to=YYYY-MM-DD, einmal pro Periode (Annual + 12 Monate). Monatlicher openingBalance = Schlusssaldo Vormonat. Fehler / leere Antwort -> Konto skippen (nicht Sync abbrechen).
  • Bexio: getAccountBalances ueber _loadRawAccounts (Account-Map) + GET /3.0/accounting/journal?from=YYYY-MM-DD&to=YYYY-MM-DD. Lokale Kumulation (debit - credit) pro Konto pro Monat. Jahres-Bucket separat.
  • Abacus: getAccountBalances ueber GET GeneralJournalEntries mit OData-Filter; gleiche Aggregations-Logik. Code-Kommentar mit Hinweis auf optionale AccountBalances-Entity (instanzabhaengig).
  • accountingDataSync._persistBalances umbauen:
    1. await connector.getAccountBalances(plainConfig, year=...) aufrufen (Jahre aus dateFrom/dateTo ableiten -- kann ueber mehrere Jahre gehen).
    2. Bei nicht-leerer Antwort: direkt persistieren.
    3. Sonst Fallback _aggregateBalancesFromLines(...) -- korrekt kumulativ.
  • Logging: pro Phase eindeutige Log-Zeile mit Connector-Name + Source (buha / local-fallback) + Zeilenzahl.

Tests

  • platform-core/tests/unit/features/trustee/test_accountingConnectorRma_balances.py: getAccountBalances mit gemocktem /gl/saldo (17 Tests: BuHa-SoHa-Szenario, ER-Reset, Parser, Helpers).
  • platform-core/tests/unit/features/trustee/test_accountingConnectorBexio_balances.py: lokale Aggregation aus gemockter Journal-Liste (8 Tests: BS cumulative, carry-over, ER-Reset).
  • platform-core/tests/unit/features/trustee/test_accountingConnectorAbacus_balances.py: OData-Aggregation (6 Tests: BS carry-over, ER-Reset).
  • platform-core/tests/unit/features/trustee/test_accountingDataSync_balances.py: End-to-End mit FakeDb (11 Tests: Connector-Path verbatim persist, Local-Fallback kumulative BS/ER-Berechnung, _resolveBalanceYears, _isIncomeStatementAccount).

Wiki

  • Plan: dieses Dokument von 1-plan/ -> 4-done/ verschoben.
  • wiki/c-work/_CHANGELOG.md: 2 Zeilen (fix + test).
  • wiki/b-reference/platform-core/features/trustee.md: Abschnitt "Saldenliste (TrusteeDataAccountBalance)" mit Datenquelle + Fallback ergaenzt; Tests-Tabelle erweitert; Link auf dieses Dokument.
  • lastReviewed: 2026-04-25 + verifiedAgainst: ... + Account-Balance-Import aktualisiert.

Akzeptanzkriterien

# Kriterium (Given-When-Then) Prio
1 Given Mandant BuHa SoHa mit RMA-Connector und Konto 1020 (echter Schlusssaldo per 31.12.2025 = 48'507.41), When der Sync laeuft, Then enthaelt TrusteeDataAccountBalance fuer accountNumber='1020', periodYear=2025, periodMonth=12 einen closingBalance = 48'507.41 (Toleranz +/- 0.01 CHF). must
2 Given derselbe Sync, When man die Annual-Zeile abfragt (periodMonth=0), Then closingBalance = Schlusssaldo per Geschaeftsjahres-Ende (= 48'507.41 bei Kalenderjahr-Mandant). must
3 Given derselbe Sync, When man die Zeile fuer periodMonth=11 abfragt, Then closingBalance = Stand per 30.11.2025 (kumulativ, NICHT November-Bewegung). must
4 Given Bexio-Connector mit lokal aggregiertem Saldo, When alle Journal-Eintraege im Importfenster liegen und kein Vortrag existiert, Then closingBalance einer Periode = Summe aller debits - Summe aller credits bis Periodenende fuer dieses Konto. must
5 Given Connector wirft Exception in getAccountBalances, When der Sync laeuft, Then erscheint die Fehlermeldung in summary["errors"], der Sync laeuft weiter und der Fallback befuellt die Tabelle aus den Journalzeilen. must
6 Given Frage an AI-Agent "Wie hoch war der Saldo Konto 1020 per 31.12.2025?", When queryData(entity="balances", mode="raw") aufgerufen wird, Then enthaelt das Resultat den Datensatz mit closingBalance=48507.41. must
7 Given Sync-Lauf, When er fertig ist, Then enthaelt das Log eine Zeile Persisted N balances for <id> in Xs (source=buha|local-fallback). should

Testplan

ID AC Art Automatisiert Repo-Pfad Status
T1 1, 2, 3 unit ja platform-core/tests/unit/features/trustee/test_accountingConnectorRma_balances.py 17/17 passed
T2 4 unit ja platform-core/tests/unit/features/trustee/test_accountingConnectorBexio_balances.py 8/8 passed
T3 4 unit ja platform-core/tests/unit/features/trustee/test_accountingConnectorAbacus_balances.py 6/6 passed
T4 1-3, 5 integration ja platform-core/tests/unit/features/trustee/test_accountingDataSync_balances.py (FakeDb) 11/11 passed
T5 1, 2, 6 manual nein PROD-Mandant BuHa SoHa, vorher/nachher Vergleich pending (nach Deployment)

Abschluss

  • wiki/b-reference/platform-core/features/trustee.md aktualisiert (Quelle der Saldenliste).
  • wiki/TOPICS.md -- kein neuer Eintrag noetig (faellt unter Trustee).
  • Dieses Dokument in wiki/c-work/4-done/ verschoben.