Reuse opening balance Tally master flow for cash ledger selection

This commit is contained in:
A R R R Associates
2026-09-04 19:24:05 +05:30
parent 5c99d512c6
commit 1c9be6bd4a
5 changed files with 112 additions and 57 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path
import zipfile
ERP_LOCAL_AGENT_VERSION = "1.22.18"
ERP_LOCAL_AGENT_VERSION = "1.22.19"
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)
@@ -1,2 +1,2 @@
__version__ = "1.22.18"
__version__ = "1.22.19"
AGENT_NAME = "ERP Local Agent"
@@ -177,6 +177,7 @@ class AgentCommandProcessor:
"cash_payment_async_ledger_master_capability": True,
"cash_payment_ledger_cache_capability": True,
"cash_payment_open_company_selection_capability": True,
"cash_payment_opening_balance_master_path_capability": True,
"opening_balance_balance_sheet_only_capability": True,
"tally_writeback_capability": True,
}
@@ -719,57 +720,115 @@ class AgentCommandProcessor:
def _cash_payment_cash_ledgers(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Discover Cash-in-Hand ledgers directly from the selected open company.
"""Return Cash-in-Hand ledgers using the proven Opening Balance master path.
The normal Cash Payment workflow must not depend on a full Tally ledger-master
export. This command uses a narrow Tally collection and returns only the
Cash-in-Hand hierarchy. The existing full-ledger async cache remains available
for the explicit manual-selection fallback.
Cash Payment no longer uses a separate Tally collection or a separate
ledger-master job merely to populate the selector. It deliberately reuses
the same open-company resolution and ``fetch_accounting_masters`` call used
by Opening Balance Review, then filters those already-returned masters.
The legacy async full-ledger cache functions remain untouched for backward
compatibility and for any older callers.
"""
client_id = int(payload.get("client_id"))
tally_guid = str(payload.get("tally_guid") or "").strip()
if not tally_guid:
raise ValueError("Open Tally company GUID is required.")
expected_company_name = str(payload.get("company_name") or "").strip()
progress_id = "CASHDISC-" + uuid.uuid4().hex[:12].upper()
expected_guid = str(payload.get("tally_guid") or "").strip()
ledger_scope = str(payload.get("ledger_scope") or "cash").strip().lower()
if ledger_scope not in {"cash", "all"}:
ledger_scope = "cash"
progress_id = "CASHMASTER-" + uuid.uuid4().hex[:12].upper()
_cash_progress_snapshot({
"job_id": progress_id, "job_kind": "cash_ledger_discovery",
"status": "running", "stage": "Reading Cash-in-Hand ledgers from Tally",
"percent": 25, "company_name": expected_company_name, "ledger_count": 0,
"job_id": progress_id,
"job_kind": "cash_ledger_discovery",
"status": "running",
"stage": "Reading Tally masters using Opening Balance mechanism",
"percent": 20,
"company_name": expected_company_name,
"ledger_count": 0,
})
try:
company, company_name = self._resolve_open_company(payload)
rows = self.tally.cash_in_hand_ledgers(company_name) or []
ledgers = self._cash_ledger_rows(rows)
# Every row returned by this filtered Tally collection belongs to the
# Cash-in-Hand hierarchy; mark it as a candidate even when it sits below
# a user-created child group rather than directly under Cash-in-Hand.
for row in ledgers:
row["is_cash_candidate"] = True
# This is intentionally the same company-resolution helper used by
# _opening_balance_snapshot(). Company name is the human selection;
# GUID is verified when supplied so another open company cannot leak in.
company = self._opening_balance_company(expected_company_name)
if expected_guid and company.guid and str(company.guid).strip() != expected_guid:
raise ValueError(
"Selected Tally company GUID does not match the currently loaded company. "
"Reload Tally companies and select the company again."
)
masters = self.tally.fetch_accounting_masters(company.name)
group_rows = list(masters.get("groups") or [])
ledger_rows = list(masters.get("ledgers") or [])
# Build the complete Cash-in-Hand descendant-group hierarchy from the
# same Group masters already returned by Tally. This supports both
# ledgers directly under Cash-in-Hand and user-created child groups.
cash_group_keys = {"cash-in-hand", "cash in hand"}
changed = True
while changed:
changed = False
for group in group_rows:
name = str(group.get("name") or "").strip()
parent = str(group.get("parent") or "").strip()
if not name:
continue
parent_key = parent.casefold().replace("-", " ").strip()
name_key = name.casefold().replace("-", " ").strip()
normalized_cash = {x.replace("-", " ") for x in cash_group_keys}
if parent_key in normalized_cash and name_key not in normalized_cash:
cash_group_keys.add(name_key)
changed = True
all_ledgers = self._cash_ledger_rows(ledger_rows)
cash_ledgers: list[dict[str, Any]] = []
normalized_groups = {x.casefold().replace("-", " ").strip() for x in cash_group_keys}
for row in all_ledgers:
parent_key = str(row.get("parent") or "").casefold().replace("-", " ").strip()
reserved = str(row.get("reserved_name") or "").strip().casefold()
name_key = str(row.get("name") or "").strip().casefold()
is_cash = parent_key in normalized_groups or reserved == "cash" or name_key == "cash"
row["is_cash_candidate"] = bool(is_cash)
if is_cash:
cash_ledgers.append(row)
all_ledgers.sort(key=lambda x: (not bool(x.get("is_cash_candidate")), str(x.get("name") or "").casefold()))
cash_ledgers.sort(key=lambda x: str(x.get("name") or "").casefold())
returned = all_ledgers if ledger_scope == "all" else cash_ledgers
_cash_progress_snapshot({
"job_id": progress_id, "job_kind": "cash_ledger_discovery",
"job_id": progress_id,
"job_kind": "cash_ledger_discovery",
"status": "completed",
"stage": f"{len(ledgers)} Cash-in-Hand ledger(s) ready",
"percent": 100, "company_name": company_name, "ledger_count": len(ledgers),
"stage": f"{len(returned)} ledger(s) ready from Tally master snapshot",
"percent": 100,
"company_name": company.name,
"ledger_count": len(returned),
})
return {
"status": "completed",
"company": {"guid": str(company.get("guid") or tally_guid), "name": company_name},
"company_name": company_name,
"ledgers": ledgers,
"cash_candidates": ledgers,
"ledger_count": len(ledgers),
"cash_only": True,
"company": company.as_dict(),
"company_name": company.name,
"ledgers": returned,
"cash_candidates": cash_ledgers,
"ledger_count": len(returned),
"total_ledgers_examined": len(all_ledgers),
"cash_ledger_count": len(cash_ledgers),
"cash_only": ledger_scope != "all",
"from_cache": False,
"source": "opening_balance_master_snapshot",
"read_only": True,
"agent": self._agent_info(),
}
except Exception as exc:
_cash_progress_snapshot({
"job_id": progress_id, "job_kind": "cash_ledger_discovery",
"status": "failed", "stage": "Cash-in-Hand ledger discovery failed",
"percent": 100, "company_name": expected_company_name,
"ledger_count": 0, "error": str(exc),
"job_id": progress_id,
"job_kind": "cash_ledger_discovery",
"status": "failed",
"stage": "Tally master snapshot failed",
"percent": 100,
"company_name": expected_company_name,
"ledger_count": 0,
"error": str(exc),
})
raise