Add TDS review drilldowns and exclude capital accounts

This commit is contained in:
A R R R Associates
2026-09-06 21:12:14 +05:30
parent cbe594e73b
commit f211d9e6a8
4 changed files with 81 additions and 7 deletions
@@ -6,7 +6,9 @@
<div class="rounded-xl bg-white p-4 shadow-soft"> <div class="rounded-xl bg-white p-4 shadow-soft">
<div class="text-xs uppercase text-slate-500">{{ k|replace('_',' ') }}</div> <div class="text-xs uppercase text-slate-500">{{ k|replace('_',' ') }}</div>
<div class="mt-1 text-xl font-semibold">{{ v }}</div> <div class="mt-1 text-xl font-semibold">{{ v }}</div>
{% if k == 'tds_not_deducted' and v %} {% if k == 'candidate_transactions' and v %}
<button type="button" data-tds-filter-status="" class="mt-2 text-xs font-semibold text-brand-700 underline underline-offset-2">View all transactions</button>
{% elif k == 'tds_not_deducted' and v %}
<button type="button" data-tds-filter-status="TDS NOT DEDUCTED" class="mt-2 text-xs font-semibold text-brand-700 underline underline-offset-2">View non-deduction</button> <button type="button" data-tds-filter-status="TDS NOT DEDUCTED" class="mt-2 text-xs font-semibold text-brand-700 underline underline-offset-2">View non-deduction</button>
{% elif k == 'short_deduction' and v %} {% elif k == 'short_deduction' and v %}
<button type="button" data-tds-filter-status="SHORT DEDUCTION" class="mt-2 text-xs font-semibold text-brand-700 underline underline-offset-2">View short deduction</button> <button type="button" data-tds-filter-status="SHORT DEDUCTION" class="mt-2 text-xs font-semibold text-brand-700 underline underline-offset-2">View short deduction</button>
@@ -42,6 +44,12 @@
<td class="p-3 text-right">{{ row.short_deduction }}</td> <td class="p-3 text-right">{{ row.short_deduction }}</td>
<td class="p-3"> <td class="p-3">
<div class="flex flex-wrap gap-x-3 gap-y-1"> <div class="flex flex-wrap gap-x-3 gap-y-1">
<button type="button"
data-tds-filter-rule="{{ row.rule_code }}"
data-tds-filter-status=""
class="text-xs font-semibold text-brand-700 underline underline-offset-2">
All ({{ row.transactions }})
</button>
{% if row.tds_not_deducted %} {% if row.tds_not_deducted %}
<button type="button" <button type="button"
data-tds-filter-rule="{{ row.rule_code }}" data-tds-filter-rule="{{ row.rule_code }}"
@@ -58,6 +66,22 @@
Short ({{ row.short_deduction }}) Short ({{ row.short_deduction }})
</button> </button>
{% endif %} {% endif %}
{% if row.ok %}
<button type="button"
data-tds-filter-rule="{{ row.rule_code }}"
data-tds-filter-status="OK"
class="text-xs font-semibold text-brand-700 underline underline-offset-2">
Deducted / OK ({{ row.ok }})
</button>
{% endif %}
{% if row.below_threshold %}
<button type="button"
data-tds-filter-rule="{{ row.rule_code }}"
data-tds-filter-status="BELOW / OUTSIDE THRESHOLD"
class="text-xs font-semibold text-brand-700 underline underline-offset-2">
Below threshold ({{ row.below_threshold }})
</button>
{% endif %}
</div> </div>
</td> </td>
</tr> </tr>
@@ -73,7 +97,7 @@
</div> </div>
<button type="button" id="tdsClearFilter" class="hidden rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700">Show all</button> <button type="button" id="tdsClearFilter" class="hidden rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700">Show all</button>
</div> </div>
<table id="tdsTransactionTable" class="min-w-full text-sm"><thead class="bg-slate-50"><tr><th class="p-3 text-left">Date</th><th class="p-3 text-left">Party</th><th class="p-3 text-left">Ledger / Rule</th><th class="p-3 text-right">Base</th><th class="p-3 text-right">Expected</th><th class="p-3 text-right">Actual</th><th class="p-3 text-right">Liability</th><th class="p-3 text-left">Status</th></tr></thead><tbody>{% for x in review.transactions %}<tr class="border-t" data-tds-status="{{ x.status }}" data-tds-rule="{{ x.rule_code }}"><td class="p-3">{{ x.date }}</td><td class="p-3">{{ x.party }}</td><td class="p-3">{{ x.nature_ledger }}<div class="text-xs text-slate-500">{{ x.rule_name }} · {{ x.legacy_section }}</div></td><td class="p-3 text-right">{{ '%.2f'|format(x.taxable_base) }}</td><td class="p-3 text-right">{{ '%.2f'|format(x.expected_tds) }}</td><td class="p-3 text-right">{{ '%.2f'|format(x.actual_tds) }}</td><td class="p-3 text-right font-semibold">{{ '%.2f'|format(x.liability) }}</td><td class="p-3">{{ x.status }}</td></tr>{% endfor %}</tbody></table></div>{% endif %}{% if history %}<div class="rounded-2xl bg-white p-5 shadow-soft"><h2 class="font-semibold">TDS Analysis History</h2><div class="mt-3 overflow-x-auto"><table class="min-w-full text-sm"><tbody>{% for row in history %}<tr class="border-t"><td class="p-2">#{{ row.id }}</td><td class="p-2">{{ row.financial_year }}</td><td class="p-2">{{ row.period_from }} → {{ row.period_to }}</td><td class="p-2 text-right"><a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=xlsx">Excel</a> · <a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=pdf">PDF</a></td></tr>{% endfor %}</tbody></table></div></div>{% endif %}</div> <table id="tdsTransactionTable" class="min-w-full text-sm"><thead class="bg-slate-50"><tr><th class="p-3 text-left">Date</th><th class="p-3 text-left">Party</th><th class="p-3 text-left">Ledger / Rule</th><th class="p-3 text-right">Base</th><th class="p-3 text-right">Expected</th><th class="p-3 text-right">Actual</th><th class="p-3 text-right">Liability</th><th class="p-3 text-left">Status</th></tr></thead><tbody>{% for x in review.transactions %}<tr class="border-t" data-tds-status="{{ x.status }}" data-tds-rule="{{ x.rule_code }}"><td class="p-3">{{ x.date }}</td><td class="p-3">{{ x.party }}</td><td class="p-3">{{ x.nature_ledger }}<div class="text-xs text-slate-500">{{ x.rule_name }} · {{ x.legacy_section }}{% if x.parent_group %} · Group: {{ x.parent_group }}{% endif %}</div></td><td class="p-3 text-right">{{ '%.2f'|format(x.taxable_base) }}</td><td class="p-3 text-right">{{ '%.2f'|format(x.expected_tds) }}</td><td class="p-3 text-right">{{ '%.2f'|format(x.actual_tds) }}</td><td class="p-3 text-right font-semibold">{{ '%.2f'|format(x.liability) }}</td><td class="p-3">{{ x.status }}</td></tr>{% endfor %}</tbody></table></div>{% endif %}{% if history %}<div class="rounded-2xl bg-white p-5 shadow-soft"><h2 class="font-semibold">TDS Analysis History</h2><div class="mt-3 overflow-x-auto"><table class="min-w-full text-sm"><tbody>{% for row in history %}<tr class="border-t"><td class="p-2">#{{ row.id }}</td><td class="p-2">{{ row.financial_year }}</td><td class="p-2">{{ row.period_from }} → {{ row.period_to }}</td><td class="p-2 text-right"><a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=xlsx">Excel</a> · <a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=pdf">PDF</a></td></tr>{% endfor %}</tbody></table></div></div>{% endif %}</div>
{% if review %} {% if review %}
<script> <script>
@@ -96,7 +120,13 @@ document.addEventListener("DOMContentLoaded", function () {
}); });
const pieces = []; const pieces = [];
if (status) pieces.push(status === "TDS NOT DEDUCTED" ? "non-deduction" : "short deduction"); if (status) {
if (status === "TDS NOT DEDUCTED") pieces.push("non-deduction");
else if (status === "SHORT DEDUCTION") pieces.push("short deduction");
else if (status === "OK") pieces.push("deducted / OK");
else if (status === "BELOW / OUTSIDE THRESHOLD") pieces.push("below / outside threshold");
else pieces.push(status.toLowerCase());
}
if (rule) pieces.push("selected category"); if (rule) pieces.push("selected category");
label.textContent = "Showing " + visible + " transaction(s)" + (pieces.length ? " for " + pieces.join(" · ") + "." : "."); label.textContent = "Showing " + visible + " transaction(s)" + (pieces.length ? " for " + pieces.join(" · ") + "." : ".");
clear.classList.toggle("hidden", !status && !rule); clear.classList.toggle("hidden", !status && !rule);
+1 -1
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path from pathlib import Path
import zipfile import zipfile
ERP_LOCAL_AGENT_VERSION = "1.25.4" ERP_LOCAL_AGENT_VERSION = "1.25.5"
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.4" __version__ = "1.25.5"
AGENT_NAME = "ERP Local Agent" AGENT_NAME = "ERP Local Agent"
@@ -3142,13 +3142,54 @@ class AgentCommandProcessor:
# TDS ledgers are identified from the local mirror ledger master only. # TDS ledgers are identified from the local mirror ledger master only.
masters = (self.tally.mirror.master_snapshot(client_id, company_name, company_guid).get("ledgers") or []) masters = (self.tally.mirror.master_snapshot(client_id, company_name, company_guid).get("ledgers") or [])
tds_ledger_names = set() tds_ledger_names = set()
ledger_parent_by_name: dict[str, str] = {}
for led in masters: for led in masters:
name = str(led.get("name") or "").strip() name = str(led.get("name") or "").strip()
parent = str(led.get("parent") or "").strip().casefold() parent_raw = str(led.get("parent") or "").strip()
parent = parent_raw.casefold()
if name:
ledger_parent_by_name[name.casefold()] = parent_raw
text = (name + " " + parent).casefold() text = (name + " " + parent).casefold()
if name and ("tds" in text or "tax deducted" in text or "duties & taxes" in parent and "income tax" in text): if name and ("tds" in text or "tax deducted" in text or "duties & taxes" in parent and "income tax" in text):
tds_ledger_names.add(name.casefold()) tds_ledger_names.add(name.casefold())
def _capital_account_parent(parent_name: str) -> bool:
"""Exclude owners'/partners' capital-account ledgers from TDS candidates.
This does not exclude fixed assets or capital expenditure merely because
they are assets. Only ledgers whose immediate Tally parent is a capital /
owners' equity style group are ignored.
"""
value = re.sub(r"[^a-z0-9]+", " ", str(parent_name or "").casefold()).strip()
if not value:
return False
exact = {
"capital account",
"capital accounts",
"partners capital",
"partner capital",
"partners capital accounts",
"partner capital accounts",
"proprietors capital",
"proprietor capital",
"owners capital",
"owner capital",
"share capital",
"reserves surplus",
"reserves and surplus",
}
if value in exact:
return True
return (
value.startswith("capital account ")
or value.endswith(" capital account")
or value.endswith(" capital accounts")
or value.startswith("partners capital")
or value.startswith("partner capital")
or value.startswith("proprietor capital")
or value.startswith("owner capital")
)
candidates = [] candidates = []
for v in vouchers: for v in vouchers:
if str(v.get("is_cancelled") or "").lower() in {"yes","true","1"} or str(v.get("is_optional") or "").lower() in {"yes","true","1"}: if str(v.get("is_cancelled") or "").lower() in {"yes","true","1"} or str(v.get("is_optional") or "").lower() in {"yes","true","1"}:
@@ -3162,6 +3203,9 @@ class AgentCommandProcessor:
amount = abs(float(e.get("amount") or 0)) amount = abs(float(e.get("amount") or 0))
if not lname or amount <= 0 or (party and lname.casefold() == party.casefold()): if not lname or amount <= 0 or (party and lname.casefold() == party.casefold()):
continue continue
parent_group = str(ledger_parent_by_name.get(lname.casefold()) or "").strip()
if _capital_account_parent(parent_group):
continue
text = lname.casefold() text = lname.casefold()
matches=[] matches=[]
for r in rules: for r in rules:
@@ -3170,7 +3214,7 @@ class AgentCommandProcessor:
if not matches: continue if not matches: continue
# More specific/longer keyword match first; user can refine keywords in Rule Master. # More specific/longer keyword match first; user can refine keywords in Rule Master.
rule=max(matches,key=lambda r:max([len(k.strip()) for k in str(r.get("nature_keywords") or "").split(',') if k.strip()] or [0])) rule=max(matches,key=lambda r:max([len(k.strip()) for k in str(r.get("nature_keywords") or "").split(',') if k.strip()] or [0]))
candidates.append({"date":str(v.get("date") or v.get("effective_date") or ""),"party":party or "Unidentified party","nature_ledger":lname,"taxable_base":round(amount,2),"actual_tds":actual_tds,"voucher_number":str(v.get("voucher_number") or ""),"voucher_type":str(v.get("voucher_type_name") or ""),"reference":str(v.get("reference") or ""),"rule":rule}) candidates.append({"date":str(v.get("date") or v.get("effective_date") or ""),"party":party or "Unidentified party","nature_ledger":lname,"parent_group":parent_group,"taxable_base":round(amount,2),"actual_tds":actual_tds,"voucher_number":str(v.get("voucher_number") or ""),"voucher_type":str(v.get("voucher_type_name") or ""),"reference":str(v.get("reference") or ""),"rule":rule})
by_party_rule=defaultdict(list) by_party_rule=defaultdict(list)
for x in candidates: by_party_rule[(x["party"].casefold(),str(x["rule"].get("rule_code")))].append(x) for x in candidates: by_party_rule[(x["party"].casefold(),str(x["rule"].get("rule_code")))].append(x)