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="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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user