Fix Client FY specific Accounting Mirror path resolution

This commit is contained in:
A R R R Associates
2026-09-18 22:52:41 +05:30
parent 8a0d5b212d
commit f20b79ff70
4 changed files with 59 additions and 7 deletions
+1 -1
View File
@@ -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)
@@ -1,2 +1,2 @@
__version__ = "1.26.17"
__version__ = "1.26.18"
AGENT_NAME = "ERP Local Agent"
@@ -167,9 +167,21 @@ 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}"):
# 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 = []
@@ -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,