Add TDS review drilldowns and exclude capital accounts
This commit is contained in:
@@ -6,7 +6,9 @@
|
||||
<div class="rounded-xl bg-white p-4 shadow-soft">
|
||||
<div class="text-xs uppercase text-slate-500">{{ k|replace('_',' ') }}</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>
|
||||
{% 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>
|
||||
@@ -42,6 +44,12 @@
|
||||
<td class="p-3 text-right">{{ row.short_deduction }}</td>
|
||||
<td class="p-3">
|
||||
<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 %}
|
||||
<button type="button"
|
||||
data-tds-filter-rule="{{ row.rule_code }}"
|
||||
@@ -58,6 +66,22 @@
|
||||
Short ({{ row.short_deduction }})
|
||||
</button>
|
||||
{% 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>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -73,7 +97,7 @@
|
||||
</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>
|
||||
</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 %}
|
||||
<script>
|
||||
@@ -96,7 +120,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
|
||||
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");
|
||||
label.textContent = "Showing " + visible + " transaction(s)" + (pieces.length ? " for " + pieces.join(" · ") + "." : ".");
|
||||
clear.classList.toggle("hidden", !status && !rule);
|
||||
|
||||
@@ -4,7 +4,7 @@ import io
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
ERP_LOCAL_AGENT_VERSION = "1.25.4"
|
||||
ERP_LOCAL_AGENT_VERSION = "1.25.5"
|
||||
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.4"
|
||||
__version__ = "1.25.5"
|
||||
AGENT_NAME = "ERP Local Agent"
|
||||
|
||||
@@ -3142,13 +3142,54 @@ class AgentCommandProcessor:
|
||||
# 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 [])
|
||||
tds_ledger_names = set()
|
||||
ledger_parent_by_name: dict[str, str] = {}
|
||||
for led in masters:
|
||||
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()
|
||||
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())
|
||||
|
||||
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 = []
|
||||
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"}:
|
||||
@@ -3162,6 +3203,9 @@ class AgentCommandProcessor:
|
||||
amount = abs(float(e.get("amount") or 0))
|
||||
if not lname or amount <= 0 or (party and lname.casefold() == party.casefold()):
|
||||
continue
|
||||
parent_group = str(ledger_parent_by_name.get(lname.casefold()) or "").strip()
|
||||
if _capital_account_parent(parent_group):
|
||||
continue
|
||||
text = lname.casefold()
|
||||
matches=[]
|
||||
for r in rules:
|
||||
@@ -3170,7 +3214,7 @@ class AgentCommandProcessor:
|
||||
if not matches: continue
|
||||
# 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]))
|
||||
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)
|
||||
for x in candidates: by_party_rule[(x["party"].casefold(),str(x["rule"].get("rule_code")))].append(x)
|
||||
|
||||
Reference in New Issue
Block a user