diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py index 766f7e3..e0d8bdf 100644 --- a/app/modules/documents/agent_package.py +++ b/app/modules/documents/agent_package.py @@ -4,7 +4,7 @@ import io from pathlib import Path import zipfile -ERP_LOCAL_AGENT_VERSION = "1.26.21" +ERP_LOCAL_AGENT_VERSION = "1.26.22" ERP_LOCAL_AGENT_NAME = "ERP Local Agent" RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime" _DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0) diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py index 06ac6b8..a5bcd3d 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py @@ -1 +1 @@ -__version__ = "1.26.21" +__version__ = "1.26.22" diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py index 3195a0b..c81db6e 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py @@ -4558,14 +4558,87 @@ class AgentCommandProcessor: (*params,limit)).fetchall()] return {"rows":rows,"count":len(rows)} if query == "trial_balance": + # Tally Trial Balance excludes cancelled and optional vouchers. Older + # mirror queries summed voucher_ledger_entry directly, so those + # non-accounting vouchers could make mirror debit/credit movement + # differ from Tally even though Tally itself was perfectly balanced. + # + # Keep Tally's master opening/closing balances as the authoritative + # control figures and independently reconstruct closing from booked + # voucher movement. The extra calculated/difference columns are + # backward compatible: existing callers continue using the original + # opening_balance, closing_balance, debit and credit fields. rows=[dict(r) for r in db.execute( - """SELECT lm.ledger_name,lm.parent_group,lm.opening_balance,lm.closing_balance, - ROUND(COALESCE(SUM(CASE WHEN vle.dr_cr='DR' THEN vle.amount ELSE 0 END),0),2) debit, - ROUND(COALESCE(SUM(CASE WHEN vle.dr_cr='CR' THEN vle.amount ELSE 0 END),0),2) credit - FROM ledger_master lm LEFT JOIN voucher_ledger_entry vle ON vle.ledger_name=lm.ledger_name - GROUP BY lm.ledger_guid,lm.ledger_name,lm.parent_group,lm.opening_balance,lm.closing_balance - ORDER BY lm.parent_group,lm.ledger_name LIMIT ?""",(limit,)).fetchall()] - return {"rows":rows,"count":len(rows)} + """WITH booked_movement AS ( + SELECT lower(trim(COALESCE(vle.ledger_name,''))) AS ledger_key, + ROUND(COALESCE(SUM(CASE WHEN vle.dr_cr='DR' THEN vle.amount ELSE 0 END),0),2) AS debit, + ROUND(COALESCE(SUM(CASE WHEN vle.dr_cr='CR' THEN vle.amount ELSE 0 END),0),2) AS credit + FROM voucher_ledger_entry vle + JOIN voucher v ON v.voucher_guid=vle.voucher_guid + WHERE COALESCE(v.is_cancelled,0)=0 + AND COALESCE(v.is_optional,0)=0 + GROUP BY lower(trim(COALESCE(vle.ledger_name,''))) + ) + SELECT lm.ledger_name,lm.parent_group,lm.opening_balance,lm.closing_balance, + ROUND(COALESCE(bm.debit,0),2) AS debit, + ROUND(COALESCE(bm.credit,0),2) AS credit, + ROUND(COALESCE(lm.opening_balance,0) + + COALESCE(bm.credit,0) - COALESCE(bm.debit,0),2) AS calculated_closing_balance, + ROUND(COALESCE(lm.closing_balance,0) + - (COALESCE(lm.opening_balance,0) + + COALESCE(bm.credit,0) - COALESCE(bm.debit,0)),2) AS reconciliation_difference + FROM ledger_master lm + LEFT JOIN booked_movement bm + ON bm.ledger_key=lower(trim(COALESCE(lm.ledger_name,''))) + ORDER BY lm.parent_group,lm.ledger_name LIMIT ?""",(limit,)).fetchall()] + + # Diagnostics are deliberately returned alongside the existing rows + # instead of changing/removing any existing Trial Balance behaviour. + # This makes extraction defects visible without ever inserting a + # balancing ledger or silently changing Tally control balances. + difference_rows = [ + r for r in rows + if abs(float(r.get("reconciliation_difference") or 0)) > 0.01 + ] + totals = { + "opening_debit": round(sum(abs(float(r.get("opening_balance") or 0)) for r in rows if float(r.get("opening_balance") or 0) < 0), 2), + "opening_credit": round(sum(float(r.get("opening_balance") or 0) for r in rows if float(r.get("opening_balance") or 0) > 0), 2), + "debit": round(sum(float(r.get("debit") or 0) for r in rows), 2), + "credit": round(sum(float(r.get("credit") or 0) for r in rows), 2), + "closing_debit": round(sum(abs(float(r.get("closing_balance") or 0)) for r in rows if float(r.get("closing_balance") or 0) < 0), 2), + "closing_credit": round(sum(float(r.get("closing_balance") or 0) for r in rows if float(r.get("closing_balance") or 0) > 0), 2), + } + totals["movement_difference"] = round(totals["debit"] - totals["credit"], 2) + totals["closing_difference"] = round(totals["closing_debit"] - totals["closing_credit"], 2) + + unmatched = [dict(r) for r in db.execute( + """SELECT vle.ledger_name, COUNT(*) AS entry_count, + ROUND(SUM(CASE WHEN vle.dr_cr='DR' THEN vle.amount ELSE 0 END),2) AS debit, + ROUND(SUM(CASE WHEN vle.dr_cr='CR' THEN vle.amount ELSE 0 END),2) AS credit + FROM voucher_ledger_entry vle + JOIN voucher v ON v.voucher_guid=vle.voucher_guid + LEFT JOIN ledger_master lm + ON lower(trim(COALESCE(lm.ledger_name,'')))=lower(trim(COALESCE(vle.ledger_name,''))) + WHERE COALESCE(v.is_cancelled,0)=0 + AND COALESCE(v.is_optional,0)=0 + AND lm.ledger_name IS NULL + GROUP BY vle.ledger_name + ORDER BY ABS(COALESCE(SUM(CASE WHEN vle.dr_cr='DR' THEN vle.amount ELSE -vle.amount END),0)) DESC + LIMIT 100""" + ).fetchall()] + return { + "rows": rows, + "count": len(rows), + "reconciliation": { + "ok": not difference_rows and not unmatched and abs(totals["movement_difference"]) <= 0.01, + "difference_count": len(difference_rows), + "differences": difference_rows[:100], + "unmatched_ledger_count": len(unmatched), + "unmatched_ledgers": unmatched, + "totals": totals, + "basis": "Booked vouchers only; cancelled and optional vouchers excluded; Tally ledger closing balance retained as control figure.", + }, + } if query == "group_master": rows=[dict(r) for r in db.execute( """SELECT master_guid,name,parent,reserved_name,is_revenue,is_deemed_positive,is_active