Fix cash payment Tally company resolution

This commit is contained in:
A R R R Associates
2026-09-04 19:42:58 +05:30
parent 1c9be6bd4a
commit deeebc01be
4 changed files with 62 additions and 31 deletions
+1 -1
View File
@@ -252,7 +252,7 @@ def cash_payment_review(
@router.post("/ledgers") @router.post("/ledgers")
async def cash_payment_ledgers(request: Request): async def cash_payment_ledgers(request: Request):
"""Load Cash ledgers using the same live Tally master snapshot path as Opening Balance Review.""" """Load Cash ledgers using the same name-resolved live Tally master snapshot path as Opening Balance Review."""
form = await request.form() form = await request.form()
validate_csrf(request, str(form.get("csrf_token") or "")) validate_csrf(request, str(form.get("csrf_token") or ""))
db = CommonSessionLocal() db = CommonSessionLocal()
+1 -1
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path from pathlib import Path
import zipfile import zipfile
ERP_LOCAL_AGENT_VERSION = "1.22.19" ERP_LOCAL_AGENT_VERSION = "1.22.20"
ERP_LOCAL_AGENT_NAME = "ERP Local Agent" ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime" RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0) _DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
@@ -1,2 +1,2 @@
__version__ = "1.22.19" __version__ = "1.22.20"
AGENT_NAME = "ERP Local Agent" AGENT_NAME = "ERP Local Agent"
@@ -429,6 +429,28 @@ class AgentCommandProcessor:
if len(matches)!=1: raise ValueError(f"Tally company '{wanted}' is not uniquely loaded. Open both previous/current companies in TallyPrime first.") if len(matches)!=1: raise ValueError(f"Tally company '{wanted}' is not uniquely loaded. Open both previous/current companies in TallyPrime first.")
return matches[0] return matches[0]
def _cash_payment_selected_company(self, payload: dict[str, Any]):
"""Resolve Cash Payment company exactly the same way as Opening Balance Review.
The visible company name selected by the user is authoritative for this
read-only review. Tally can report a different CURRENTCOMPANY while
multiple companies are open, so Cash Payment must not veto the selection
merely because that global/current-company value differs. The selected
company's real GUID is returned for cache identity once the name has been
resolved from Tally's loaded-company list.
"""
company_name = str(payload.get("company_name") or "").strip()
company = self._opening_balance_company(company_name)
selected_guid = str(payload.get("tally_guid") or "").strip()
actual_guid = str(company.guid or "").strip()
guid_changed = bool(selected_guid and actual_guid and selected_guid != actual_guid)
if guid_changed:
self.logger.warning(
"Cash Payment selected-company GUID refreshed by name: selected=%s actual=%s company=%s",
selected_guid, actual_guid, company.name,
)
return company, (actual_guid or selected_guid), guid_changed
def _opening_balance_snapshot(self, payload: dict[str, Any]) -> dict[str, Any]: def _opening_balance_snapshot(self, payload: dict[str, Any]) -> dict[str, Any]:
previous=self._opening_balance_company(str(payload.get("previous_company_name") or "")) previous=self._opening_balance_company(str(payload.get("previous_company_name") or ""))
current=self._opening_balance_company(str(payload.get("current_company_name") or "")) current=self._opening_balance_company(str(payload.get("current_company_name") or ""))
@@ -746,15 +768,11 @@ class AgentCommandProcessor:
"ledger_count": 0, "ledger_count": 0,
}) })
try: try:
# This is intentionally the same company-resolution helper used by # Resolve by the exact company name selected by the user, exactly like
# _opening_balance_snapshot(). Company name is the human selection; # Opening Balance Review. Do not use Tally's global CURRENTCOMPANY as
# GUID is verified when supplied so another open company cannot leak in. # a veto when multiple companies are open; Tally may report whichever
company = self._opening_balance_company(expected_company_name) # company currently has UI focus.
if expected_guid and company.guid and str(company.guid).strip() != expected_guid: company, resolved_guid, guid_refreshed = self._cash_payment_selected_company(payload)
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) masters = self.tally.fetch_accounting_masters(company.name)
group_rows = list(masters.get("groups") or []) group_rows = list(masters.get("groups") or [])
@@ -808,6 +826,8 @@ class AgentCommandProcessor:
"status": "completed", "status": "completed",
"company": company.as_dict(), "company": company.as_dict(),
"company_name": company.name, "company_name": company.name,
"company_guid": resolved_guid,
"selected_guid_refreshed": guid_refreshed,
"ledgers": returned, "ledgers": returned,
"cash_candidates": cash_ledgers, "cash_candidates": cash_ledgers,
"ledger_count": len(returned), "ledger_count": len(returned),
@@ -1341,19 +1361,24 @@ class AgentCommandProcessor:
if not self.store.exists(client_id): if not self.store.exists(client_id):
raise ValueError("Accounting local storage is not initialized for this client.") raise ValueError("Accounting local storage is not initialized for this client.")
self._ensure_cash_cache_schema(client_id) self._ensure_cash_cache_schema(client_id)
mapping = self.store.get_active_mapping_by_guid(client_id, tally_guid) or {}
company_name = str(payload.get("company_name") or mapping.get("company_name") or "").strip() # Match Opening Balance Review: resolve the exact company selected by
if not company_name: # name from Tally's loaded-company list. This deliberately avoids the
# Read-only cash compliance may work against any company that is currently # generic CURRENTCOMPANY name guard, which is unreliable when two Tally
# open in TallyPrime. Do not create or alter the client's permanent mapping. # companies are open at the same time.
company, live_company_name = self._resolve_open_company(payload) company, resolved_guid, _guid_refreshed = self._cash_payment_selected_company(payload)
company_name = live_company_name company_name = str(company.name or "").strip()
mapping = { if resolved_guid:
"tally_guid": str(company.get("guid") or tally_guid), tally_guid = resolved_guid
"company_name": company_name, payload["tally_guid"] = resolved_guid
"gstin": str(company.get("gstin") or ""), payload["company_name"] = company_name
"transient_open_company": True,
} mapping = self.store.get_active_mapping_by_guid(client_id, tally_guid) or {
"tally_guid": tally_guid,
"company_name": company_name,
"gstin": str(getattr(company, "gstin", "") or ""),
"transient_open_company": True,
}
# Reuse a paused/interrupted identical cache only inside the worker. # Reuse a paused/interrupted identical cache only inside the worker.
with self.store.connect(client_id) as db: with self.store.connect(client_id) as db:
@@ -1451,12 +1476,18 @@ class AgentCommandProcessor:
self._cash_cache_update(client_id, job_id, status="cancelled", stage="Cancelled before Tally connection", completed_at_utc=datetime.now(timezone.utc).isoformat()) self._cash_cache_update(client_id, job_id, status="cancelled", stage="Cancelled before Tally connection", completed_at_utc=datetime.now(timezone.utc).isoformat())
return return
# This is deliberately inside the worker. It may be slow, but it can # Resolve the company with the same name-based mechanism used by
# no longer hold open the ERP start-job command or block its timeout. # Opening Balance Review. Do not compare against Tally CURRENTCOMPANY;
company, company_name = self._resolve_open_company(payload) # that value can point at another open company simply because it has UI
expected_guid = str(payload.get("tally_guid") or "").strip() # focus. Persist the actual GUID returned for the selected company.
if str(company.get("guid") or "").strip() != expected_guid: company, resolved_guid, _guid_refreshed = self._cash_payment_selected_company(payload)
raise ValueError("The selected Tally company is not the currently resolved company.") company_name = str(company.name or "").strip()
if resolved_guid:
payload["tally_guid"] = resolved_guid
self._cash_cache_update(
client_id, job_id, tally_guid=resolved_guid, company_name=company_name,
status="running", stage="Selected Tally company resolved by name", current_date=""
)
self._cash_cache_update(client_id, job_id, status="running", stage="Reading cash ledger masters", current_date="") self._cash_cache_update(client_id, job_id, status="running", stage="Reading cash ledger masters", current_date="")
# One master request only, then a generous cooling gap before vouchers. # One master request only, then a generous cooling gap before vouchers.