diff --git a/app/modules/accounting/templates/accounting/tally.html b/app/modules/accounting/templates/accounting/tally.html
index f3029c8..5e88099 100644
--- a/app/modules/accounting/templates/accounting/tally.html
+++ b/app/modules/accounting/templates/accounting/tally.html
@@ -261,7 +261,9 @@
ERP Scope |
Tally Company |
GUID |
- GSTIN |
+ ERP GSTIN |
+ Tally GSTIN |
+ GSTIN Match |
Loaded |
Action |
@@ -280,7 +282,25 @@
{{ mapping.company_name }} |
{{ mapping.tally_guid }} |
- {{ mapping.gstin or '-' }} |
+
+ {% if mapping.erp_gstins %}
+ {% for gstin in mapping.erp_gstins %} {{ gstin }} {% endfor %}
+ {% else %}-{% endif %}
+ |
+ {{ mapping.tally_gstin_display or '-' }} |
+
+ {% if mapping.gstin_match_status == 'matched' %}
+ Matched
+ {% elif mapping.gstin_match_status == 'mismatch' %}
+ Mismatch
+ {% elif mapping.gstin_match_status == 'tally_missing' %}
+ Tally GSTIN unavailable
+ {% elif mapping.gstin_match_status == 'erp_missing' %}
+ ERP GSTIN unavailable
+ {% else %}
+ Unavailable
+ {% endif %}
+ |
{{ 'Yes' if mapping.currently_loaded else 'No' }}
@@ -314,7 +334,7 @@
{% if tally.companies %}
- | Company | GUID | GSTIN |
+ | Company | GUID | Tally GSTIN |
{% for company in tally.companies %}
diff --git a/app/modules/accounting/ui.py b/app/modules/accounting/ui.py
index 6edd734..89fab17 100644
--- a/app/modules/accounting/ui.py
+++ b/app/modules/accounting/ui.py
@@ -2,6 +2,7 @@ from __future__ import annotations
from datetime import date, datetime, timezone
from pathlib import Path
+import re
from urllib.parse import quote
from fastapi import APIRouter, Form, Request
@@ -104,6 +105,59 @@ def _registration_payload(registration, registration_type) -> dict:
}
+def _normalise_gstin(value: str) -> str:
+ return re.sub(r"\s+", "", str(value or "").strip().upper())
+
+
+def _erp_client_gstins(registrations) -> list[str]:
+ values: list[str] = []
+ for registration, registration_type in registrations or []:
+ if str(getattr(registration_type, "code", "") or "").strip().upper() != "GSTIN":
+ continue
+ number = _normalise_gstin(getattr(registration, "registration_number", ""))
+ if number and number not in values:
+ values.append(number)
+ return values
+
+
+def _enrich_gstin_display(live_result: dict | None, registrations) -> None:
+ """Attach ERP/Tally GSTIN display metadata without mutating stored mappings."""
+ if not isinstance(live_result, dict):
+ return
+ accounting = live_result.get("accounting") or {}
+ tally = live_result.get("tally") or {}
+ mappings = accounting.get("mappings") or []
+ companies = tally.get("companies") or []
+ client_gstins = _erp_client_gstins(registrations)
+ company_by_guid = {str(row.get("guid") or "").strip(): row for row in companies if isinstance(row, dict)}
+
+ for mapping in mappings:
+ if not isinstance(mapping, dict):
+ continue
+ registration_type = str(mapping.get("registration_type_code") or "").strip().upper()
+ registration_number = _normalise_gstin(mapping.get("registration_number"))
+ if mapping.get("registration_id") and registration_type == "GSTIN" and registration_number:
+ erp_gstins = [registration_number]
+ elif not mapping.get("registration_id"):
+ erp_gstins = list(client_gstins)
+ else:
+ erp_gstins = []
+
+ live_company = company_by_guid.get(str(mapping.get("tally_guid") or "").strip()) or {}
+ tally_gstin = _normalise_gstin(live_company.get("gstin") or mapping.get("gstin"))
+ mapping["erp_gstins"] = erp_gstins
+ mapping["tally_gstin_display"] = tally_gstin
+
+ if erp_gstins and tally_gstin:
+ mapping["gstin_match_status"] = "matched" if tally_gstin in erp_gstins else "mismatch"
+ elif erp_gstins and not tally_gstin:
+ mapping["gstin_match_status"] = "tally_missing"
+ elif tally_gstin and not erp_gstins:
+ mapping["gstin_match_status"] = "erp_missing"
+ else:
+ mapping["gstin_match_status"] = "unavailable"
+
+
def _node_online(node) -> bool:
if not node or not node.last_seen_at_utc:
return False
@@ -175,6 +229,7 @@ def tally_tool(
)
if response_data.get("ok"):
live_result = response_data.get("result") or {}
+ _enrich_gstin_display(live_result, registrations)
else:
command_error = str(response_data.get("error") or "Local agent command failed.")
except Exception as exc:
diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py
index 8120df3..10918e4 100644
--- a/app/modules/documents/agent_package.py
+++ b/app/modules/documents/agent_package.py
@@ -4,7 +4,7 @@ import io
from pathlib import Path
import zipfile
-ERP_LOCAL_AGENT_VERSION = "1.9.0"
+ERP_LOCAL_AGENT_VERSION = "1.9.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)
diff --git a/app/modules/documents/local_agent_runtime/README_ERP_LOCAL_AGENT.txt b/app/modules/documents/local_agent_runtime/README_ERP_LOCAL_AGENT.txt
index 795dc55..d95819a 100644
--- a/app/modules/documents/local_agent_runtime/README_ERP_LOCAL_AGENT.txt
+++ b/app/modules/documents/local_agent_runtime/README_ERP_LOCAL_AGENT.txt
@@ -1,4 +1,4 @@
-ERP Local Agent 1.9.0
+ERP Local Agent 1.9.4
Existing storage, WebSocket tunnel, Tally and client .act functionality are preserved.
@@ -66,3 +66,27 @@ Desktop Supervisor 1.9.0
- Closing the desktop dashboard does NOT stop the supervisor or worker.
- The dashboard automatically reconnects/reloads when the worker goes down and returns during an update.
- Existing browser access at http://127.0.0.1:8788 remains available.
+
+Supervisor detection hotfix 1.9.1:
+- Desktop dashboard no longer relies on os.kill(pid, 0) to detect the SYSTEM-owned supervisor.
+- Uses Windows OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) when available.
+- Uses localhost dashboard health and recent supervisor_state.json as additional valid signals.
+- Prevents false 'ERP Local Agent supervisor did not start' errors caused by cross-user Windows process permissions.
+
+Single-supervisor hotfix 1.9.2:
+- Replaces PID-text singleton logic with an msvcrt held byte-range file lock that works across Windows processes/logon sessions.
+- Duplicate supervisors immediately exit without starting/killing workers.
+- Desktop dashboard never launches an interactive supervisor when the installed ERP Local Agent Scheduled Task exists.
+- If the task exists but is stopped, dashboard requests Task Scheduler to start it and waits for localhost health.
+- Prevents supervisor/worker restart storms caused by multiple supervisors killing one another's workers.
+
+Global mutex hardening 1.9.3:
+- Supervisor primary singleton is Windows Global\ARRR_ERP_Local_Agent_Supervisor via CreateMutexW.
+- Existing byte-range lock remains only as fallback if Global mutex creation is unavailable.
+- Scheduled Task installation is expected to use MultipleInstances=IgnoreNew.
+
+
+Tally GSTIN enrichment 1.9.4:
+- Loaded-company discovery requests GSTRegistrationNumber, GSTRegistrationDetails, GSTIN and PartyGSTIN.
+- Company parser searches GSTIN candidates recursively, including nested GST registration details.
+- Existing company GUID/name discovery remains unchanged.
diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py
index d93f970..cf0d4e4 100644
--- a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py
+++ b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py
@@ -1,2 +1,2 @@
-__version__ = "1.9.0"
+__version__ = "1.9.4"
AGENT_NAME = "ERP Local Agent"
diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/tally.py b/app/modules/documents/local_agent_runtime/erp_local_agent/tally.py
index b266554..709e4e3 100644
--- a/app/modules/documents/local_agent_runtime/erp_local_agent/tally.py
+++ b/app/modules/documents/local_agent_runtime/erp_local_agent/tally.py
@@ -201,7 +201,7 @@ class TallyLiveConnector:
$$SysName:XML
- CompanyName,GUID,GSTRegistrationNumber
+ CompanyName,GUID,GSTRegistrationNumber,GSTRegistrationDetails,GSTIN,PartyGSTIN
"""
@@ -563,5 +563,15 @@ class TallyLiveConnector:
if not name or key in seen:
continue
seen.add(key)
- companies.append(TallyCompany(name=name, guid=_child_text(element, "GUID"), gstin=_child_text(element, "GSTREGISTRATIONNUMBER")))
+ gstin = _first_text(
+ element,
+ (
+ "GSTREGISTRATIONNUMBER",
+ "GSTIN",
+ "PARTYGSTIN",
+ "GSTNO",
+ "GSTNUMBER",
+ ),
+ )
+ companies.append(TallyCompany(name=name, guid=_child_text(element, "GUID"), gstin=gstin))
return companies
|