Simplify Tally mirror workflow and use GUID company identity

This commit is contained in:
A R R R Associates
2026-09-06 19:20:41 +05:30
parent 9500668413
commit b46a618b13
5 changed files with 73 additions and 33 deletions
@@ -1,2 +1,2 @@
__version__ = "1.24.3"
__version__ = "1.24.4"
AGENT_NAME = "ERP Local Agent"
@@ -354,10 +354,17 @@ class AccountingMirrorEngine:
actual_guid = _text(company.get("company_guid"))
exp_name = _text(expected_company_name)
exp_guid = _text(expected_company_guid)
if exp_name and actual_name.casefold() != exp_name.casefold():
raise RuntimeError(f"Active Tally company mismatch. ERP expected '{exp_name}', ODBC returned '{actual_name}'.")
validation_warnings: list[str] = []
# The Tally GUID is the permanent company identity. Company names are descriptive
# snapshots only and commonly contain FY/year suffixes (for example "-25-26").
# Never reject a valid mirror merely because the descriptive name changed.
if exp_guid and actual_guid and exp_guid != actual_guid:
raise RuntimeError(f"Tally company GUID mismatch. ERP expected {exp_guid}, ODBC returned {actual_guid}.")
if exp_name and actual_name and actual_name.casefold() != exp_name.casefold():
validation_warnings.append(
f"Tally company name differs from the ERP snapshot: ERP '{exp_name}', Tally '{actual_name}'. "
"The export was accepted because company identity is based on the Tally GUID."
)
report(96, "Publishing Accounting Mirror", "Publishing the verified SQLite mirror inside the client's Accounting folder.", rows=dict(rows))
paths.current_mirror.parent.mkdir(parents=True, exist_ok=True)
@@ -377,6 +384,7 @@ class AccountingMirrorEngine:
result = {
"ok": True, "synced_at_utc": _utc_now(), "company_name": actual_name,
"company_guid": actual_guid, "company_cache_path": str(cache),
"validation_warnings": validation_warnings,
"extractor_log_tail": log[-1500:], "loader_log_tail": llog[-1500:],
**self.status(client_id),
}
@@ -385,22 +393,37 @@ class AccountingMirrorEngine:
def company_db(self, client_id: int, company_name: str, company_guid: str = "") -> Path:
paths = self.paths(client_id)
requested_name = _text(company_name)
requested_guid = _text(company_guid)
current = self._summary(paths.current_mirror)
c = current.get("company") or {}
if current.get("ready") and _text(c.get("company_name")).casefold() == _text(company_name).casefold():
if not company_guid or not _text(c.get("company_guid")) or _text(c.get("company_guid")) == _text(company_guid):
current_name = _text(c.get("company_name"))
current_guid = _text(c.get("company_guid"))
# GUID is authoritative and survives Tally company renames / FY suffix changes.
if current.get("ready"):
if requested_guid and current_guid and requested_guid == current_guid:
return paths.current_mirror
exact = self._cache_file(paths, company_name, company_guid)
if not requested_guid and current_name.casefold() == requested_name.casefold():
return paths.current_mirror
exact = self._cache_file(paths, requested_name, requested_guid)
if exact.is_file():
return exact
# GUID may be omitted by callers. Search only for an exact company-name prefix.
prefix = _safe_key(company_name) + "_"
candidates = sorted(paths.cache_root.glob(prefix + "*.act"), key=lambda p: p.stat().st_mtime, reverse=True)
for candidate in candidates:
s = self._summary(candidate)
c = s.get("company") or {}
if _text(c.get("company_name")).casefold() == _text(company_name).casefold():
return candidate
candidates = sorted(paths.cache_root.glob("*.act"), key=lambda p: p.stat().st_mtime, reverse=True)
if requested_guid:
for candidate in candidates:
snapshot = self._summary(candidate)
company = snapshot.get("company") or {}
if _text(company.get("company_guid")) == requested_guid:
return candidate
else:
for candidate in candidates:
snapshot = self._summary(candidate)
company = snapshot.get("company") or {}
if _text(company.get("company_name")).casefold() == requested_name.casefold():
return candidate
raise RuntimeError(
f"No .NET/.act mirror snapshot is cached for Tally company '{company_name}'. "
"Make that company active in TallyPrime and run Master/Transaction Sync once."
@@ -2950,18 +2950,21 @@ class AgentCommandProcessor:
timeout_seconds=max(300, int(payload.get("timeout_seconds") or 1800)),
progress_callback=mirror_progress,
)
publish(status="running", percent=98, stage="Updating client accounting database", message="Refreshing the existing client .act transaction snapshot from the completed ODBC 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 ""))
transactions = self.tally.mirror.transactions(client_id, company_name, date_from, date_to, tally_guid)
mirror_company_name = str(mirror_result.get("company_name") or company_name).strip() or company_name
validation_warnings = list(mirror_result.get("validation_warnings") or [])
publish(status="running", percent=98, stage="Updating client accounting database", message="Refreshing the existing client .act transaction snapshot from the completed ODBC 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)
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": company_name}, transactions=transactions,
client_id, mapping={**mapping, "company_name": mirror_company_name}, transactions=transactions,
requested_by_user_id=int(requested_by) if requested_by not in (None, "") else None,
)
mirror_summary = (mirror_result.get("mirror") or {})
counts = mirror_summary.get("counts") or {}
publish(
status="completed", percent=100, stage="Complete",
message="Full Accounting Export completed. ERP analysis tools can now read the refreshed SQLite data without re-extracting from Tally.",
finished_at_utc=datetime.now(timezone.utc).isoformat(), error="",
message=("Full Accounting Export completed. ERP analysis tools can now read the refreshed SQLite data without re-extracting from Tally."
+ ((" " + validation_warnings[0]) if validation_warnings else "")),
finished_at_utc=datetime.now(timezone.utc).isoformat(), error="", warnings=validation_warnings,
rows=counts, mirror=mirror_summary, mirror_db_path=str(mirror_result.get("mirror_db_path") or ""),
accounting_db_path=str(self.store.db_path(client_id)),
transaction_sync=transaction_sync,