From f20b79ff70fbd70fc782f905402213eee8e155a5 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Fri, 18 Sep 2026 22:52:41 +0530 Subject: [PATCH] Fix Client FY specific Accounting Mirror path resolution --- app/modules/documents/agent_package.py | 2 +- .../erp_local_agent/__init__.py | 2 +- .../erp_local_agent/accounting_store.py | 20 +++++++-- .../erp_local_agent/commands.py | 42 ++++++++++++++++++- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py index 45c3de3..4cfcf2e 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.17" +ERP_LOCAL_AGENT_VERSION = "1.26.18" 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 3a4b3fc..6721d61 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.26.17" +__version__ = "1.26.18" AGENT_NAME = "ERP Local Agent" diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py b/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py index 90c22c0..ae7214b 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py @@ -167,10 +167,22 @@ class LocalAccountingStore: for path in accounting_clients.glob(f"*/*/{filename}"): candidates.append(path) - # Earlier/future FY client Accounting locations (for client rename or FY - # resolver changes). The active target is excluded below. - for path in self.storage_root.glob(f"FY*/Clients/*/*/Accounting/{filename}"): - candidates.append(path) + # Earlier versions also searched every FY folder for the newest client + # database. That is unsafe now that Accounting Mirrors are authoritative + # per Client + Financial Year: a 2022-23 database must never be copied into + # the 2021-22 location merely because it is newer. Preserve the useful + # client-rename recovery behaviour, but restrict it to the SAME FY as the + # target path. + try: + relative_target = target_db.resolve().relative_to(self.storage_root.resolve()) + fy_segment = relative_target.parts[0] if relative_target.parts else "" + except Exception: + fy_segment = "" + if fy_segment.lower().startswith("fy"): + for path in self.storage_root.glob( + f"{fy_segment}/Clients/*/*/Accounting/{filename}" + ): + candidates.append(path) unique = [] seen = set() 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 d5033f2..7b54e44 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 @@ -219,6 +219,20 @@ class AgentCommandProcessor: if client_id in (None, "") or not relative_dir: self.tally.bind_context(None, payload) return + + # Client + Financial Year is the authoritative Accounting Mirror identity. + # Do not allow a stale Local-Agent path binding to redirect an FY request to + # another year's .act file. ERP supplies paths in the canonical form + # FY2022-23/Clients/.../Accounting. + requested_fy = str(payload.get("financial_year") or "").strip() + first_segment = relative_dir.replace("\\", "/").split("/", 1)[0].strip() + path_fy = first_segment[2:] if first_segment.lower().startswith("fy") else "" + if requested_fy and path_fy and requested_fy != path_fy: + raise ValueError( + f"Accounting Mirror FY/path mismatch: ERP requested FY {requested_fy}, " + f"but storage path targets FY {path_fy}." + ) + self.store.bind_client_path(int(client_id), relative_dir) self.tally.bind_context(int(client_id), payload) @@ -4355,7 +4369,30 @@ class AgentCommandProcessor: publish(status="failed", percent=100, stage="Busy", error="Another Accounting Mirror export is already running on this workstation.", finished_at_utc=datetime.now(timezone.utc).isoformat()) return try: - publish(status="running", percent=2, stage="Starting", message="Starting read-only ODBC export from TallyPrime.") + # Re-bind inside the worker thread. The export is asynchronous and the + # Local Agent may have served other Client/FY requests after the job was + # queued. Rebinding here guarantees that the selected ERP FY controls + # the mirror path used by extraction, publication and registry metadata. + self._bind_accounting_storage(payload) + bound_db = self.store.db_path(client_id) + requested_fy = str(payload.get("financial_year") or "").strip() + expected_dir = str(payload.get("accounting_relative_dir") or "").strip().replace("\\", "/") + actual_path = str(bound_db).replace("\\", "/") + if expected_dir and expected_dir.casefold() not in actual_path.casefold(): + raise RuntimeError( + "Accounting Mirror storage binding did not resolve to the ERP-selected Client/FY path. " + f"Expected directory '{expected_dir}', resolved '{bound_db}'." + ) + + publish( + status="running", percent=2, stage="Starting", + message=( + "Starting read-only ODBC export from TallyPrime for " + f"FY {requested_fy or 'selected period'} into {bound_db}." + ), + financial_year=requested_fy, + mirror_db_path=str(bound_db), + ) def mirror_progress(progress: dict[str, Any]) -> None: updates = dict(progress or {}) @@ -4381,6 +4418,8 @@ class AgentCommandProcessor: voucher_period = mirror_summary.get("voucher_period") or {} odbc_from = str(voucher_period.get("from_date") or "").strip() odbc_to = str(voucher_period.get("to_date") or "").strip() + # Keep the same Client/FY binding for all post-extraction reads. + self._bind_accounting_storage(payload) requested_transactions = self.tally.mirror.transactions( client_id, mirror_company_name, date_from, date_to, tally_guid ) @@ -4428,6 +4467,7 @@ class AgentCommandProcessor: ) publish(status="running", percent=98, stage="Updating client accounting database", message="Refreshing the existing client .act transaction snapshot from the completed Accounting Mirror. Master analysis remains available directly from the mirror without clearing any existing auxiliary masters.", mirror_db_path=str(mirror_result.get("mirror_db_path") or ""), warnings=validation_warnings) + self._bind_accounting_storage(payload) transactions = self.tally.mirror.transactions(client_id, mirror_company_name, date_from, date_to, tally_guid) transaction_sync = self.store.replace_transaction_snapshot( client_id, mapping={**mapping, "company_name": mirror_company_name}, transactions=transactions,