227 lines
8.9 KiB
Python
227 lines
8.9 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Refresh accounting data from external system (e.g. Abacus) into local TrusteeData* tables.
|
|
Checks lastSyncAt to avoid redundant imports unless forceRefresh is set.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import time
|
|
from typing import Dict, Any
|
|
|
|
from modules.datamodels.datamodelChat import ActionResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_SYNC_THRESHOLD_SECONDS = 3600
|
|
|
|
|
|
async def refreshAccountingData(self, parameters: Dict[str, Any]) -> ActionResult:
|
|
"""Import/refresh accounting data from the configured external system.
|
|
|
|
If data was synced within the last hour and forceRefresh is not set,
|
|
returns cached counts without triggering an external sync.
|
|
"""
|
|
featureInstanceId = parameters.get("featureInstanceId") or (
|
|
self.services.featureInstanceId if hasattr(self.services, "featureInstanceId") else None
|
|
)
|
|
forceRefresh = parameters.get("forceRefresh", False)
|
|
if isinstance(forceRefresh, str):
|
|
forceRefresh = forceRefresh.lower() in ("true", "1", "yes")
|
|
dateFrom = parameters.get("dateFrom") or None
|
|
dateTo = parameters.get("dateTo") or None
|
|
|
|
if not featureInstanceId:
|
|
return ActionResult.isFailure(error="featureInstanceId is required")
|
|
|
|
try:
|
|
from modules.features.trustee.interfaceFeatureTrustee import getInterface as getTrusteeInterface
|
|
from modules.features.trustee.datamodelFeatureTrustee import (
|
|
TrusteeAccountingConfig,
|
|
TrusteeDataAccount,
|
|
TrusteeDataJournalEntry,
|
|
TrusteeDataJournalLine,
|
|
TrusteeDataContact,
|
|
TrusteeDataAccountBalance,
|
|
)
|
|
|
|
trusteeInterface = getTrusteeInterface(
|
|
self.services.user,
|
|
mandateId=self.services.mandateId,
|
|
featureInstanceId=featureInstanceId,
|
|
)
|
|
|
|
cfgRecords = trusteeInterface.db.getRecordset(
|
|
TrusteeAccountingConfig,
|
|
recordFilter={"featureInstanceId": featureInstanceId, "isActive": True},
|
|
)
|
|
if not cfgRecords:
|
|
return ActionResult.isFailure(error="No active accounting configuration found for this Trustee instance")
|
|
|
|
cfgRecord = cfgRecords[0]
|
|
lastSyncAt = cfgRecord.get("lastSyncAt") or 0
|
|
lastSyncStatus = cfgRecord.get("lastSyncStatus") or ""
|
|
|
|
isFresh = (
|
|
lastSyncAt
|
|
and (time.time() - lastSyncAt) < _SYNC_THRESHOLD_SECONDS
|
|
and lastSyncStatus in ("success", "partial")
|
|
)
|
|
|
|
if isFresh and not forceRefresh:
|
|
counts = _getCachedCounts(trusteeInterface, featureInstanceId)
|
|
counts["synced"] = False
|
|
counts["lastSyncAt"] = lastSyncAt
|
|
counts["lastSyncStatus"] = lastSyncStatus
|
|
counts["message"] = f"Data is fresh (synced {int(time.time() - lastSyncAt)}s ago). Use forceRefresh=true to re-import."
|
|
dataExport = _exportAccountingData(trusteeInterface, featureInstanceId, dateFrom, dateTo)
|
|
return ActionResult.isSuccess(data={
|
|
"summary": counts,
|
|
"accountingData": dataExport,
|
|
})
|
|
|
|
from modules.features.trustee.accounting.accountingDataSync import AccountingDataSync
|
|
|
|
sync = AccountingDataSync(trusteeInterface)
|
|
summary = await sync.importData(
|
|
featureInstanceId=featureInstanceId,
|
|
mandateId=self.services.mandateId,
|
|
dateFrom=dateFrom,
|
|
dateTo=dateTo,
|
|
)
|
|
summary["synced"] = True
|
|
summary.pop("startedAt", None)
|
|
summary.pop("finishedAt", None)
|
|
|
|
try:
|
|
from modules.serviceCenter.services.serviceAgent.coreTools._featureSubAgentTools import clearFeatureQueryCache
|
|
clearFeatureQueryCache(featureInstanceId)
|
|
logger.info("Cleared feature query cache for instance %s after accounting import", featureInstanceId)
|
|
except Exception as cacheErr:
|
|
logger.warning("Could not clear feature query cache: %s", cacheErr)
|
|
|
|
dataExport = _exportAccountingData(trusteeInterface, featureInstanceId, dateFrom, dateTo)
|
|
return ActionResult.isSuccess(data={
|
|
"summary": summary,
|
|
"accountingData": dataExport,
|
|
})
|
|
except Exception as e:
|
|
logger.exception("refreshAccountingData failed")
|
|
return ActionResult.isFailure(error=str(e))
|
|
|
|
|
|
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."""
|
|
from modules.features.trustee.datamodelFeatureTrustee import (
|
|
TrusteeDataAccount,
|
|
TrusteeDataJournalEntry,
|
|
TrusteeDataJournalLine,
|
|
TrusteeDataAccountBalance,
|
|
)
|
|
try:
|
|
baseFilter = {"featureInstanceId": featureInstanceId}
|
|
|
|
accounts = trusteeInterface.db.getRecordset(TrusteeDataAccount, recordFilter=baseFilter) or []
|
|
accountMap = {}
|
|
for a in accounts:
|
|
nr = a.get("accountNumber", "")
|
|
accountMap[nr] = {
|
|
"nr": nr,
|
|
"label": a.get("label", ""),
|
|
"type": a.get("accountType", ""),
|
|
"group": a.get("accountGroup", ""),
|
|
}
|
|
|
|
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),
|
|
})
|
|
|
|
entries = trusteeInterface.db.getRecordset(TrusteeDataJournalEntry, recordFilter=baseFilter) or []
|
|
entryMap = {}
|
|
for e in entries:
|
|
eid = e.get("id", "")
|
|
bDate = e.get("bookingDate", "")
|
|
if dateFrom and bDate and bDate < dateFrom:
|
|
continue
|
|
if dateTo and bDate and bDate > dateTo:
|
|
continue
|
|
entryMap[eid] = {
|
|
"date": bDate,
|
|
"ref": e.get("reference", ""),
|
|
"desc": e.get("description", ""),
|
|
"amount": e.get("totalAmount", 0),
|
|
}
|
|
|
|
lines = trusteeInterface.db.getRecordset(TrusteeDataJournalLine, recordFilter=baseFilter) or []
|
|
lineList = []
|
|
for ln in lines:
|
|
jeId = ln.get("journalEntryId", "")
|
|
if jeId not in entryMap:
|
|
continue
|
|
entry = entryMap[jeId]
|
|
lineList.append({
|
|
"date": entry["date"],
|
|
"ref": entry["ref"],
|
|
"account": ln.get("accountNumber", ""),
|
|
"accountLabel": accountMap.get(ln.get("accountNumber", ""), {}).get("label", ""),
|
|
"debit": ln.get("debitAmount", 0),
|
|
"credit": ln.get("creditAmount", 0),
|
|
"desc": ln.get("description", "") or entry["desc"],
|
|
"taxCode": ln.get("taxCode", ""),
|
|
"costCenter": ln.get("costCenter", ""),
|
|
})
|
|
|
|
export = {
|
|
"accounts": list(accountMap.values()),
|
|
"balances": balanceList,
|
|
"journalLines": lineList,
|
|
"meta": {
|
|
"accountCount": len(accountMap),
|
|
"entryCount": len(entryMap),
|
|
"lineCount": len(lineList),
|
|
"balanceCount": len(balanceList),
|
|
"dateFrom": dateFrom,
|
|
"dateTo": dateTo,
|
|
},
|
|
}
|
|
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))
|
|
return result
|
|
except Exception as e:
|
|
logger.warning("Could not export accounting data: %s", e)
|
|
return ""
|
|
|
|
|
|
def _getCachedCounts(trusteeInterface, featureInstanceId: str) -> Dict[str, Any]:
|
|
"""Count existing records per TrusteeData* table without triggering an external sync."""
|
|
from modules.features.trustee.datamodelFeatureTrustee import (
|
|
TrusteeDataAccount,
|
|
TrusteeDataJournalEntry,
|
|
TrusteeDataJournalLine,
|
|
TrusteeDataContact,
|
|
TrusteeDataAccountBalance,
|
|
)
|
|
counts = {}
|
|
for label, model in [
|
|
("accounts", TrusteeDataAccount),
|
|
("journalEntries", TrusteeDataJournalEntry),
|
|
("journalLines", TrusteeDataJournalLine),
|
|
("contacts", TrusteeDataContact),
|
|
("accountBalances", TrusteeDataAccountBalance),
|
|
]:
|
|
records = trusteeInterface.db.getRecordset(
|
|
model, recordFilter={"featureInstanceId": featureInstanceId}
|
|
)
|
|
counts[label] = len(records) if records else 0
|
|
return counts
|