Fix cross-FY Accounting Mirror creditor follow-up

This commit is contained in:
A R R R Associates
2026-09-18 19:06:41 +05:30
parent aa3096d0fd
commit 93f2e02c35
3 changed files with 61 additions and 8 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path from pathlib import Path
import zipfile import zipfile
ERP_LOCAL_AGENT_VERSION = "1.26.15" ERP_LOCAL_AGENT_VERSION = "1.26.16"
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.15" __version__ = "1.26.16"
AGENT_NAME = "ERP Local Agent" AGENT_NAME = "ERP Local Agent"
@@ -61,6 +61,29 @@ def _company_identity_name(value: Any) -> str:
return re.sub(r"\s+", " ", text).strip() return re.sub(r"\s+", " ", text).strip()
def _company_fy_split_identity_name(value: Any) -> str:
"""Normalize an FY-split Tally company name without weakening normal mapping checks.
Tally users commonly keep a separate company for each financial year and Tally
may expose that company as ``NAME - (from 1-Apr-22)`` while the ERP mapping was
created against ``NAME Audit``. When ODBC returns only the ACTIVE_COMPANY token
there is no usable GUID to compare, so this helper removes only those explicit
FY-split/display qualifiers. It is used only when at least one of the compared
names carries a ``(from ...)`` marker; ordinary company-name mismatches remain
rejected.
"""
text = _text(value).strip().casefold()
text = re.sub(r"\s*[-–—]?\s*\(\s*from\s+[^)]*\)\s*$", "", text).strip()
text = re.sub(r"[\s_-]+(?:fy\s*)?\d{2,4}\s*[-/]\s*\d{2,4}\s*$", "", text).strip()
text = re.sub(r"\s+audit\s*$", "", text).strip()
text = re.sub(r"[^a-z0-9]+", " ", text)
return re.sub(r"\s+", " ", text).strip()
def _has_tally_from_period_marker(value: Any) -> bool:
return bool(re.search(r"\(\s*from\s+[^)]*\)\s*$", _text(value), flags=re.IGNORECASE))
def _safe_key(value: str) -> str: def _safe_key(value: str) -> str:
text = re.sub(r"[^A-Za-z0-9_.-]+", "_", _text(value)).strip("_") text = re.sub(r"[^A-Za-z0-9_.-]+", "_", _text(value)).strip("_")
return text[:80] or "company" return text[:80] or "company"
@@ -527,12 +550,35 @@ class AccountingMirrorEngine:
if exp_guid and actual_guid_is_placeholder: if exp_guid and actual_guid_is_placeholder:
expected_name_key = _company_identity_name(exp_name) expected_name_key = _company_identity_name(exp_name)
actual_name_key = _company_identity_name(actual_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: names_match = bool(expected_name_key and actual_name_key and expected_name_key == actual_name_key)
# FY-split Tally companies can legitimately have a different display
# suffix, for example ERP ``RAHMAN RAHIM TRADERS Audit`` versus ODBC
# ``RAHMAN RAHIM TRADERS - (from 1-Apr-22)``. Accept that narrow case
# only when Tally explicitly exposes a '(from ...)' period marker and
# the underlying company-family name still matches. This keeps the
# existing safety rejection for unrelated company names.
fy_split_match = False
if not names_match and (_has_tally_from_period_marker(exp_name) or _has_tally_from_period_marker(actual_name)):
expected_family = _company_fy_split_identity_name(exp_name)
actual_family = _company_fy_split_identity_name(actual_name)
fy_split_match = bool(expected_family and actual_family and expected_family == actual_family)
if not names_match and not fy_split_match:
raise RuntimeError( raise RuntimeError(
"Tally ODBC did not return a usable company GUID " "Tally ODBC did not return a usable company GUID "
f"(returned '{actual_guid or 'blank'}') and the company name could not safely " 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}'." f"confirm the saved mapping. ERP expected '{exp_name}', ODBC returned '{actual_name}'."
) )
if fy_split_match:
validation_warnings.append(
"Tally ODBC returned a selector token instead of the real company GUID "
f"('{actual_guid or 'blank'}'). The mirror was accepted as an FY-split Tally company "
f"because ERP '{exp_name}' and ODBC '{actual_name}' resolve to the same base company name. "
"The saved ERP GUID was preserved."
)
else:
validation_warnings.append( validation_warnings.append(
"Tally ODBC returned a selector token instead of the real company GUID " "Tally ODBC returned a selector token instead of the real company GUID "
f"('{actual_guid or 'blank'}'). The mirror was accepted because the normalized " f"('{actual_guid or 'blank'}'). The mirror was accepted because the normalized "
@@ -790,6 +836,13 @@ class AccountingMirrorEngine:
current_name = _text(c.get("company_name")) current_name = _text(c.get("company_name"))
current_guid = _text(c.get("company_guid")) current_guid = _text(c.get("company_guid"))
# The storage path is already bound by the ERP to one client + one FY.
# Analytical queries (including Sundry Creditors follow-up) intentionally
# do not need a live/current Tally company. If no company identity was
# supplied, the ready mirror in that bound FY folder is authoritative.
if current.get("ready") and not requested_guid and not requested_name:
return paths.current_mirror
# GUID is authoritative and survives Tally company renames / FY suffix changes. # GUID is authoritative and survives Tally company renames / FY suffix changes.
if current.get("ready"): if current.get("ready"):
if requested_guid and current_guid and requested_guid == current_guid: if requested_guid and current_guid and requested_guid == current_guid: