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()
|
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")
|
@router.get("/compliance")
|
||||||
def compliance(request:Request,client_id:int|None=None,tally_guid:str="",date_from:str="",date_to:str="",run:int=0):
|
def compliance(request:Request,client_id:int|None=None,tally_guid:str="",date_from:str="",date_to:str="",run:int=0):
|
||||||
db=CommonSessionLocal()
|
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()">
|
<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>
|
<option value="">Select client</option>
|
||||||
{% for client in clients %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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/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{% 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/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/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-violet-200 bg-white px-3 py-2 text-sm font-semibold text-violet-700">TDS Rules</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-violet-200 bg-white px-3 py-2 text-sm font-semibold text-violet-700">TDS Liability</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-violet-200 bg-white px-3 py-2 text-sm font-semibold text-violet-700">TDS Reconciliation</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/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>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.25.0"
|
ERP_LOCAL_AGENT_VERSION = "1.25.2"
|
||||||
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||||
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||||
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
_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"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -796,9 +796,112 @@ class LocalAccountingStore:
|
|||||||
# flagged only when fewer than 180 calendar days remain including that date.
|
# flagged only when fewer than 180 calendar days remain including that date.
|
||||||
return ((fy_end - acquired).days + 1) < 180
|
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]:
|
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()
|
rows = db.execute(
|
||||||
all_groups = {str(r["name"] or "").strip(): str(r["parent"] or "").strip() for r in rows}
|
"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"}
|
fixed = {name for name in all_groups if name.casefold() == "fixed assets"}
|
||||||
changed = True
|
changed = True
|
||||||
while changed:
|
while changed:
|
||||||
@@ -806,8 +909,24 @@ class LocalAccountingStore:
|
|||||||
fixed_fold = {x.casefold() for x in fixed}
|
fixed_fold = {x.casefold() for x in fixed}
|
||||||
for name, parent in all_groups.items():
|
for name, parent in all_groups.items():
|
||||||
if name not in fixed and parent.casefold() in fixed_fold:
|
if name not in fixed and parent.casefold() in fixed_fold:
|
||||||
fixed.add(name); changed = True
|
fixed.add(name)
|
||||||
return fixed
|
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:
|
def _transaction_coverage(self, db: sqlite3.Connection, tally_guid: str, fy_start: date, fy_end: date) -> dict:
|
||||||
today = date.today()
|
today = date.today()
|
||||||
@@ -857,7 +976,7 @@ class LocalAccountingStore:
|
|||||||
with self.connect(client_id) as db:
|
with self.connect(client_id) as db:
|
||||||
fixed_groups = self._fixed_asset_group_names(db, tally_guid)
|
fixed_groups = self._fixed_asset_group_names(db, tally_guid)
|
||||||
if not fixed_groups:
|
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(
|
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",
|
"SELECT master_guid, name, parent, opening_balance, closing_balance, payload_json FROM tally_ledgers WHERE tally_guid=? ORDER BY name COLLATE NOCASE",
|
||||||
(tally_guid,),
|
(tally_guid,),
|
||||||
|
|||||||
Reference in New Issue
Block a user