Fix Tally ODBC selector GUID and workspace FY mirror defaults
This commit is contained in:
@@ -316,6 +316,7 @@
|
|||||||
<option value="{{ fy }}" {% if fy == selected_financial_year %}selected{% endif %}>FY {{ fy }}</option>
|
<option value="{{ fy }}" {% if fy == selected_financial_year %}selected{% endif %}>FY {{ fy }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<span class="mt-1 block text-xs text-slate-500">Defaults to the ERP workspace FY (FY {{ workspace_financial_year }}). You can still choose another FY to create or refresh its separate mirror.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="text-sm font-medium text-slate-700">Mapped Tally Company
|
<label class="text-sm font-medium text-slate-700">Mapped Tally Company
|
||||||
<select name="tally_guid" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
<select name="tally_guid" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||||||
|
|||||||
@@ -67,6 +67,19 @@ def _financial_year_for_date(value: date | None = None) -> str:
|
|||||||
return f"{start_year}-{str(start_year + 1)[-2:]}"
|
return f"{start_year}-{str(start_year + 1)[-2:]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _active_financial_year(request: Request) -> str:
|
||||||
|
"""Return the FY selected in the ERP workspace header."""
|
||||||
|
value = (
|
||||||
|
request.session.get("active_financial_year")
|
||||||
|
or getattr(request.state, "year_code", None)
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
value = str(value or "").strip()
|
||||||
|
if re.fullmatch(r"\d{4}-\d{2}", value):
|
||||||
|
return value
|
||||||
|
return _financial_year_for_date()
|
||||||
|
|
||||||
|
|
||||||
def _financial_year_bounds(financial_year: str) -> tuple[date, date]:
|
def _financial_year_bounds(financial_year: str) -> tuple[date, date]:
|
||||||
match = re.fullmatch(r"(\d{4})-(\d{2})", str(financial_year or "").strip())
|
match = re.fullmatch(r"(\d{4})-(\d{2})", str(financial_year or "").strip())
|
||||||
if not match:
|
if not match:
|
||||||
@@ -237,13 +250,14 @@ def tally_tool(
|
|||||||
live_result = None
|
live_result = None
|
||||||
analysis_history = []
|
analysis_history = []
|
||||||
command_error = error or ""
|
command_error = error or ""
|
||||||
selected_financial_year = str(financial_year or _financial_year_for_date()).strip()
|
workspace_financial_year = _active_financial_year(request)
|
||||||
|
selected_financial_year = str(financial_year or workspace_financial_year).strip()
|
||||||
try:
|
try:
|
||||||
selected_fy_start, selected_fy_end = _financial_year_bounds(selected_financial_year)
|
selected_fy_start, selected_fy_end = _financial_year_bounds(selected_financial_year)
|
||||||
except Exception:
|
except Exception:
|
||||||
selected_financial_year = _financial_year_for_date()
|
selected_financial_year = workspace_financial_year
|
||||||
selected_fy_start, selected_fy_end = _financial_year_bounds(selected_financial_year)
|
selected_fy_start, selected_fy_end = _financial_year_bounds(selected_financial_year)
|
||||||
mirror_fy_options = _financial_year_options(_financial_year_for_date(), 7)
|
mirror_fy_options = _financial_year_options(workspace_financial_year, 7)
|
||||||
should_query_agent = bool(refresh or selected_client)
|
should_query_agent = bool(refresh or selected_client)
|
||||||
|
|
||||||
if refresh:
|
if refresh:
|
||||||
@@ -309,6 +323,7 @@ def tally_tool(
|
|||||||
transaction_date_from=transaction_date_from,
|
transaction_date_from=transaction_date_from,
|
||||||
transaction_date_to=transaction_date_to,
|
transaction_date_to=transaction_date_to,
|
||||||
selected_financial_year=selected_financial_year,
|
selected_financial_year=selected_financial_year,
|
||||||
|
workspace_financial_year=workspace_financial_year,
|
||||||
mirror_fy_options=mirror_fy_options,
|
mirror_fy_options=mirror_fy_options,
|
||||||
command_error=command_error,
|
command_error=command_error,
|
||||||
analysis_history=analysis_history,
|
analysis_history=analysis_history,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.26.0"
|
ERP_LOCAL_AGENT_VERSION = "1.26.1"
|
||||||
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.0"
|
__version__ = "1.26.1"
|
||||||
AGENT_NAME = "ERP Local Agent"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -31,6 +31,36 @@ def _number(value: Any) -> float:
|
|||||||
m = re.search(r"[-+]?\d+(?:\.\d+)?", s)
|
m = re.search(r"[-+]?\d+(?:\.\d+)?", s)
|
||||||
return float(m.group(0)) if m else 0.0
|
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:
|
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"
|
||||||
@@ -361,15 +391,34 @@ class AccountingMirrorEngine:
|
|||||||
exp_name = _text(expected_company_name)
|
exp_name = _text(expected_company_name)
|
||||||
exp_guid = _text(expected_company_guid)
|
exp_guid = _text(expected_company_guid)
|
||||||
validation_warnings: list[str] = []
|
validation_warnings: list[str] = []
|
||||||
# The Tally GUID is the permanent company identity. Company names are descriptive
|
# A genuine Tally GUID remains authoritative. Some ODBC builds return a
|
||||||
# snapshots only and commonly contain FY/year suffixes (for example "-25-26").
|
# selector token such as ACTIVE_COMPANY instead of the real GUID.
|
||||||
# Never reject a valid mirror merely because the descriptive name changed.
|
actual_guid_is_placeholder = _is_placeholder_company_guid(actual_guid)
|
||||||
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_guid and actual_guid and not actual_guid_is_placeholder and exp_guid != actual_guid:
|
||||||
if exp_name and actual_name and actual_name.casefold() != exp_name.casefold():
|
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(
|
validation_warnings.append(
|
||||||
f"Tally company name differs from the ERP snapshot: ERP '{exp_name}', Tally '{actual_name}'. "
|
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))
|
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()
|
dst.close(); src.close()
|
||||||
temp.replace(paths.current_mirror)
|
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)
|
shutil.copy2(paths.current_mirror, cache)
|
||||||
result = {
|
result = {
|
||||||
"ok": True, "synced_at_utc": _utc_now(), "company_name": actual_name,
|
"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,
|
"validation_warnings": validation_warnings,
|
||||||
"extractor_log_tail": log[-1500:], "loader_log_tail": llog[-1500:],
|
"extractor_log_tail": log[-1500:], "loader_log_tail": llog[-1500:],
|
||||||
**self.status(client_id),
|
**self.status(client_id),
|
||||||
|
|||||||
Reference in New Issue
Block a user