Enrich ERP and Tally GSTIN display

This commit is contained in:
A R R R Associates
2026-08-20 21:24:35 +05:30
parent de8b32b06d
commit fa0f681106
6 changed files with 117 additions and 8 deletions
@@ -261,7 +261,9 @@
<th class="px-4 py-3 text-left font-semibold text-slate-600">ERP Scope</th> <th class="px-4 py-3 text-left font-semibold text-slate-600">ERP Scope</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600">Tally Company</th> <th class="px-4 py-3 text-left font-semibold text-slate-600">Tally Company</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600">GUID</th> <th class="px-4 py-3 text-left font-semibold text-slate-600">GUID</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600">GSTIN</th> <th class="px-4 py-3 text-left font-semibold text-slate-600">ERP GSTIN</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600">Tally GSTIN</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600">GSTIN Match</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600">Loaded</th> <th class="px-4 py-3 text-left font-semibold text-slate-600">Loaded</th>
<th class="px-4 py-3 text-right font-semibold text-slate-600">Action</th> <th class="px-4 py-3 text-right font-semibold text-slate-600">Action</th>
</tr> </tr>
@@ -280,7 +282,25 @@
</td> </td>
<td class="px-4 py-3 font-medium text-slate-900">{{ mapping.company_name }}</td> <td class="px-4 py-3 font-medium text-slate-900">{{ mapping.company_name }}</td>
<td class="px-4 py-3 font-mono text-xs text-slate-600">{{ mapping.tally_guid }}</td> <td class="px-4 py-3 font-mono text-xs text-slate-600">{{ mapping.tally_guid }}</td>
<td class="px-4 py-3 text-slate-600">{{ mapping.gstin or '-' }}</td> <td class="px-4 py-3 text-slate-600">
{% if mapping.erp_gstins %}
{% for gstin in mapping.erp_gstins %}<div class="font-mono text-xs">{{ gstin }}</div>{% endfor %}
{% else %}-{% endif %}
</td>
<td class="px-4 py-3 font-mono text-xs text-slate-600">{{ mapping.tally_gstin_display or '-' }}</td>
<td class="px-4 py-3">
{% if mapping.gstin_match_status == 'matched' %}
<span class="rounded-full bg-emerald-50 px-2 py-1 text-xs font-semibold text-emerald-700">Matched</span>
{% elif mapping.gstin_match_status == 'mismatch' %}
<span class="rounded-full bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Mismatch</span>
{% elif mapping.gstin_match_status == 'tally_missing' %}
<span class="rounded-full bg-amber-50 px-2 py-1 text-xs font-semibold text-amber-700">Tally GSTIN unavailable</span>
{% elif mapping.gstin_match_status == 'erp_missing' %}
<span class="rounded-full bg-amber-50 px-2 py-1 text-xs font-semibold text-amber-700">ERP GSTIN unavailable</span>
{% else %}
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-600">Unavailable</span>
{% endif %}
</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<span class="rounded-full px-2 py-1 text-xs font-semibold {% if mapping.currently_loaded %}bg-emerald-50 text-emerald-700{% else %}bg-slate-100 text-slate-600{% endif %}"> <span class="rounded-full px-2 py-1 text-xs font-semibold {% if mapping.currently_loaded %}bg-emerald-50 text-emerald-700{% else %}bg-slate-100 text-slate-600{% endif %}">
{{ 'Yes' if mapping.currently_loaded else 'No' }} {{ 'Yes' if mapping.currently_loaded else 'No' }}
@@ -314,7 +334,7 @@
{% if tally.companies %} {% if tally.companies %}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-200 text-sm"> <table class="min-w-full divide-y divide-slate-200 text-sm">
<thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left font-semibold text-slate-600">Company</th><th class="px-4 py-3 text-left font-semibold text-slate-600">GUID</th><th class="px-4 py-3 text-left font-semibold text-slate-600">GSTIN</th></tr></thead> <thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left font-semibold text-slate-600">Company</th><th class="px-4 py-3 text-left font-semibold text-slate-600">GUID</th><th class="px-4 py-3 text-left font-semibold text-slate-600">Tally GSTIN</th></tr></thead>
<tbody class="divide-y divide-slate-100"> <tbody class="divide-y divide-slate-100">
{% for company in tally.companies %} {% for company in tally.companies %}
<tr> <tr>
+55
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
from pathlib import Path from pathlib import Path
import re
from urllib.parse import quote from urllib.parse import quote
from fastapi import APIRouter, Form, Request 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: def _node_online(node) -> bool:
if not node or not node.last_seen_at_utc: if not node or not node.last_seen_at_utc:
return False return False
@@ -175,6 +229,7 @@ def tally_tool(
) )
if response_data.get("ok"): if response_data.get("ok"):
live_result = response_data.get("result") or {} live_result = response_data.get("result") or {}
_enrich_gstin_display(live_result, registrations)
else: else:
command_error = str(response_data.get("error") or "Local agent command failed.") command_error = str(response_data.get("error") or "Local agent command failed.")
except Exception as exc: except Exception as exc:
+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.9.0" ERP_LOCAL_AGENT_VERSION = "1.9.4"
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,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. 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. - 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. - 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. - 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.
@@ -1,2 +1,2 @@
__version__ = "1.9.0" __version__ = "1.9.4"
AGENT_NAME = "ERP Local Agent" AGENT_NAME = "ERP Local Agent"
@@ -201,7 +201,7 @@ class TallyLiveConnector:
<BODY><DESC> <BODY><DESC>
<STATICVARIABLES><SVEXPORTFORMAT>$$SysName:XML</SVEXPORTFORMAT></STATICVARIABLES> <STATICVARIABLES><SVEXPORTFORMAT>$$SysName:XML</SVEXPORTFORMAT></STATICVARIABLES>
<TDL><TDLMESSAGE> <TDL><TDLMESSAGE>
<COLLECTION NAME="ARRRAccountingLoadedCompanies" ISMODIFY="No"><TYPE>Company</TYPE><FETCH>Name,GUID,GSTRegistrationNumber</FETCH></COLLECTION> <COLLECTION NAME="ARRRAccountingLoadedCompanies" ISMODIFY="No"><TYPE>Company</TYPE><FETCH>Name,GUID,GSTRegistrationNumber,GSTRegistrationDetails,GSTIN,PartyGSTIN</FETCH></COLLECTION>
</TDLMESSAGE></TDL> </TDLMESSAGE></TDL>
</DESC></BODY> </DESC></BODY>
</ENVELOPE>""" </ENVELOPE>"""
@@ -563,5 +563,15 @@ class TallyLiveConnector:
if not name or key in seen: if not name or key in seen:
continue continue
seen.add(key) 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 return companies