Enrich ERP and Tally GSTIN display
This commit is contained in:
@@ -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">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">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-right font-semibold text-slate-600">Action</th>
|
||||
</tr>
|
||||
@@ -280,7 +282,25 @@
|
||||
</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 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">
|
||||
<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' }}
|
||||
@@ -314,7 +334,7 @@
|
||||
{% if tally.companies %}
|
||||
<div class="overflow-x-auto">
|
||||
<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">
|
||||
{% for company in tally.companies %}
|
||||
<tr>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = "1.9.0"
|
||||
__version__ = "1.9.4"
|
||||
AGENT_NAME = "ERP Local Agent"
|
||||
|
||||
@@ -201,7 +201,7 @@ class TallyLiveConnector:
|
||||
<BODY><DESC>
|
||||
<STATICVARIABLES><SVEXPORTFORMAT>$$SysName:XML</SVEXPORTFORMAT></STATICVARIABLES>
|
||||
<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>
|
||||
</DESC></BODY>
|
||||
</ENVELOPE>"""
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user