Fix cross-FY Accounting Mirror creditor follow-up
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.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,17 +550,40 @@ 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}'."
|
||||||
)
|
)
|
||||||
validation_warnings.append(
|
|
||||||
"Tally ODBC returned a selector token instead of the real company GUID "
|
if fy_split_match:
|
||||||
f"('{actual_guid or 'blank'}'). The mirror was accepted because the normalized "
|
validation_warnings.append(
|
||||||
"company name matches the saved ERP mapping. The saved ERP GUID was preserved."
|
"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(
|
||||||
|
"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):
|
if exp_name and actual_name and _company_identity_name(actual_name) != _company_identity_name(exp_name):
|
||||||
validation_warnings.append(
|
validation_warnings.append(
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user