Fix depreciation mirror fallback and TDS compliance routing
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,),
|
||||
|
||||
Reference in New Issue
Block a user