diff --git a/app/modules/accounting/tds_ui.py b/app/modules/accounting/tds_ui.py
index f0edc00..01dfabc 100644
--- a/app/modules/accounting/tds_ui.py
+++ b/app/modules/accounting/tds_ui.py
@@ -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()
diff --git a/app/modules/accounting/templates/accounting/opening_balances.html b/app/modules/accounting/templates/accounting/opening_balances.html
index 3bf8abd..d40984f 100644
--- a/app/modules/accounting/templates/accounting/opening_balances.html
+++ b/app/modules/accounting/templates/accounting/opening_balances.html
@@ -25,7 +25,7 @@
diff --git a/app/modules/accounting/templates/accounting/tally.html b/app/modules/accounting/templates/accounting/tally.html
index eccc48f..e7d1087 100644
--- a/app/modules/accounting/templates/accounting/tally.html
+++ b/app/modules/accounting/templates/accounting/tally.html
@@ -48,10 +48,10 @@
Depreciation (IT)
Cash Payment Compliance
Cash Payment Entry Allocation
- TDS Compliance Review
- TDS Rules
- TDS Liability
- TDS Reconciliation
+ TDS Compliance Review
+ TDS Rules
+ TDS Liability
+ TDS Reconciliation
Accounting AI
Internal Model
diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py
index ee72c0e..7b2f473 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.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)
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 cb2b376..223190d 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.25.0"
+__version__ = "1.25.2"
AGENT_NAME = "ERP Local Agent"
diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py b/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py
index 7accf31..2cd3b3a 100644
--- a/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py
+++ b/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py
@@ -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,8 +909,24 @@ 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
- return fixed
+ 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()
@@ -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,),