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:
closingBalance = debit - creditnur 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.openingBalanceist hart auf0.0kodiert -- der Saldovortrag aus den Vorjahren / vor dem Importfenster fehlt komplett.- 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/saldomitaccno,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 mitdebit_account_id,credit_account_id,amount,date); AbacusGET 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
TrusteeDataAccountBalancebleibt unveraendert -- die FelderopeningBalance,debitTotal,creditTotal,closingBalancesind bereits vorhanden. Der Bug ist rein in der Befuellung; kein DB-Migration noetig (_bulkClear+_bulkCreateueberschreibt 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 insummary["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:
TrusteeDataAccountBalanceenthaelt 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
BaseAccountingConnectorum optionale MethodegetAccountBalances(...). 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 fuelleopeningBalanceaus den Vorjahresbuchungen (oder0, 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 weiterentity="balances"aus derselben Tabelle. - Explizit NICHT: Saldo-Logik ausserhalb von Trustee (Sanctions-Plattform etc.).
Betroffene Module
- Gateway:
gateway/modules/features/trustee/accounting/accountingConnectorBase.py-- neue DatenklasseAccountingPeriodBalance, neue optionale MethodegetAccountBalances.gateway/modules/features/trustee/accounting/connectors/accountingConnectorRma.py--getAccountBalancesviaGET /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.gateway/modules/features/trustee/accounting/connectors/accountingConnectorBexio.py--getAccountBalancesaggregiert lokal ausGET /3.0/accounting/journal(filtert perfrom/to-Param) -- Bexio bietet keine Saldoliste.gateway/modules/features/trustee/accounting/connectors/accountingConnectorAbacus.py--getAccountBalancesaggregiert lokal ausGET GeneralJournalEntries(OData-FilterJournalDate ge ... and le ...). Mit Schemahinweis im Code, falls Abacus-Instanz die EntityAccountBalances(offiziell vorhanden, aber Verfuegbarkeit instanzabhaengig) ausliefern kann.gateway/modules/features/trustee/accounting/accountingDataSync.py-- Phase 4 (_persistBalances) umbauen: zuerst Connector fragen; bei leerer / fehlerhafter Antwort den korrigierten lokalen Fallback nutzen.gateway/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) inaccountingConnectorBase.py: FelderaccountNumber,periodYear,periodMonth,openingBalance,debitTotal,creditTotal,closingBalance,currency,asOfDate.BaseAccountingConnector.getAccountBalances(config, year, accountNumbers=None)mit Default-Implementierungreturn []. Doc-String dokumentiert Vertrag (Annual-Bucket =periodMonth=0).- RMA:
getAccountBalancesviaGET /gl/saldo. Pro Konto-Filteraccno(z.B.xxxx) und Datumto=YYYY-MM-DD, einmal pro Periode (Annual + 12 Monate). MonatlicheropeningBalance= Schlusssaldo Vormonat. Fehler / leere Antwort -> Konto skippen (nicht Sync abbrechen). - Bexio:
getAccountBalancesueber_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:
getAccountBalancesueberGET GeneralJournalEntriesmit OData-Filter; gleiche Aggregations-Logik. Code-Kommentar mit Hinweis auf optionaleAccountBalances-Entity (instanzabhaengig). accountingDataSync._persistBalancesumbauen:await connector.getAccountBalances(plainConfig, year=...)aufrufen (Jahre aus dateFrom/dateTo ableiten -- kann ueber mehrere Jahre gehen).- Bei nicht-leerer Antwort: direkt persistieren.
- Sonst Fallback
_aggregateBalancesFromLines(...)-- korrekt kumulativ.
- Logging: pro Phase eindeutige Log-Zeile mit Connector-Name + Source
(
buha/local-fallback) + Zeilenzahl.
Tests
gateway/tests/unit/features/trustee/test_accountingConnectorRma_balances.py:getAccountBalancesmit gemocktem/gl/saldo(17 Tests: BuHa-SoHa-Szenario, ER-Reset, Parser, Helpers).gateway/tests/unit/features/trustee/test_accountingConnectorBexio_balances.py: lokale Aggregation aus gemockter Journal-Liste (8 Tests: BS cumulative, carry-over, ER-Reset).gateway/tests/unit/features/trustee/test_accountingConnectorAbacus_balances.py: OData-Aggregation (6 Tests: BS carry-over, ER-Reset).gateway/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/gateway/features/trustee.md: Abschnitt "Saldenliste (TrusteeDataAccountBalance)" mit Datenquelle + Fallback ergaenzt; Tests-Tabelle erweitert; Link auf dieses Dokument.lastReviewed: 2026-04-25+verifiedAgainst: ... + Account-Balance-Importaktualisiert.
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 | gateway/tests/unit/features/trustee/test_accountingConnectorRma_balances.py | 17/17 passed |
| T2 | 4 | unit | ja | gateway/tests/unit/features/trustee/test_accountingConnectorBexio_balances.py | 8/8 passed |
| T3 | 4 | unit | ja | gateway/tests/unit/features/trustee/test_accountingConnectorAbacus_balances.py | 6/6 passed |
| T4 | 1-3, 5 | integration | ja | gateway/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) |
Links
- Bug-Report (Kunden-E-Mail vom 2026-04-25)
- RMA API Doc Saldo: https://runmyaccountsag.github.io/runmyaccounts-rest-api/Resources/resources.html#general-ledger
- Bexio Journal API: https://docs.bexio.com/#tag/Accounting/operation/listJournal
- Abacus REST API: https://downloads.abacus.ch/fileadmin/ablage/abaconnect/htmlfiles/docs/restapi/abacus_rest_api.html
Abschluss
wiki/b-reference/gateway/features/trustee.mdaktualisiert (Quelle der Saldenliste).wiki/TOPICS.md-- kein neuer Eintrag noetig (faellt unter Trustee).- Dieses Dokument in
wiki/c-work/4-done/verschoben.