trustee budget fix

This commit is contained in:
ValueOn AG 2026-05-19 17:38:18 +02:00
parent 1ed462ad13
commit 9773c00bca
2 changed files with 74 additions and 19 deletions

View file

@ -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, "

View file

@ -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)