Fix depreciation mirror fallback and TDS compliance routing

This commit is contained in:
A R R R Associates
2026-09-06 20:35:20 +05:30
parent 6e0abe7909
commit 739973e370
6 changed files with 139 additions and 12 deletions
+8
View File
@@ -130,6 +130,14 @@ async def update_rule(request: Request, rule_id:int):
finally: db.close()
@router.get("")
@router.get("/")
def tds_root(request: Request, client_id: int | None = None):
"""Backward-compatible TDS landing route."""
suffix = f"?client_id={int(client_id)}" if client_id else ""
return RedirectResponse(url="/tools/accounting/tds/compliance" + suffix, status_code=302)
@router.get("/compliance")
def compliance(request:Request,client_id:int|None=None,tally_guid:str="",date_from:str="",date_to:str="",run:int=0):
db=CommonSessionLocal()
@@ -25,7 +25,7 @@
<select name="client_id" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2" onchange="this.form.submit()">
<option value="">Select client</option>
{% for client in clients %}
<option value="{{ client.id }}" {% if selected_client and selected_client.id==client.id %}selected{% endif %}>{{ client.client_code }} · {{ client.client_name }}</option>
<option value="{{ client.id }}" {% if selected_client and selected_client.id==client.id %}selected{% endif %}>{{ client.client_name }}{% if client.client_code %} · {{ client.client_code }}{% endif %}</option>
{% endfor %}
</select>
</label>
@@ -48,10 +48,10 @@
<a href="/tools/tally/depreciation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">Depreciation (IT)</a>
<a href="/tools/accounting/cash-payments{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-orange-300 bg-white px-3 py-2 text-sm font-semibold text-orange-800">Cash Payment Compliance</a>
<a href="/tools/accounting/cash-payments/allocation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-amber-300 bg-white px-3 py-2 text-sm font-semibold text-amber-800">Cash Payment Entry Allocation</a>
<a href="/tools/accounting/tds{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-violet-300 bg-white px-3 py-2 text-sm font-semibold text-violet-800">TDS Compliance Review</a>
<a href="/tools/accounting/tds/rules" class="rounded-xl border border-violet-200 bg-white px-3 py-2 text-sm font-semibold text-violet-700">TDS Rules</a>
<a href="/tools/accounting/tds/liability{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-violet-200 bg-white px-3 py-2 text-sm font-semibold text-violet-700">TDS Liability</a>
<a href="/tools/accounting/tds/reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-violet-200 bg-white px-3 py-2 text-sm font-semibold text-violet-700">TDS Reconciliation</a>
<a href="/tools/accounting/tds/compliance{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-brand-600 bg-brand-600 px-3 py-2 text-sm font-semibold text-white">TDS Compliance Review</a>
<a href="/tools/accounting/tds/rules" class="rounded-xl border border-brand-300 bg-brand-50 px-3 py-2 text-sm font-semibold text-brand-700">TDS Rules</a>
<a href="/tools/accounting/tds/liability{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-brand-300 bg-brand-50 px-3 py-2 text-sm font-semibold text-brand-700">TDS Liability</a>
<a href="/tools/accounting/tds/reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-brand-300 bg-brand-50 px-3 py-2 text-sm font-semibold text-brand-700">TDS Reconciliation</a>
<a href="/tools/accounting/ai{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-indigo-300 bg-indigo-50 px-3 py-2 text-sm font-semibold text-indigo-800">Accounting AI</a>
<a href="/tools/accounting/internal-model" class="rounded-xl border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800">Internal Model</a>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path
import zipfile
ERP_LOCAL_AGENT_VERSION = "1.25.0"
ERP_LOCAL_AGENT_VERSION = "1.25.2"
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,2 +1,2 @@
__version__ = "1.25.0"
__version__ = "1.25.2"
AGENT_NAME = "ERP Local Agent"
@@ -796,9 +796,112 @@ class LocalAccountingStore:
# flagged only when fewer than 180 calendar days remain including that date.
return ((fy_end - acquired).days + 1) < 180
@staticmethod
def _looks_like_fixed_asset_group(name: str) -> bool:
"""Conservative fallback for mirrors that do not expose Tally Group Master.
The proven ODBC mirror v3.0.1 always contains each ledger's parent group,
but intentionally does not depend on the later Group Master TDL that caused
the v3.1 loading failure. Prefer the real group hierarchy when available;
otherwise recognise standard fixed-asset parent/subgroup names from the
mirrored ledger rows themselves.
"""
value = re.sub(r"[^a-z0-9]+", " ", str(name or "").strip().casefold()).strip()
if not value:
return False
exact = {
"fixed assets",
"fixed asset",
"plant machinery",
"plant and machinery",
"machinery",
"furniture fixtures",
"furniture and fixtures",
"furniture fittings",
"furniture and fittings",
"office equipment",
"office equipments",
"computer",
"computers",
"computer equipment",
"computer equipments",
"computer peripherals",
"computer and peripherals",
"vehicles",
"vehicle",
"motor vehicles",
"motor vehicle",
"motor cars",
"motor car",
"building",
"buildings",
"factory building",
"office building",
"land",
"electrical equipment",
"electrical equipments",
"tools equipment",
"tools and equipment",
"factory equipment",
"factory equipments",
"leasehold improvements",
"leasehold improvement",
"intangible assets",
"intangible asset",
"software",
"software assets",
}
if value in exact:
return True
excluded = {
"current assets",
"current asset",
"loans advances asset",
"loans and advances asset",
"bank accounts",
"bank account",
"cash in hand",
"stock in hand",
"sundry debtors",
"deposits asset",
"investments",
}
if value in excluded:
return False
# Common descriptive subgroup names. Keep this deliberately narrower
# than a generic "contains asset" test so current assets are not pulled
# into the depreciation schedule.
keywords = (
"plant machinery",
"plant and machinery",
"furniture fixture",
"office equipment",
"computer equipment",
"computer peripheral",
"motor vehicle",
"motor car",
"factory equipment",
"electrical equipment",
"leasehold improvement",
"fixed asset",
)
return any(token in value for token in keywords)
def _fixed_asset_group_names(self, db: sqlite3.Connection, tally_guid: str) -> set[str]:
rows = db.execute("SELECT name, parent FROM tally_groups WHERE tally_guid=?", (tally_guid,)).fetchall()
all_groups = {str(r["name"] or "").strip(): str(r["parent"] or "").strip() for r in rows}
rows = db.execute(
"SELECT name, parent FROM tally_groups WHERE tally_guid=?",
(tally_guid,),
).fetchall()
all_groups = {
str(r["name"] or "").strip(): str(r["parent"] or "").strip()
for r in rows
if str(r["name"] or "").strip()
}
# Preferred path: full Tally group hierarchy is available.
fixed = {name for name in all_groups if name.casefold() == "fixed assets"}
changed = True
while changed:
@@ -806,9 +909,25 @@ class LocalAccountingStore:
fixed_fold = {x.casefold() for x in fixed}
for name, parent in all_groups.items():
if name not in fixed and parent.casefold() in fixed_fold:
fixed.add(name); changed = True
fixed.add(name)
changed = True
if fixed:
return fixed
# Proven ODBC mirror fallback: group_master may intentionally be absent,
# but every mirrored ledger still carries its parent group.
ledger_parents = db.execute(
"""SELECT DISTINCT TRIM(COALESCE(parent,'')) AS parent
FROM tally_ledgers
WHERE tally_guid=? AND TRIM(COALESCE(parent,''))<>''""",
(tally_guid,),
).fetchall()
return {
str(row["parent"] or "").strip()
for row in ledger_parents
if self._looks_like_fixed_asset_group(str(row["parent"] or ""))
}
def _transaction_coverage(self, db: sqlite3.Connection, tally_guid: str, fy_start: date, fy_end: date) -> dict:
today = date.today()
required_end = min(fy_end, today)
@@ -857,7 +976,7 @@ class LocalAccountingStore:
with self.connect(client_id) as db:
fixed_groups = self._fixed_asset_group_names(db, tally_guid)
if not fixed_groups:
raise ValueError("Fixed Assets group was not found in synchronized Tally masters. Synchronize Phase 3 masters first.")
raise ValueError("No Fixed Asset ledger/group could be identified in the local Accounting Mirror. Verify that fixed-asset ledgers are grouped under Fixed Assets (or a standard fixed-asset subgroup) in Tally, then refresh the mirror.")
ledgers = db.execute(
"SELECT master_guid, name, parent, opening_balance, closing_balance, payload_json FROM tally_ledgers WHERE tally_guid=? ORDER BY name COLLATE NOCASE",
(tally_guid,),