From 9773c00bca3b48216c86adb6e733ae1060f37851 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 19 May 2026 17:38:18 +0200 Subject: [PATCH] trustee budget fix --- modules/features/trustee/mainTrustee.py | 10 ++- .../actions/refreshAccountingData.py | 83 +++++++++++++++---- 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/modules/features/trustee/mainTrustee.py b/modules/features/trustee/mainTrustee.py index b3f7cdcf..41903211 100644 --- a/modules/features/trustee/mainTrustee.py +++ b/modules/features/trustee/mainTrustee.py @@ -484,8 +484,14 @@ TEMPLATE_WORKFLOWS = [ "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" + "DATENQUELLEN:\n" + "- SOLL (Budget): Aus der uebergebenen Budget-Datei (Excel).\n" + "- IST (Buchhaltung): Verwende AUSSCHLIESSLICH das Feld " + "\"closingBalance\" aus \"accountSummary\" im Kontext-JSON. " + "Dort steht pro Konto GENAU EIN Ist-Wert (Jahresabschluss-Saldo). " + "Fuer Quartals-Budgets stehen zusaetzlich Q1/Q2/Q3/Q4-Felder bereit. " + "SUMMIERE NIEMALS mehrere Zeilen oder Journal-Eintraege auf -- der " + "closingBalance in accountSummary ist bereits der korrekte Ist-Wert.\n\n" "WICHTIG: Erstelle KEINEN separaten Chart pro Konto. Nur EIN " "Uebersichts-Chart ueber alle Konten ist gewuenscht.\n\n" "Hinweis: Das documentTheme ist 'finance'. Wenn du ein Dokument erstellst, " diff --git a/modules/workflows/methods/methodTrustee/actions/refreshAccountingData.py b/modules/workflows/methods/methodTrustee/actions/refreshAccountingData.py index 6ff5641c..0d6e737c 100644 --- a/modules/workflows/methods/methodTrustee/actions/refreshAccountingData.py +++ b/modules/workflows/methods/methodTrustee/actions/refreshAccountingData.py @@ -38,6 +38,52 @@ def _tsToIso(ts) -> Optional[str]: _SYNC_THRESHOLD_SECONDS = 3600 +def _buildAccountSummary(accountMap: Dict[str, dict], balances: list, year: int) -> list: + """Aggregate balance records into one row per account for *year*. + + For each account the annual balance record (``periodMonth == 0``) of + *year* is preferred. If that row is missing, we also check the + previous year's annual record so that YTD carry-forwards are visible. + Additionally, quarterly closing balances (Q1-Q4) are derived from the + monthly records so the AI can compare against quarterly budgets. + """ + bestClosing: Dict[str, float] = {} + quarterClosing: Dict[str, Dict[str, float]] = {} + + for b in balances: + acct = b.get("accountNumber", "") + bYear = b.get("periodYear", 0) + bMonth = b.get("periodMonth", 0) + closing = b.get("closingBalance", 0) or 0 + + if bYear == year and bMonth == 0: + bestClosing[acct] = closing + + if bYear == year and bMonth in (3, 6, 9, 12): + qLabel = f"Q{bMonth // 3}" + quarterClosing.setdefault(acct, {})[qLabel] = closing + + if acct not in bestClosing and bYear == year - 1 and bMonth == 0: + bestClosing[acct] = closing + + summary = [] + for nr in sorted(accountMap.keys()): + info = accountMap[nr] + row = { + "account": nr, + "label": info.get("label", ""), + "type": info.get("type", ""), + "group": info.get("group", ""), + "closingBalance": round(bestClosing.get(nr, 0), 2), + } + qData = quarterClosing.get(nr, {}) + for q in ("Q1", "Q2", "Q3", "Q4"): + if q in qData: + row[q] = round(qData[q], 2) + summary.append(row) + return summary + + async def refreshAccountingData(self, parameters: Dict[str, Any]) -> ActionResult: """Import/refresh accounting data from the configured external system. @@ -133,7 +179,13 @@ async def refreshAccountingData(self, parameters: Dict[str, Any]) -> ActionResul def _exportAccountingData(trusteeInterface, featureInstanceId: str, dateFrom: str = None, dateTo: str = None) -> str: - """Export accounting data (accounts, balances, journal entries+lines) as compact JSON for downstream AI nodes.""" + """Export accounting data as compact JSON for downstream AI nodes. + + Produces a pre-aggregated ``accountSummary`` (one row per account with + a single *Ist* value) so the AI does not have to navigate thousands of + raw balance records. Raw per-month balances are deliberately omitted to + avoid confusion and reduce payload size. + """ from modules.features.trustee.datamodelFeatureTrustee import ( TrusteeDataAccount, TrusteeDataJournalEntry, @@ -155,17 +207,9 @@ def _exportAccountingData(trusteeInterface, featureInstanceId: str, dateFrom: st } balances = trusteeInterface.db.getRecordset(TrusteeDataAccountBalance, recordFilter=baseFilter) or [] - balanceList = [] - for b in balances: - balanceList.append({ - "account": b.get("accountNumber", ""), - "year": b.get("periodYear", 0), - "month": b.get("periodMonth", 0), - "opening": b.get("openingBalance", 0), - "debit": b.get("debitTotal", 0), - "credit": b.get("creditTotal", 0), - "closing": b.get("closingBalance", 0), - }) + + currentYear = _dt.now(tz=_tz.utc).year + accountSummary = _buildAccountSummary(accountMap, balances, currentYear) entries = trusteeInterface.db.getRecordset(TrusteeDataJournalEntry, recordFilter=baseFilter) or [] fromTs = _isoToTs(dateFrom) @@ -205,21 +249,26 @@ def _exportAccountingData(trusteeInterface, featureInstanceId: str, dateFrom: st }) export = { - "accounts": list(accountMap.values()), - "balances": balanceList, + "accountSummary": accountSummary, "journalLines": lineList, "meta": { "accountCount": len(accountMap), "entryCount": len(entryMap), "lineCount": len(lineList), - "balanceCount": len(balanceList), + "summaryYear": currentYear, "dateFrom": dateFrom, "dateTo": dateTo, + "hint": ( + "accountSummary contains ONE row per account with the " + "current-year closing balance (Ist). Use this for " + "budget comparisons. journalLines lists individual " + "bookings for drill-down." + ), }, } result = json.dumps(export, ensure_ascii=False, default=str) - logger.info("Exported accounting data: %d accounts, %d entries, %d lines, %d balances (%d bytes)", - len(accountMap), len(entryMap), len(lineList), len(balanceList), len(result)) + logger.info("Exported accounting data: %d accounts (summary), %d entries, %d lines (%d bytes)", + len(accountSummary), len(entryMap), len(lineList), len(result)) return result except Exception as e: logger.warning("Could not export accounting data: %s", e)