Add GST return download local storage and reconciliation workflow

This commit is contained in:
A R R R Associates
2026-09-10 21:46:10 +05:30
parent faed89b655
commit 477ea79f76
9 changed files with 638 additions and 7 deletions
@@ -0,0 +1,42 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="mx-auto max-w-7xl space-y-5 p-4">
<div class="flex items-center justify-between gap-3"><div><h1 class="text-2xl font-bold">GST Return Reconciliation</h1><p class="text-sm text-slate-600">Download GST portal data through Credential Vault, store it in client local storage, and reconcile against Accounting Mirror.</p></div><a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-lg border px-3 py-2 text-sm">Back to Accounting</a></div>
{% if message %}<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-emerald-800">{{ message }}</div>{% endif %}
{% if error %}<div class="rounded-lg border border-red-200 bg-red-50 p-3 text-red-800">{{ error }}</div>{% endif %}
<form method="get" class="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-4">
<label class="text-sm">Client<select name="client_id" class="mt-1 w-full rounded border p-2" onchange="this.form.submit()"><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">GSTIN<select name="registration_id" class="mt-1 w-full rounded border p-2" onchange="this.form.submit()"><option value="">Select GSTIN</option>{% for r,t in registrations %}<option value="{{ r.id }}" {% if selected_registration and r.id==selected_registration.id %}selected{% endif %}>{{ r.registration_number }}{% if r.trade_name %} — {{ r.trade_name }}{% endif %}</option>{% endfor %}</select></label>
<label class="text-sm">Return Period (MMYYYY)<input name="period" value="{{ period }}" pattern="[0-9]{6}" placeholder="042026" class="mt-1 w-full rounded border p-2"></label>
<div class="flex items-end"><button class="w-full rounded bg-slate-800 px-3 py-2 text-white">Load</button></div>
</form>
{% if selected_client and selected_registration %}
<div class="grid gap-4 lg:grid-cols-2">
<form method="post" action="/tools/accounting/gst-reconciliation/download/start" class="rounded-xl border bg-white p-4 space-y-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="registration_id" value="{{ selected_registration.id }}"><input type="hidden" name="period" value="{{ period }}">
<h2 class="font-semibold">1. Download from GST Portal</h2>
<p class="text-xs text-slate-500">The Local Storage Agent opens GST portal on the workstation. Username/password are taken from Credential Vault; captcha/OTP remains interactive. Raw return data is saved under the client's FY/GST/GSTIN/period directory.</p>
<label class="text-sm">Credential Vault Entry<select name="credential_id" required class="mt-1 w-full rounded border p-2"><option value="">Select GST credential</option>{% for c in credentials %}<option value="{{ c.id }}">{{ c.title }}{% if c.reference_number %} — {{ c.reference_number }}{% endif %}</option>{% endfor %}</select></label>
<label class="flex gap-2 text-sm"><input type="checkbox" name="include_2a" value="1"> Also download GSTR-2A</label>
<button {% if not period or not node_online %}disabled{% endif %} class="rounded bg-indigo-600 px-4 py-2 text-white disabled:opacity-50">Start GST Download</button>
<div class="text-xs">Storage Agent: <b>{{ 'Online' if node_online else 'Offline' }}</b></div>
</form>
<form method="post" action="/tools/accounting/gst-reconciliation/analyze" class="rounded-xl border bg-white p-4 space-y-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="registration_id" value="{{ selected_registration.id }}"><input type="hidden" name="period" value="{{ period }}">
<h2 class="font-semibold">2. Reconcile Stored Data</h2>
<p class="text-sm text-slate-600">Sales: Accounting Mirror vs GSTR-1. Purchases: Accounting Mirror vs GSTR-2B. ITC: GSTR-2B vs GSTR-3B. Reconciliation JSON is retained in the same local client GST directory.</p>
<button {% if not period or not node_online %}disabled{% endif %} class="rounded bg-emerald-600 px-4 py-2 text-white disabled:opacity-50">Run Reconciliation</button>
</form>
</div>
{% if status %}<div class="rounded-xl border bg-white p-4"><h2 class="font-semibold">Download Status</h2><div class="mt-2 grid gap-2 text-sm md:grid-cols-3"><div>Status: <b>{{ status.status or '-' }}</b></div><div>Stage: {{ status.stage or '-' }}</div><div>{{ status.message or '' }}</div></div></div>{% endif %}
{% set a=request.session.get('gst_reconciliation_result') or {} %}
{% if a %}
{% set s=a.get('sales_reconciliation',{}).get('counts',{}) %}{% set p=a.get('purchase_reconciliation',{}).get('counts',{}) %}
<div class="rounded-xl border bg-white p-4 space-y-4"><h2 class="font-semibold">Reconciliation Summary — {{ a.get('period','') }}</h2>
<div class="grid gap-3 md:grid-cols-2"><div class="rounded-lg border p-3"><h3 class="font-medium">Sales vs GSTR-1</h3><p class="text-sm">Books {{ s.get('books',0) }} · Portal {{ s.get('portal',0) }} · Matched {{ s.get('matched',0) }} · Missing in portal {{ s.get('missing_in_portal',0) }} · Missing in books {{ s.get('missing_in_books',0) }} · Value mismatch {{ s.get('value_mismatch',0) }}</p></div><div class="rounded-lg border p-3"><h3 class="font-medium">Purchases vs GSTR-2B</h3><p class="text-sm">Books {{ p.get('books',0) }} · 2B {{ p.get('portal',0) }} · Matched {{ p.get('matched',0) }} · Missing in 2B {{ p.get('missing_in_portal',0) }} · Missing in books {{ p.get('missing_in_books',0) }} · Value mismatch {{ p.get('value_mismatch',0) }}</p></div></div>
<div><h3 class="font-medium mb-2">ITC: GSTR-2B vs GSTR-3B</h3><div class="overflow-x-auto"><table class="min-w-full text-sm"><thead><tr><th class="p-2 text-left">Tax</th><th class="p-2 text-right">GSTR-2B</th><th class="p-2 text-right">GSTR-3B</th><th class="p-2 text-right">Difference</th></tr></thead><tbody>{% for tax,row in a.get('itc_reconciliation',{}).items() %}<tr class="border-t"><td class="p-2 uppercase">{{ tax }}</td><td class="p-2 text-right">{{ '%.2f'|format(row.get('gstr2b',0)) }}</td><td class="p-2 text-right">{{ '%.2f'|format(row.get('gstr3b',0)) }}</td><td class="p-2 text-right">{{ '%.2f'|format(row.get('difference',0)) }}</td></tr>{% endfor %}</tbody></table></div></div>
</div>
{% endif %}
{% endif %}
</div>
{% endblock %}
@@ -33,6 +33,7 @@
<a href="/tools/accounting/historical-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Historical Learning</a>
<a href="/tools/accounting/ledger-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Ledger Learning</a>
<a href="/tools/accounting/gstr2b{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">GSTR-2B Intelligence</a>
<a href="/tools/accounting/gst-reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800 hover:bg-emerald-100">GST Return Reconciliation</a>
<a href="/tools/accounting/purchase-enrichment{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">E-Invoice / E-Way Bill</a>
<a href="/tools/accounting/purchase-review{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Purchase Review</a>
<a href="/tools/accounting/purchase-posting{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Purchase → Tally</a>