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,),