Validate FY mirrors and repair depreciation and TDS review

This commit is contained in:
A R R R Associates
2026-09-06 20:49:42 +05:30
parent 739973e370
commit eb83e5418d
12 changed files with 166 additions and 22 deletions
+2 -2
View File
@@ -203,13 +203,13 @@ def page(
# the ERP header is now FY2026-27. # the ERP header is now FY2026-27.
if not requested_fy: if not requested_fy:
active_status, active_response = mirror_status_for(active_fy) active_status, active_response = mirror_status_for(active_fy)
active_ready = bool((active_status.get("mirror") or {}).get("ready")) active_ready = bool((active_status.get("mirror") or {}).get("ready_for_financial_year"))
if active_ready: if active_ready:
current_fy = active_fy current_fy = active_fy
else: else:
prior_active_fy = _previous_financial_year(active_fy) prior_active_fy = _previous_financial_year(active_fy)
prior_status, prior_response = mirror_status_for(prior_active_fy) prior_status, prior_response = mirror_status_for(prior_active_fy)
prior_ready = bool((prior_status.get("mirror") or {}).get("ready")) prior_ready = bool((prior_status.get("mirror") or {}).get("ready_for_financial_year"))
if prior_ready: if prior_ready:
current_fy = prior_active_fy current_fy = prior_active_fy
else: else:
@@ -18,7 +18,7 @@
<section class="grid gap-4 md:grid-cols-3"> <section class="grid gap-4 md:grid-cols-3">
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">ERP Local Agent</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if agent_online else 'text-red-700' }}">{{ 'Connected' if agent_online else 'Offline' }}</div></div> <div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">ERP Local Agent</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if agent_online else 'text-red-700' }}">{{ 'Connected' if agent_online else 'Offline' }}</div></div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">SQLite Mirror</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if mirror.ready else 'text-amber-700' }}">{{ 'Ready' if mirror.ready else 'Mirror required' }}</div><div class="mt-1 text-xs text-slate-500">{{ mirror.company.company_name if mirror.ready and mirror.company else '' }}</div></div> <div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">SQLite Mirror</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if mirror.ready_for_financial_year else 'text-amber-700' }}">{{ 'Ready' if mirror.ready_for_financial_year else 'Mirror required' }}</div><div class="mt-1 text-xs text-slate-500">{{ mirror.company.company_name if mirror.ready_for_financial_year and mirror.company else '' }}</div></div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Write-back</div><div class="mt-2 text-lg font-semibold text-slate-700">Controlled</div><div class="mt-1 text-xs text-slate-500">Tally is contacted only after approval/posting.</div></div> <div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Write-back</div><div class="mt-2 text-lg font-semibold text-slate-700">Controlled</div><div class="mt-1 text-xs text-slate-500">Tally is contacted only after approval/posting.</div></div>
</section> </section>
@@ -122,8 +122,6 @@
{% endif %} {% endif %}
</section> </section>
{% endif %} {% endif %}
<div class="rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-800">Phase 6 enables only controlled depreciation Journal write-back. All master and transaction synchronization remain read-only; no other Tally create/alter/delete operation is enabled.</div>
</div> </div>
<script> <script>
@@ -46,22 +46,28 @@
{% if selected_client %} {% if selected_client %}
{% set prev = previous_mirror_status.mirror if previous_mirror_status and previous_mirror_status.mirror else {} %} {% set prev = previous_mirror_status.mirror if previous_mirror_status and previous_mirror_status.mirror else {} %}
{% set curr = current_mirror_status.mirror if current_mirror_status and current_mirror_status.mirror else {} %} {% set curr = current_mirror_status.mirror if current_mirror_status and current_mirror_status.mirror else {} %}
{% set prev_ready = prev.ready_for_financial_year %}
{% set curr_ready = curr.ready_for_financial_year %}
<div class="mt-5 grid gap-3 md:grid-cols-2"> <div class="mt-5 grid gap-3 md:grid-cols-2">
<div class="rounded-xl border p-4 {{ 'border-emerald-200 bg-emerald-50' if prev.ready else 'border-amber-200 bg-amber-50' }}"> <div class="rounded-xl border p-4 {{ 'border-emerald-200 bg-emerald-50' if prev_ready else 'border-amber-200 bg-amber-50' }}">
<div class="text-xs font-semibold uppercase text-slate-500">Previous FY Mirror · FY {{ previous_fy }}</div> <div class="text-xs font-semibold uppercase text-slate-500">Previous FY Mirror · FY {{ previous_fy }}</div>
<div class="mt-1 font-semibold">{{ 'Ready' if prev.ready else 'Mirror required' }}</div> <div class="mt-1 font-semibold">{{ 'Ready' if prev_ready else 'Mirror required' }}</div>
{% if prev.ready %} {% if prev_ready %}
<div class="mt-1 text-xs text-slate-600">{{ prev.company.company_name }} · {{ prev.counts.voucher }} vouchers</div> <div class="mt-1 text-xs text-slate-600">{{ prev.company.company_name }} · {{ prev.counts.voucher }} vouchers</div>
<div class="mt-1 break-all text-[11px] text-slate-500">{{ previous_mirror_status.mirror_db_path or prev.path or '' }}</div> <div class="mt-1 break-all text-[11px] text-slate-500">{{ previous_mirror_status.mirror_db_path or prev.path or '' }}</div>
{% elif prev.ready and prev.financial_year_reason %}
<div class="mt-1 text-xs text-amber-800">{{ prev.financial_year_reason }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="rounded-xl border p-4 {{ 'border-emerald-200 bg-emerald-50' if curr.ready else 'border-amber-200 bg-amber-50' }}"> <div class="rounded-xl border p-4 {{ 'border-emerald-200 bg-emerald-50' if curr_ready else 'border-amber-200 bg-amber-50' }}">
<div class="text-xs font-semibold uppercase text-slate-500">Current FY Mirror · FY {{ current_fy }}</div> <div class="text-xs font-semibold uppercase text-slate-500">Current FY Mirror · FY {{ current_fy }}</div>
<div class="mt-1 font-semibold">{{ 'Ready' if curr.ready else 'Mirror required' }}</div> <div class="mt-1 font-semibold">{{ 'Ready' if curr_ready else 'Mirror required' }}</div>
{% if curr.ready %} {% if curr_ready %}
<div class="mt-1 text-xs text-slate-600">{{ curr.company.company_name }} · {{ curr.counts.voucher }} vouchers</div> <div class="mt-1 text-xs text-slate-600">{{ curr.company.company_name }} · {{ curr.counts.voucher }} vouchers</div>
<div class="mt-1 break-all text-[11px] text-slate-500">{{ current_mirror_status.mirror_db_path or curr.path or '' }}</div> <div class="mt-1 break-all text-[11px] text-slate-500">{{ current_mirror_status.mirror_db_path or curr.path or '' }}</div>
{% elif curr.ready and curr.financial_year_reason %}
<div class="mt-1 text-xs text-amber-800">{{ curr.financial_year_reason }}</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -70,7 +76,7 @@
<input type="hidden" name="client_id" value="{{ selected_client.id }}"> <input type="hidden" name="client_id" value="{{ selected_client.id }}">
<input type="hidden" name="previous_fy" value="{{ previous_fy }}"> <input type="hidden" name="previous_fy" value="{{ previous_fy }}">
<input type="hidden" name="current_fy" value="{{ current_fy }}"> <input type="hidden" name="current_fy" value="{{ current_fy }}">
<button class="rounded-xl bg-slate-900 px-5 py-2 text-sm font-semibold text-white" {% if not node_online or not prev.ready or not curr.ready %}disabled{% endif %}>Compare Opening Balances</button> <button class="rounded-xl bg-slate-900 px-5 py-2 text-sm font-semibold text-white" {% if not node_online or not prev_ready or not curr_ready %}disabled{% endif %}>Compare Opening Balances</button>
</form> </form>
{% endif %} {% endif %}
</section> </section>
@@ -1,5 +1,35 @@
{% extends "ui/templates/base/layout.html" %} {% extends "ui/templates/base/layout.html" %}
{% block content %} {% block content %}
<div class="space-y-6"> <div class="space-y-6">
<div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-violet-600">TDS &amp; Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Compliance Review</h1><p class="mt-1 text-sm text-slate-500">Review expenses, purchases, journals and creditor postings from the client SQLite Accounting Mirror. Rules and thresholds come from the editable TDS Rule Master.</p></div><div class="flex gap-2"><a href="/tools/accounting/tds/rules" class="rounded-xl border px-3 py-2 text-sm font-semibold">Rule Master</a><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Tally</a></div></div>{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}<form method="get" class="rounded-2xl bg-white p-5 shadow-soft"><div class="grid gap-3 lg:grid-cols-3"><label class="text-sm">Client<select name="client_id" required class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Select client</option>{% for c in clients %}<option value="{{ c.id }}" {% if selected_client and c.id==selected_client.id %}selected{% endif %}>{{ c.client_name }}</option>{% endfor %}</select></label><label class="text-sm">From<input type="date" name="date_from" value="{{ date_from }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">To<input type="date" name="date_to" value="{{ date_to }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><input type="hidden" name="run" value="1"><button class="mt-4 rounded-xl bg-violet-700 px-4 py-2 text-sm font-semibold text-white">Run TDS Review</button></form>{% if review %}<div class="flex justify-end gap-2"><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=xlsx" class="rounded-xl border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800">Download Excel</a><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=pdf" class="rounded-xl border px-3 py-2 text-sm font-semibold">Download PDF</a></div><div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">{% for k,v in review.summary.items() %}<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></div>{% endfor %}</div><div class="overflow-x-auto rounded-2xl bg-white shadow-soft"><table 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"><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-violet-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-violet-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> <div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">TDS &amp; Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Compliance Review</h1><p class="mt-1 text-sm text-slate-500">Review expenses, purchases, journals and creditor postings from the client SQLite Accounting Mirror. Rules and thresholds come from the editable TDS Rule Master.</p></div><div class="flex gap-2"><a href="/tools/accounting/tds/rules" class="rounded-xl border px-3 py-2 text-sm font-semibold">Rule Master</a><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Tally</a></div></div>{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}<form method="get" class="rounded-2xl bg-white p-5 shadow-soft"><div class="grid gap-3 lg:grid-cols-3"><label class="text-sm">Client<select name="client_id" required class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Select client</option>{% for c in clients %}<option value="{{ c.id }}" {% if selected_client and c.id==selected_client.id %}selected{% endif %}>{{ c.client_name }}</option>{% endfor %}</select></label><label class="text-sm">From<input type="date" name="date_from" value="{{ date_from }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">To<input type="date" name="date_to" value="{{ date_to }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><input type="hidden" name="run" value="1"><button class="mt-4 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Run TDS Review</button></form>{% if review %}<div class="flex justify-end gap-2"><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=xlsx" class="rounded-xl border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800">Download Excel</a><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=pdf" class="rounded-xl border px-3 py-2 text-sm font-semibold">Download PDF</a></div><div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">{% for k,v in review.summary.items() %}<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></div>{% endfor %}</div><div class="overflow-x-auto rounded-2xl bg-white shadow-soft">
<div class="border-b border-slate-100 px-4 py-3"><h2 class="font-semibold text-slate-900">TDS Category Summary</h2><p class="mt-1 text-xs text-slate-500">Rule/category-wise totals for quick review before transaction-level exceptions.</p></div>
<table class="min-w-full text-sm">
<thead class="bg-slate-50">
<tr>
<th class="p-3 text-left">Category / Section</th>
<th class="p-3 text-right">Transactions</th>
<th class="p-3 text-right">Taxable Base</th>
<th class="p-3 text-right">Expected TDS</th>
<th class="p-3 text-right">Actual TDS</th>
<th class="p-3 text-right">Liability</th>
<th class="p-3 text-right">Not Deducted</th>
<th class="p-3 text-right">Short</th>
</tr>
</thead>
<tbody>
{% for row in review.category_summary %}
<tr class="border-t">
<td class="p-3"><div class="font-medium">{{ row.category }}</div><div class="text-xs text-slate-500">{{ row.legacy_section }}{% if row.statutory_reference %} · {{ row.statutory_reference }}{% endif %}</div></td>
<td class="p-3 text-right">{{ row.transactions }}</td>
<td class="p-3 text-right">{{ '%.2f'|format(row.taxable_base) }}</td>
<td class="p-3 text-right">{{ '%.2f'|format(row.expected_tds) }}</td>
<td class="p-3 text-right">{{ '%.2f'|format(row.actual_tds) }}</td>
<td class="p-3 text-right font-semibold">{{ '%.2f'|format(row.liability) }}</td>
<td class="p-3 text-right">{{ row.tds_not_deducted }}</td>
<td class="p-3 text-right">{{ row.short_deduction }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div><div class="overflow-x-auto rounded-2xl bg-white shadow-soft"><table 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"><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>
{% endblock %} {% endblock %}
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
{% extends "ui/templates/base/layout.html" %} {% extends "ui/templates/base/layout.html" %}
{% block content %} {% block content %}
<div class="space-y-6"> <div class="space-y-6">
<div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-violet-600">TDS &amp; Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Reconciliation</h1><p class="mt-1 text-sm text-slate-500">Compare live Tally TDS with return/form, challan and TRACES totals. Paste exports as one row per line: PAN, section/code, amount.</p></div><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Tally</a></div>{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}<form method="post" class="rounded-2xl bg-white p-5 shadow-soft"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><div class="grid gap-3 lg:grid-cols-4"><label class="text-sm">Client<select name="client_id" required class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Select client</option>{% for c in clients %}<option value="{{ c.id }}" {% if selected_client and c.id==selected_client.id %}selected{% endif %}>{{ c.client_name }}</option>{% endfor %}</select></label><label class="text-sm">Tally GUID<input name="tally_guid" value="{{ tally_guid or '' }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">From<input type="date" name="date_from" value="{{ date_from or '' }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">To<input type="date" name="date_to" value="{{ date_to or '' }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><div class="mt-4 grid gap-3 lg:grid-cols-3"><label class="text-sm">TDS return / form export<textarea name="return_data" rows="8" class="mt-1 w-full rounded-xl border p-2 font-mono text-xs">{{ return_data or '' }}</textarea></label><label class="text-sm">Challan export<textarea name="challan_data" rows="8" class="mt-1 w-full rounded-xl border p-2 font-mono text-xs">{{ challan_data or '' }}</textarea></label><label class="text-sm">TRACES export<textarea name="traces_data" rows="8" class="mt-1 w-full rounded-xl border p-2 font-mono text-xs">{{ traces_data or '' }}</textarea></label></div><button class="mt-4 rounded-xl bg-violet-700 px-4 py-2 text-sm font-semibold text-white">Run Reconciliation</button></form>{% if result %}<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">{% for label,value in [('Tally booked TDS',result.tally_actual),('Expected TDS',result.tally_expected),('Return / Forms',result.return_total),('Challans',result.challan_total),('TRACES',result.traces_total)] %}<div class="rounded-xl bg-white p-4 shadow-soft"><div class="text-xs uppercase text-slate-500">{{ label }}</div><div class="mt-1 text-xl font-semibold">₹{{ '%.2f'|format(value) }}</div></div>{% endfor %}</div><div class="grid gap-3 md:grid-cols-3"><div class="rounded-xl bg-white p-4 shadow-soft">Tally − Return: <b>₹{{ '%.2f'|format(result.tally_vs_return) }}</b></div><div class="rounded-xl bg-white p-4 shadow-soft">Return − Challan: <b>₹{{ '%.2f'|format(result.return_vs_challan) }}</b></div><div class="rounded-xl bg-white p-4 shadow-soft">Return − TRACES: <b>₹{{ '%.2f'|format(result.return_vs_traces) }}</b></div></div>{% endif %}</div> <div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">TDS &amp; Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Reconciliation</h1><p class="mt-1 text-sm text-slate-500">Compare live Tally TDS with return/form, challan and TRACES totals. Paste exports as one row per line: PAN, section/code, amount.</p></div><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Tally</a></div>{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}<form method="post" class="rounded-2xl bg-white p-5 shadow-soft"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><div class="grid gap-3 lg:grid-cols-4"><label class="text-sm">Client<select name="client_id" required class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Select client</option>{% for c in clients %}<option value="{{ c.id }}" {% if selected_client and c.id==selected_client.id %}selected{% endif %}>{{ c.client_name }}</option>{% endfor %}</select></label><label class="text-sm">Tally GUID<input name="tally_guid" value="{{ tally_guid or '' }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">From<input type="date" name="date_from" value="{{ date_from or '' }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">To<input type="date" name="date_to" value="{{ date_to or '' }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><div class="mt-4 grid gap-3 lg:grid-cols-3"><label class="text-sm">TDS return / form export<textarea name="return_data" rows="8" class="mt-1 w-full rounded-xl border p-2 font-mono text-xs">{{ return_data or '' }}</textarea></label><label class="text-sm">Challan export<textarea name="challan_data" rows="8" class="mt-1 w-full rounded-xl border p-2 font-mono text-xs">{{ challan_data or '' }}</textarea></label><label class="text-sm">TRACES export<textarea name="traces_data" rows="8" class="mt-1 w-full rounded-xl border p-2 font-mono text-xs">{{ traces_data or '' }}</textarea></label></div><button class="mt-4 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Run Reconciliation</button></form>{% if result %}<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">{% for label,value in [('Tally booked TDS',result.tally_actual),('Expected TDS',result.tally_expected),('Return / Forms',result.return_total),('Challans',result.challan_total),('TRACES',result.traces_total)] %}<div class="rounded-xl bg-white p-4 shadow-soft"><div class="text-xs uppercase text-slate-500">{{ label }}</div><div class="mt-1 text-xl font-semibold">₹{{ '%.2f'|format(value) }}</div></div>{% endfor %}</div><div class="grid gap-3 md:grid-cols-3"><div class="rounded-xl bg-white p-4 shadow-soft">Tally − Return: <b>₹{{ '%.2f'|format(result.tally_vs_return) }}</b></div><div class="rounded-xl bg-white p-4 shadow-soft">Return − Challan: <b>₹{{ '%.2f'|format(result.return_vs_challan) }}</b></div><div class="rounded-xl bg-white p-4 shadow-soft">Return − TRACES: <b>₹{{ '%.2f'|format(result.return_vs_traces) }}</b></div></div>{% endif %}</div>
{% endblock %} {% endblock %}
@@ -1,7 +1,7 @@
{% extends "ui/templates/base/layout.html" %} {% extends "ui/templates/base/layout.html" %}
{% block content %} {% block content %}
<div class="space-y-6"> <div class="space-y-6">
<div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-violet-600">TDS &amp; Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Rule Master</h1><p class="mt-1 text-sm text-slate-500">Default rules are seeded for each firm and remain editable. Historical effective-date versions can be retained instead of hard-coding thresholds in Python.</p></div><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Back to Tally</a></div> <div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">TDS &amp; Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Rule Master</h1><p class="mt-1 text-sm text-slate-500">Default rules are seeded for each firm and remain editable. Historical effective-date versions can be retained instead of hard-coding thresholds in Python.</p></div><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Back to Tally</a></div>
{% if saved %}<div class="rounded-xl bg-emerald-50 p-3 text-sm text-emerald-800">Rule saved.</div>{% endif %}{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %} {% if saved %}<div class="rounded-xl bg-emerald-50 p-3 text-sm text-emerald-800">Rule saved.</div>{% endif %}{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}
<div class="space-y-4">{% for r in rules %}<form method="post" action="/tools/accounting/tds/rules/{{ r.id }}" class="rounded-2xl bg-white p-5 shadow-soft"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><div class="grid gap-3 lg:grid-cols-4"><label class="text-sm">Rule name<input name="name" value="{{ r.name }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">New Act reference<input name="statutory_reference" value="{{ r.statutory_reference }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Legacy section<input name="legacy_section" value="{{ r.legacy_section or '' }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Threshold mode<select name="threshold_mode" class="mt-1 w-full rounded-xl border px-3 py-2">{% for m in ['ANNUAL','SINGLE','BOTH','MONTHLY'] %}<option {% if r.threshold_mode==m %}selected{% endif %}>{{ m }}</option>{% endfor %}</select></label><label class="text-sm">Single / monthly threshold<input name="single_threshold" value="{{ r.single_threshold }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Annual aggregate threshold<input name="aggregate_threshold" value="{{ r.aggregate_threshold }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Rate - Individual/HUF %<input name="rate_individual_huf" value="{{ r.rate_individual_huf }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Rate - Other %<input name="rate_other" value="{{ r.rate_other }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">No-PAN rate %<input name="no_pan_rate" value="{{ r.no_pan_rate }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm lg:col-span-3">Ledger/nature keywords<input name="nature_keywords" value="{{ r.nature_keywords }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><div class="mt-3 flex flex-wrap gap-4 text-sm"><label><input type="checkbox" name="deduct_on_excess_only" {% if r.deduct_on_excess_only %}checked{% endif %}> Deduct only on amount exceeding threshold</label><label><input type="checkbox" name="applies_to_purchases" {% if r.applies_to_purchases %}checked{% endif %}> Purchases</label><label><input type="checkbox" name="applies_to_expenses" {% if r.applies_to_expenses %}checked{% endif %}> Expenses/services</label><label><input type="checkbox" name="is_active" {% if r.is_active %}checked{% endif %}> Active</label></div><button class="mt-4 rounded-xl bg-violet-700 px-4 py-2 text-sm font-semibold text-white">Save Rule</button></form>{% endfor %}</div></div> <div class="space-y-4">{% for r in rules %}<form method="post" action="/tools/accounting/tds/rules/{{ r.id }}" class="rounded-2xl bg-white p-5 shadow-soft"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><div class="grid gap-3 lg:grid-cols-4"><label class="text-sm">Rule name<input name="name" value="{{ r.name }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">New Act reference<input name="statutory_reference" value="{{ r.statutory_reference }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Legacy section<input name="legacy_section" value="{{ r.legacy_section or '' }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Threshold mode<select name="threshold_mode" class="mt-1 w-full rounded-xl border px-3 py-2">{% for m in ['ANNUAL','SINGLE','BOTH','MONTHLY'] %}<option {% if r.threshold_mode==m %}selected{% endif %}>{{ m }}</option>{% endfor %}</select></label><label class="text-sm">Single / monthly threshold<input name="single_threshold" value="{{ r.single_threshold }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Annual aggregate threshold<input name="aggregate_threshold" value="{{ r.aggregate_threshold }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Rate - Individual/HUF %<input name="rate_individual_huf" value="{{ r.rate_individual_huf }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">Rate - Other %<input name="rate_other" value="{{ r.rate_other }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">No-PAN rate %<input name="no_pan_rate" value="{{ r.no_pan_rate }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm lg:col-span-3">Ledger/nature keywords<input name="nature_keywords" value="{{ r.nature_keywords }}" class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><div class="mt-3 flex flex-wrap gap-4 text-sm"><label><input type="checkbox" name="deduct_on_excess_only" {% if r.deduct_on_excess_only %}checked{% endif %}> Deduct only on amount exceeding threshold</label><label><input type="checkbox" name="applies_to_purchases" {% if r.applies_to_purchases %}checked{% endif %}> Purchases</label><label><input type="checkbox" name="applies_to_expenses" {% if r.applies_to_expenses %}checked{% endif %}> Expenses/services</label><label><input type="checkbox" name="is_active" {% if r.is_active %}checked{% endif %}> Active</label></div><button class="mt-4 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Save Rule</button></form>{% endfor %}</div></div>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -832,7 +832,7 @@ def depreciation_it_tool(
else: else:
command_error = str(status_response.get("error") or "Accounting Mirror status failed.") command_error = str(status_response.get("error") or "Accounting Mirror status failed.")
if mirror_status and (mirror_status.get("mirror") or {}).get("ready"): if mirror_status and (mirror_status.get("mirror") or {}).get("ready_for_financial_year"):
preview_response = request_agent_command( preview_response = request_agent_command(
node.node_code, node.node_code,
"accounting_depreciation_preview", "accounting_depreciation_preview",
+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.2" ERP_LOCAL_AGENT_VERSION = "1.25.3"
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.2" __version__ = "1.25.3"
AGENT_NAME = "ERP Local Agent" AGENT_NAME = "ERP Local Agent"
@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import date, datetime, timedelta, timezone from datetime import date, datetime, timedelta, timezone
import json import json
import os import os
import re
from pathlib import Path from pathlib import Path
import shutil import shutil
import sqlite3 import sqlite3
@@ -3194,7 +3194,61 @@ class AgentCommandProcessor:
liability=round(max(0.0,expected-actual),2) liability=round(max(0.0,expected-actual),2)
status="OK" if expected>0 and liability<=0.009 else ("TDS NOT DEDUCTED" if expected>0 and actual<=0.009 else ("SHORT DEDUCTION" if liability>0 else "BELOW / OUTSIDE THRESHOLD")) status="OK" if expected>0 and liability<=0.009 else ("TDS NOT DEDUCTED" if expected>0 and actual<=0.009 else ("SHORT DEDUCTION" if liability>0 else "BELOW / OUTSIDE THRESHOLD"))
transactions.append({**{k:v for k,v in x.items() if k!='rule'},"rule_id":r.get("id"),"rule_code":r.get("rule_code"),"rule_name":r.get("name"),"legacy_section":r.get("legacy_section"),"statutory_reference":r.get("statutory_reference"),"cumulative_amount":cumulative,"expected_tds":expected,"actual_tds":actual,"liability":liability,"status":status}) transactions.append({**{k:v for k,v in x.items() if k!='rule'},"rule_id":r.get("id"),"rule_code":r.get("rule_code"),"rule_name":r.get("name"),"legacy_section":r.get("legacy_section"),"statutory_reference":r.get("statutory_reference"),"cumulative_amount":cumulative,"expected_tds":expected,"actual_tds":actual,"liability":liability,"status":status})
review = {"company_name":company_name,"company_guid":company_guid,"date_from":date_from,"date_to":date_to,"summary":{"vouchers_reviewed":len(vouchers),"candidate_transactions":len(transactions),"tds_not_deducted":sum(1 for x in transactions if x["status"]=="TDS NOT DEDUCTED"),"short_deduction":sum(1 for x in transactions if x["status"]=="SHORT DEDUCTION"),"purchase_transactions":sum(1 for x in transactions if x["rule_code"]=="PURCHASE_GOODS")},"transactions":transactions,"tds_ledgers":sorted(tds_ledger_names),"read_only":True,"sqlite_mirror":True} category_map = {}
for item in transactions:
key = str(item.get("rule_code") or item.get("rule_name") or "UNCLASSIFIED")
row = category_map.setdefault(key, {
"rule_code": str(item.get("rule_code") or ""),
"category": str(item.get("rule_name") or "Unclassified"),
"legacy_section": str(item.get("legacy_section") or ""),
"statutory_reference": str(item.get("statutory_reference") or ""),
"transactions": 0,
"taxable_base": 0.0,
"expected_tds": 0.0,
"actual_tds": 0.0,
"liability": 0.0,
"tds_not_deducted": 0,
"short_deduction": 0,
"ok": 0,
"below_threshold": 0,
})
row["transactions"] += 1
row["taxable_base"] = round(row["taxable_base"] + float(item.get("taxable_base") or 0), 2)
row["expected_tds"] = round(row["expected_tds"] + float(item.get("expected_tds") or 0), 2)
row["actual_tds"] = round(row["actual_tds"] + float(item.get("actual_tds") or 0), 2)
row["liability"] = round(row["liability"] + float(item.get("liability") or 0), 2)
status = str(item.get("status") or "")
if status == "TDS NOT DEDUCTED":
row["tds_not_deducted"] += 1
elif status == "SHORT DEDUCTION":
row["short_deduction"] += 1
elif status == "OK":
row["ok"] += 1
else:
row["below_threshold"] += 1
category_summary = sorted(
category_map.values(),
key=lambda row: (-float(row.get("liability") or 0), str(row.get("category") or "").casefold()),
)
review = {
"company_name": company_name,
"company_guid": company_guid,
"date_from": date_from,
"date_to": date_to,
"summary": {
"vouchers_reviewed": len(vouchers),
"candidate_transactions": len(transactions),
"tds_not_deducted": sum(1 for x in transactions if x["status"]=="TDS NOT DEDUCTED"),
"short_deduction": sum(1 for x in transactions if x["status"]=="SHORT DEDUCTION"),
"purchase_transactions": sum(1 for x in transactions if x["rule_code"]=="PURCHASE_GOODS"),
},
"category_summary": category_summary,
"transactions": transactions,
"tds_ledgers": sorted(tds_ledger_names),
"read_only": True,
"sqlite_mirror": True,
}
analysis_run_id = self._save_analysis_run( analysis_run_id = self._save_analysis_run(
client_id, client_id,
analysis_type="TDS_COMPLIANCE", analysis_type="TDS_COMPLIANCE",
@@ -3531,11 +3585,66 @@ class AgentCommandProcessor:
@staticmethod
def _mirror_financial_year_match(mirror: dict[str, Any], financial_year: str) -> dict[str, Any]:
"""Validate mirror contents against the FY requested by ERP.
A mirror file can exist in an FY folder even when its vouchers were
extracted while Tally was on another active period. Such a file must
not be shown as Ready for Opening Balance / depreciation analysis.
"""
fy = str(financial_year or "").strip()
match = re.fullmatch(r"(\d{4})-(\d{2})", fy)
if not match:
return {"requested_financial_year": fy, "ready_for_financial_year": bool(mirror.get("ready"))}
start_year = int(match.group(1))
fy_from = _dt_date(start_year, 4, 1)
fy_to = _dt_date(start_year + 1, 3, 31)
period = mirror.get("voucher_period") or {}
from_text = str(period.get("from_date") or "").strip()
to_text = str(period.get("to_date") or "").strip()
try:
mirror_from = _dt_date.fromisoformat(from_text)
mirror_to = _dt_date.fromisoformat(to_text)
except Exception:
return {
"requested_financial_year": fy,
"ready_for_financial_year": False,
"financial_year_reason": "Mirror voucher period is unavailable.",
"financial_year_from": fy_from.isoformat(),
"financial_year_to": fy_to.isoformat(),
}
# Require the mirror's actual voucher period to fall inside the requested
# Indian FY. This deliberately rejects a copied/misfiled FY2025-26
# extraction sitting in the FY2026-27 directory.
ready = bool(mirror.get("ready")) and mirror_from >= fy_from and mirror_to <= fy_to
return {
"requested_financial_year": fy,
"ready_for_financial_year": ready,
"financial_year_reason": "" if ready else (
f"Mirror contains {mirror_from.isoformat()} to {mirror_to.isoformat()}, "
f"not FY {fy}."
),
"financial_year_from": fy_from.isoformat(),
"financial_year_to": fy_to.isoformat(),
}
def _accounting_mirror_status(self, payload: dict[str, Any]) -> dict[str, Any]: def _accounting_mirror_status(self, payload: dict[str, Any]) -> dict[str, Any]:
client_id = int(payload.get("client_id") or 0) client_id = int(payload.get("client_id") or 0)
if client_id <= 0: if client_id <= 0:
raise ValueError("client_id is required.") raise ValueError("client_id is required.")
return self.tally.mirror.status(client_id) result = self.tally.mirror.status(client_id)
mirror = result.get("mirror") or {}
fy_check = self._mirror_financial_year_match(mirror, str(payload.get("financial_year") or ""))
mirror.update(fy_check)
# Keep file-level readiness for diagnostics, but expose the effective
# readiness used by ERP analysis pages separately.
result["mirror"] = mirror
result["ready_for_financial_year"] = bool(fy_check.get("ready_for_financial_year"))
return result
def _accounting_mirror_sync(self, payload: dict[str, Any]) -> dict[str, Any]: def _accounting_mirror_sync(self, payload: dict[str, Any]) -> dict[str, Any]:
client_id = int(payload.get("client_id") or 0) client_id = int(payload.get("client_id") or 0)