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)