Fix Tally ODBC selector GUID and workspace FY mirror defaults
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
__version__ = "1.26.0"
|
||||
__version__ = "1.26.1"
|
||||
AGENT_NAME = "ERP Local Agent"
|
||||
|
||||
@@ -31,6 +31,36 @@ def _number(value: Any) -> float:
|
||||
m = re.search(r"[-+]?\d+(?:\.\d+)?", s)
|
||||
return float(m.group(0)) if m else 0.0
|
||||
|
||||
def _is_placeholder_company_guid(value: Any) -> bool:
|
||||
"""True when ODBC returned a company selector token rather than a GUID."""
|
||||
text = _text(value).strip()
|
||||
if not text:
|
||||
return True
|
||||
normal = re.sub(r"[^A-Z0-9]+", "_", text.upper()).strip("_")
|
||||
return normal in {
|
||||
"ACTIVE_COMPANY",
|
||||
"CURRENT_COMPANY",
|
||||
"SELECTED_COMPANY",
|
||||
"TALLY_ACTIVE_COMPANY",
|
||||
"COMPANY",
|
||||
"ACTIVE",
|
||||
"CURRENT",
|
||||
"N_A",
|
||||
"NA",
|
||||
"NONE",
|
||||
"NULL",
|
||||
}
|
||||
|
||||
|
||||
def _company_identity_name(value: Any) -> str:
|
||||
"""Normalize descriptive company name for GUID-unavailable fallback."""
|
||||
text = _text(value).strip().casefold()
|
||||
# Ignore only a trailing FY/year suffix such as -25-26 or 2025-26.
|
||||
text = re.sub(r"[\s_-]+(?:fy\s*)?\d{2,4}\s*[-/]\s*\d{2,4}\s*$", "", text).strip()
|
||||
text = re.sub(r"[^a-z0-9]+", " ", text)
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def _safe_key(value: str) -> str:
|
||||
text = re.sub(r"[^A-Za-z0-9_.-]+", "_", _text(value)).strip("_")
|
||||
return text[:80] or "company"
|
||||
@@ -361,15 +391,34 @@ class AccountingMirrorEngine:
|
||||
exp_name = _text(expected_company_name)
|
||||
exp_guid = _text(expected_company_guid)
|
||||
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():
|
||||
# A genuine Tally GUID remains authoritative. Some ODBC builds return a
|
||||
# selector token such as ACTIVE_COMPANY instead of the real GUID.
|
||||
actual_guid_is_placeholder = _is_placeholder_company_guid(actual_guid)
|
||||
|
||||
if exp_guid and actual_guid and not actual_guid_is_placeholder and exp_guid != actual_guid:
|
||||
raise RuntimeError(
|
||||
f"Tally company GUID mismatch. ERP expected {exp_guid}, ODBC returned {actual_guid}."
|
||||
)
|
||||
|
||||
if exp_guid and actual_guid_is_placeholder:
|
||||
expected_name_key = _company_identity_name(exp_name)
|
||||
actual_name_key = _company_identity_name(actual_name)
|
||||
if not expected_name_key or not actual_name_key or expected_name_key != actual_name_key:
|
||||
raise RuntimeError(
|
||||
"Tally ODBC did not return a usable company GUID "
|
||||
f"(returned '{actual_guid or 'blank'}') and the company name could not safely "
|
||||
f"confirm the saved mapping. ERP expected '{exp_name}', ODBC returned '{actual_name}'."
|
||||
)
|
||||
validation_warnings.append(
|
||||
"Tally ODBC returned a selector token instead of the real company GUID "
|
||||
f"('{actual_guid or 'blank'}'). The mirror was accepted because the normalized "
|
||||
"company name matches the saved ERP mapping. The saved ERP GUID was preserved."
|
||||
)
|
||||
|
||||
if exp_name and actual_name and _company_identity_name(actual_name) != _company_identity_name(exp_name):
|
||||
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."
|
||||
"The saved ERP mapping was not changed."
|
||||
)
|
||||
|
||||
report(96, "Publishing Accounting Mirror", "Publishing the verified SQLite mirror inside the client's Accounting folder.", rows=dict(rows))
|
||||
@@ -385,11 +434,14 @@ class AccountingMirrorEngine:
|
||||
dst.close(); src.close()
|
||||
temp.replace(paths.current_mirror)
|
||||
|
||||
cache = self._cache_file(paths, actual_name, actual_guid)
|
||||
effective_guid = exp_guid if _is_placeholder_company_guid(actual_guid) else actual_guid
|
||||
cache = self._cache_file(paths, actual_name, effective_guid)
|
||||
shutil.copy2(paths.current_mirror, cache)
|
||||
result = {
|
||||
"ok": True, "synced_at_utc": _utc_now(), "company_name": actual_name,
|
||||
"company_guid": actual_guid, "company_cache_path": str(cache),
|
||||
"company_guid": effective_guid,
|
||||
"odbc_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),
|
||||
|
||||
Reference in New Issue
Block a user