diff --git a/app/modules/accounting/cash_payment_ui.py b/app/modules/accounting/cash_payment_ui.py index 18791c3..16be99a 100644 --- a/app/modules/accounting/cash_payment_ui.py +++ b/app/modules/accounting/cash_payment_ui.py @@ -252,7 +252,7 @@ def cash_payment_review( @router.post("/ledgers") 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() validate_csrf(request, str(form.get("csrf_token") or "")) db = CommonSessionLocal() diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py index 78fd769..08891be 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.22.19" +ERP_LOCAL_AGENT_VERSION = "1.22.20" 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 94580b7..19d30a5 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,2 +1,2 @@ -__version__ = "1.22.19" +__version__ = "1.22.20" AGENT_NAME = "ERP Local Agent" 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 7904cf5..d8cb9b9 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 @@ -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.") 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]: previous=self._opening_balance_company(str(payload.get("previous_company_name") or "")) current=self._opening_balance_company(str(payload.get("current_company_name") or "")) @@ -746,15 +768,11 @@ class AgentCommandProcessor: "ledger_count": 0, }) try: - # 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." - ) + # Resolve by the exact company name selected by the user, exactly like + # Opening Balance Review. Do not use Tally's global CURRENTCOMPANY as + # a veto when multiple companies are open; Tally may report whichever + # company currently has UI focus. + company, resolved_guid, guid_refreshed = self._cash_payment_selected_company(payload) masters = self.tally.fetch_accounting_masters(company.name) group_rows = list(masters.get("groups") or []) @@ -808,6 +826,8 @@ class AgentCommandProcessor: "status": "completed", "company": company.as_dict(), "company_name": company.name, + "company_guid": resolved_guid, + "selected_guid_refreshed": guid_refreshed, "ledgers": returned, "cash_candidates": cash_ledgers, "ledger_count": len(returned), @@ -1341,19 +1361,24 @@ class AgentCommandProcessor: if not self.store.exists(client_id): raise ValueError("Accounting local storage is not initialized for this client.") 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() - if not company_name: - # Read-only cash compliance may work against any company that is currently - # open in TallyPrime. Do not create or alter the client's permanent mapping. - company, live_company_name = self._resolve_open_company(payload) - company_name = live_company_name - mapping = { - "tally_guid": str(company.get("guid") or tally_guid), - "company_name": company_name, - "gstin": str(company.get("gstin") or ""), - "transient_open_company": True, - } + + # Match Opening Balance Review: resolve the exact company selected by + # name from Tally's loaded-company list. This deliberately avoids the + # generic CURRENTCOMPANY name guard, which is unreliable when two Tally + # companies are open at the same time. + company, resolved_guid, _guid_refreshed = self._cash_payment_selected_company(payload) + company_name = str(company.name or "").strip() + if resolved_guid: + tally_guid = resolved_guid + payload["tally_guid"] = resolved_guid + payload["company_name"] = company_name + + 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. 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()) return - # This is deliberately inside the worker. It may be slow, but it can - # no longer hold open the ERP start-job command or block its timeout. - company, company_name = self._resolve_open_company(payload) - expected_guid = str(payload.get("tally_guid") or "").strip() - if str(company.get("guid") or "").strip() != expected_guid: - raise ValueError("The selected Tally company is not the currently resolved company.") + # Resolve the company with the same name-based mechanism used by + # Opening Balance Review. Do not compare against Tally CURRENTCOMPANY; + # that value can point at another open company simply because it has UI + # focus. Persist the actual GUID returned for the selected company. + company, resolved_guid, _guid_refreshed = self._cash_payment_selected_company(payload) + 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="") # One master request only, then a generous cooling gap before vouchers.