Simplify Tally mirror workflow and use GUID company identity
This commit is contained in:
@@ -4,7 +4,7 @@ import io
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
ERP_LOCAL_AGENT_VERSION = "1.24.3"
|
||||
ERP_LOCAL_AGENT_VERSION = "1.24.4"
|
||||
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.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,
|
||||
|
||||
Reference in New Issue
Block a user