Add Phase 11 bank to Tally with multi-bank contra intelligence

This commit is contained in:
A R R R Associates
2026-08-22 19:59:40 +05:30
parent 215ad2c6c6
commit 8748ee9539
16 changed files with 1140 additions and 28 deletions
@@ -0,0 +1,76 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<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-brand-600">Accounting Intelligence · Phase 11</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">Bank → Tally Review</h1><p class="mt-1 max-w-4xl text-sm text-slate-500">Import a completed Bank Analyzer job, reuse the common ledger-learning engine for non-contra transactions, and post only reviewed Payment / Receipt / Contra vouchers through the durable Local Agent job system.</p></div>
<a href="/tools/bank-statement-analyzer" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold">Bank Analyzer</a>
</div>
{% if message %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-800">{{ message }}</div>{% endif %}
{% if error %}<div class="rounded-xl border border-red-200 bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<form method="post" action="/tools/accounting/bank-posting/import" class="grid gap-4 md:grid-cols-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label class="text-sm font-medium">Client<select name="client_id" required class="mt-1 w-full rounded-xl border border-slate-300 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 font-medium">Completed Bank Analyzer job<select name="job_id" required class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"><option value="">Select analysis</option>{% for j in jobs %}<option value="{{ j.id }}">{{ j.completed_at_utc or j.submitted_at_utc }} · {{ j.selected_bank|replace('_',' ')|title }} · {{ j.file_count }} file(s)</option>{% endfor %}</select></label>
<div class="flex items-end"><button class="w-full rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Import to Accounting Engine</button></div>
</form>
</section>
{% if selected_client %}
<section class="rounded-2xl bg-white shadow-soft">
<div class="border-b border-slate-200 px-5 py-4"><h2 class="font-semibold text-slate-900">{{ selected_client.client_name }}</h2><p class="text-sm text-slate-500">{{ total }} bank transaction(s). Matched multi-bank contra rows are visibly linked by pair ID.</p></div>
<div class="divide-y divide-slate-100">
{% for tx in rows %}
{% set choice = choices.get(tx.id) %}
<article class="p-5">
<div class="grid gap-4 xl:grid-cols-12">
<div class="xl:col-span-3"><div class="font-semibold">{{ tx.transaction_date }} · {{ tx.bank_name }}</div><div class="mt-1 text-xs text-slate-500">{{ tx.account_number }}</div><div class="mt-2 text-sm">{{ tx.narration }}</div><div class="mt-2 font-semibold">₹{{ '%.2f'|format(tx.amount) }} · {{ tx.direction }}</div>{% if tx.contra_pair_id %}<div class="mt-2 rounded-lg bg-indigo-50 p-2 text-xs text-indigo-800"><strong>{{ tx.contra_pair_id }}</strong> · {{ tx.contra_counter_bank }} / {{ tx.contra_counter_account }} · {{ '%.0f'|format(tx.contra_confidence) }}%</div>{% endif %}</div>
<div class="xl:col-span-3"><div class="text-xs uppercase text-slate-500">Suggestion</div><div class="mt-1 font-semibold">{{ tx.suggested_voucher_type }}</div><div class="text-sm">{{ tx.auto_party or '—' }}</div><div class="text-sm">{{ tx.suggested_ledger_name or tx.analyzer_ledger or 'Review ledger' }}</div><div class="text-xs text-slate-500">Confidence {{ tx.suggested_confidence }}%</div></div>
<div class="xl:col-span-6">
{% if tx.review_status != 'reviewed' %}
<form method="post" action="/tools/accounting/bank-posting/{{ tx.id }}/review" class="grid gap-2 md:grid-cols-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}">
<label class="text-xs font-semibold">Voucher Type<select name="voucher_type" class="mt-1 w-full rounded-lg border border-slate-300 px-2 py-2">{% for v in ['Payment','Receipt','Contra'] %}<option {% if tx.suggested_voucher_type==v %}selected{% endif %}>{{ v }}</option>{% endfor %}</select></label>
<label class="text-xs font-semibold">Nature<select name="nature_id" class="mt-1 w-full rounded-lg border border-slate-300 px-2 py-2"><option value="">Not required for Contra</option>{% for n in natures %}<option value="{{ n.id }}" {% if tx.suggested_nature_id==n.id %}selected{% endif %}>{{ n.name }}</option>{% endfor %}</select></label>
<label class="text-xs font-semibold md:col-span-2">Mapped Tally Counter Ledger<input name="ledger_name" value="{{ tx.suggested_ledger_name }}" class="mt-1 w-full rounded-lg border border-slate-300 px-2 py-2" placeholder="Required for Payment / Receipt"></label>
<label class="text-xs font-semibold md:col-span-2">Review note<input name="review_note" class="mt-1 w-full rounded-lg border border-slate-300 px-2 py-2"></label>
<button class="rounded-lg bg-slate-900 px-3 py-2 text-sm font-semibold text-white md:col-span-2">Confirm Accounting Treatment</button>
</form>
{% elif tx.posting_status in ['not_started','preflight_failed','posting_failed'] %}
<form method="post" action="/tools/accounting/bank-posting/{{ tx.id }}/preflight" class="rounded-xl border border-slate-200 p-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}">
<div class="text-sm font-semibold">Reviewed: {{ tx.final_voucher_type }}{% if tx.final_ledger_name %} · {{ tx.final_ledger_name }}{% endif %}</div>
<select name="workstation_id" required class="mt-2 w-full rounded-lg border border-slate-300 px-2 py-2"><option value="">Select workstation with correct Tally company</option>{% for ws in workstations %}<option value="{{ ws.id }}">{{ ws.machine_name }} · {{ 'Tally connected' if ws.tally_connected else 'offline' }}</option>{% endfor %}</select>
<button class="mt-2 rounded-lg bg-brand-600 px-3 py-2 text-sm font-semibold text-white">Run Tally Preflight</button>
{% if tx.last_error %}<div class="mt-2 text-xs text-red-700">{{ tx.last_error }}</div>{% endif %}
</form>
{% elif tx.posting_status in ['preflight_queued','preflight_claimed','posting_queued','posting_claimed'] %}
<div class="rounded-xl bg-blue-50 p-3 text-sm text-blue-800">{{ tx.posting_status|replace('_',' ')|title }}. Refresh the page after the workstation processes the durable job.</div>
{% elif tx.posting_status == 'preflight_ready' and choice %}
<form method="post" action="/tools/accounting/bank-posting/{{ tx.id }}/post" class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}">
{% if choice.duplicate_candidates %}<div class="mb-2 text-sm font-semibold text-red-700">Possible duplicate exists in Tally. Posting is blocked until resolved.</div>{% endif %}
<label class="text-xs font-semibold">Bank Ledger<select name="bank_ledger_name" required class="mt-1 w-full rounded-lg border border-emerald-300 bg-white px-2 py-2"><option value="">Select</option>{% for x in choice.bank_ledgers or [] %}<option value="{{ x.name }}">{{ x.name }}</option>{% endfor %}</select></label>
{% if tx.final_voucher_type == 'Contra' %}
<label class="mt-2 block text-xs font-semibold">Other Bank Ledger<select name="other_bank_ledger_name" required class="mt-1 w-full rounded-lg border border-emerald-300 bg-white px-2 py-2"><option value="">Select</option>{% for x in choice.bank_ledgers or [] %}<option value="{{ x.name }}">{{ x.name }}</option>{% endfor %}</select></label><input type="hidden" name="counter_ledger_name" value="">
{% else %}
<label class="mt-2 block text-xs font-semibold">Counter Ledger<select name="counter_ledger_name" required class="mt-1 w-full rounded-lg border border-emerald-300 bg-white px-2 py-2"><option value="">Select</option>{% for x in choice.all_ledgers or [] %}<option value="{{ x.name }}" {% if x.name==tx.final_ledger_name %}selected{% endif %}>{{ x.name }}</option>{% endfor %}</select></label><input type="hidden" name="other_bank_ledger_name" value="">
{% endif %}
<button class="mt-3 rounded-lg bg-emerald-700 px-3 py-2 text-sm font-semibold text-white" {% if choice.duplicate_candidates %}disabled{% endif %}>Post to Tally</button>
</form>
{% elif tx.posting_status == 'posted' %}
<div class="rounded-xl bg-emerald-50 p-3 text-sm text-emerald-800"><strong>Posted.</strong> Voucher {{ tx.tally_voucher_number or tx.tally_voucher_id or 'created' }}.</div>
{% elif tx.posting_status == 'posting_indeterminate' %}
<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800"><strong>Manual verification required — do not retry.</strong> {{ tx.last_error }}</div>
{% endif %}
</div>
</div>
</article>
{% else %}<div class="p-10 text-center text-sm text-slate-500">No imported bank transactions.</div>{% endfor %}
</div>
{% if pages > 1 %}<div class="flex justify-between border-t p-4 text-sm"><span>Page {{ page }} of {{ pages }}</span><div class="flex gap-2">{% if page>1 %}<a href="?client_id={{ selected_client.id }}&page={{ page-1 }}&per_page={{ per_page }}" class="rounded border px-3 py-1">Previous</a>{% endif %}{% if page<pages %}<a href="?client_id={{ selected_client.id }}&page={{ page+1 }}&per_page={{ per_page }}" class="rounded border px-3 py-1">Next</a>{% endif %}</div></div>{% endif %}
</section>
{% endif %}
</div>
{% endblock %}
@@ -20,6 +20,7 @@
{% if selected_client %}<a href="/tools/accounting/purchase-enrichment?client_id={{ selected_client.id }}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">E-Invoice / E-Way Bill</a>{% endif %}
{% if selected_client %}<a href="/tools/accounting/purchase-review?client_id={{ selected_client.id }}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Purchase Review</a>{% endif %}
{% if selected_client %}<a href="/tools/accounting/purchase-posting?client_id={{ selected_client.id }}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Post to Tally</a>{% endif %}
{% if selected_client %}<a href="/tools/accounting/bank-posting?client_id={{ selected_client.id }}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Bank → Tally</a>{% endif %}
{% if selected_client %}<a href="/tools/tally/depreciation?client_id={{ selected_client.id }}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Depreciation (IT)</a>{% endif %}
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}&refresh=1{% else %}?refresh=1{% endif %}" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">Refresh Tally Companies</a>
</div>