Fix Client FY specific Accounting Mirror path resolution
This commit is contained in:
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.26.17"
|
ERP_LOCAL_AGENT_VERSION = "1.26.18"
|
||||||
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.26.17"
|
__version__ = "1.26.18"
|
||||||
AGENT_NAME = "ERP Local Agent"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -167,9 +167,21 @@ class LocalAccountingStore:
|
|||||||
for path in accounting_clients.glob(f"*/*/{filename}"):
|
for path in accounting_clients.glob(f"*/*/{filename}"):
|
||||||
candidates.append(path)
|
candidates.append(path)
|
||||||
|
|
||||||
# Earlier/future FY client Accounting locations (for client rename or FY
|
# Earlier versions also searched every FY folder for the newest client
|
||||||
# resolver changes). The active target is excluded below.
|
# database. That is unsafe now that Accounting Mirrors are authoritative
|
||||||
for path in self.storage_root.glob(f"FY*/Clients/*/*/Accounting/{filename}"):
|
# 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)
|
candidates.append(path)
|
||||||
|
|
||||||
unique = []
|
unique = []
|
||||||
|
|||||||
@@ -219,6 +219,20 @@ class AgentCommandProcessor:
|
|||||||
if client_id in (None, "") or not relative_dir:
|
if client_id in (None, "") or not relative_dir:
|
||||||
self.tally.bind_context(None, payload)
|
self.tally.bind_context(None, payload)
|
||||||
return
|
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.store.bind_client_path(int(client_id), relative_dir)
|
||||||
self.tally.bind_context(int(client_id), payload)
|
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())
|
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
|
return
|
||||||
try:
|
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:
|
def mirror_progress(progress: dict[str, Any]) -> None:
|
||||||
updates = dict(progress or {})
|
updates = dict(progress or {})
|
||||||
@@ -4381,6 +4418,8 @@ class AgentCommandProcessor:
|
|||||||
voucher_period = mirror_summary.get("voucher_period") or {}
|
voucher_period = mirror_summary.get("voucher_period") or {}
|
||||||
odbc_from = str(voucher_period.get("from_date") or "").strip()
|
odbc_from = str(voucher_period.get("from_date") or "").strip()
|
||||||
odbc_to = str(voucher_period.get("to_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(
|
requested_transactions = self.tally.mirror.transactions(
|
||||||
client_id, mirror_company_name, date_from, date_to, tally_guid
|
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)
|
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)
|
transactions = self.tally.mirror.transactions(client_id, mirror_company_name, date_from, date_to, tally_guid)
|
||||||
transaction_sync = self.store.replace_transaction_snapshot(
|
transaction_sync = self.store.replace_transaction_snapshot(
|
||||||
client_id, mapping={**mapping, "company_name": mirror_company_name}, transactions=transactions,
|
client_id, mapping={**mapping, "company_name": mirror_company_name}, transactions=transactions,
|
||||||
|
|||||||
Reference in New Issue
Block a user