253 lines
17 KiB
HTML
253 lines
17 KiB
HTML
{% extends "ui/templates/base/layout.html" %}
|
|
{% block content %}
|
|
<div class="space-y-6">
|
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
<div>
|
|
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Tools · Accounting Intelligence</p>
|
|
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Controlled Tally Purchase Posting</h1>
|
|
<p class="mt-1 max-w-4xl text-sm text-slate-500">Phase 10 posts only accountant-reviewed purchases. Every voucher must pass workstation preflight, open-company verification, Tally ledger validation, duplicate screening and a durable Phase 2 idempotent job. Financial write jobs are never automatically retried.</p>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<a href="/tools/accounting/purchase-review{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" 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>
|
|
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Back to Tally</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if message %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">{{ message }}</div>{% endif %}
|
|
{% if error %}<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">{{ error }}</div>{% endif %}
|
|
|
|
<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
|
<strong>Controlled write boundary:</strong> Phase 10 never creates Tally ledgers or stock items. If a creditor, purchase ledger or GST input ledger cannot be verified in the currently opened Tally company, posting is blocked.
|
|
</div>
|
|
|
|
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
|
<form method="get" action="/tools/accounting/purchase-posting" class="grid gap-4 md:grid-cols-4">
|
|
<label class="text-sm font-medium text-slate-700 md:col-span-2">
|
|
ERP Client
|
|
<select name="client_id" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
|
<option value="">Select client</option>
|
|
{% for client in clients %}
|
|
<option value="{{ client.id }}" {% if selected_client and selected_client.id == client.id %}selected{% endif %}>{{ client.client_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
{% if selected_client %}
|
|
<label class="text-sm font-medium text-slate-700">
|
|
Rows
|
|
<select name="per_page" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
|
{% for size in [10,25,50,100] %}<option value="{{ size }}" {% if result and result.per_page == size %}selected{% endif %}>{{ size }}</option>{% endfor %}
|
|
</select>
|
|
</label>
|
|
{% endif %}
|
|
<div class="flex items-end"><button class="w-full rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Load Reviewed Purchases</button></div>
|
|
</form>
|
|
</section>
|
|
|
|
{% if selected_client and result %}
|
|
<section class="rounded-2xl bg-white shadow-soft">
|
|
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-5 py-4">
|
|
<div>
|
|
<h2 class="font-semibold text-slate-900">Reviewed Purchase Queue</h2>
|
|
<p class="mt-1 text-sm text-slate-500">{{ result.total }} reviewed purchase(s) · page {{ result.page }} of {{ result.pages }}</p>
|
|
</div>
|
|
<a href="/tools/accounting/purchase-posting?client_id={{ selected_client.id }}&page={{ result.page }}&per_page={{ result.per_page }}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Refresh Job Status</a>
|
|
</div>
|
|
|
|
<div class="divide-y divide-slate-100">
|
|
{% for tuple_row in result.rows %}
|
|
{% set row = tuple_row[0] %}
|
|
{% set attempt = attempts.get(row.id) %}
|
|
{% set choice = choices.get(row.id) %}
|
|
<article class="p-5">
|
|
<div class="grid gap-5 xl:grid-cols-12">
|
|
<div class="xl:col-span-3">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<h3 class="font-semibold text-slate-900">{{ row.supplier_name or row.supplier_gstin or 'Supplier' }}</h3>
|
|
{% if row.posting_status == 'posted' or (attempt and attempt.status == 'posted') %}
|
|
<span class="rounded-full bg-emerald-50 px-2 py-1 text-xs font-semibold text-emerald-700">Posted</span>
|
|
{% elif attempt %}
|
|
<span class="rounded-full bg-blue-50 px-2 py-1 text-xs font-semibold text-blue-700">{{ attempt.status|replace('_',' ')|title }}</span>
|
|
{% else %}
|
|
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-600">Not started</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="mt-2 space-y-1 text-sm text-slate-600">
|
|
<div>GSTIN: <span class="font-medium text-slate-900">{{ row.supplier_gstin or '-' }}</span></div>
|
|
<div>Invoice: <span class="font-medium text-slate-900">{{ row.invoice_number }}</span> · {{ row.invoice_date }}</div>
|
|
<div>Taxable: <span class="font-medium text-slate-900">₹{{ '%.2f'|format(row.taxable_value) }}</span></div>
|
|
<div>Invoice Value: <span class="font-medium text-slate-900">₹{{ '%.2f'|format(row.invoice_value) }}</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="xl:col-span-3">
|
|
<div class="rounded-xl bg-slate-50 p-3">
|
|
<div class="text-xs font-semibold uppercase text-slate-500">Reviewed treatment</div>
|
|
<div class="mt-1 font-semibold text-slate-900">{{ row.final_ledger_name }}</div>
|
|
<div class="mt-2 grid grid-cols-2 gap-2 text-xs text-slate-600">
|
|
<div>IGST ₹{{ '%.2f'|format(row.igst) }}</div>
|
|
<div>CGST ₹{{ '%.2f'|format(row.cgst) }}</div>
|
|
<div>SGST ₹{{ '%.2f'|format(row.sgst) }}</div>
|
|
<div>Cess ₹{{ '%.2f'|format(row.cess) }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="xl:col-span-6">
|
|
{% if not attempt %}
|
|
<form method="post" action="/tools/accounting/purchase-posting/purchase/{{ row.id }}/preflight" class="rounded-xl border border-slate-200 p-4">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
|
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
|
<input type="hidden" name="page" value="{{ result.page }}" />
|
|
<input type="hidden" name="per_page" value="{{ result.per_page }}" />
|
|
<label class="text-sm font-medium text-slate-700">
|
|
Workstation running the correct open Tally company
|
|
<select name="workstation_id" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
|
<option value="">Select workstation</option>
|
|
{% for ws in workstations %}
|
|
<option value="{{ ws.id }}">{{ ws.machine_name }} · Agent {{ ws.agent_version or '-' }} · {{ 'Tally connected' if ws.tally_connected else 'Tally offline' }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
<button class="mt-3 rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Run Tally Preflight</button>
|
|
</form>
|
|
|
|
{% elif attempt.status in ['preflight_queued','preflight_claimed'] %}
|
|
<div class="rounded-xl border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800">
|
|
Preflight is waiting for workstation <strong>#{{ attempt.workstation_agent_id }}</strong>. Refresh after the Local Agent processes it.
|
|
</div>
|
|
|
|
{% elif attempt.status == 'preflight_failed' %}
|
|
<div class="rounded-xl border border-red-200 bg-red-50 p-4">
|
|
<div class="text-sm font-semibold text-red-800">Preflight failed</div>
|
|
<div class="mt-1 text-sm text-red-700">{{ attempt.last_error }}</div>
|
|
<form method="post" action="/tools/accounting/purchase-posting/purchase/{{ row.id }}/preflight" class="mt-3">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
|
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
|
<input type="hidden" name="page" value="{{ result.page }}" />
|
|
<input type="hidden" name="per_page" value="{{ result.per_page }}" />
|
|
<select name="workstation_id" required class="w-full rounded-lg border border-red-300 bg-white px-3 py-2 text-sm">
|
|
<option value="">Select workstation</option>
|
|
{% for ws in workstations %}<option value="{{ ws.id }}">{{ ws.machine_name }} · {{ 'Tally connected' if ws.tally_connected else 'Tally offline' }}</option>{% endfor %}
|
|
</select>
|
|
<button class="mt-2 rounded-lg bg-red-700 px-3 py-2 text-sm font-semibold text-white">Run New Preflight</button>
|
|
</form>
|
|
</div>
|
|
|
|
{% elif attempt.status == 'preflight_ready' and choice %}
|
|
{% if choice.duplicate_candidates %}
|
|
<div class="rounded-xl border border-red-200 bg-red-50 p-4">
|
|
<div class="text-sm font-semibold text-red-800">Possible duplicate found in Tally — posting blocked</div>
|
|
{% for dup in choice.duplicate_candidates[:5] %}
|
|
<div class="mt-2 text-xs text-red-700">{{ dup.voucher_number or '-' }} · {{ dup.reference or '-' }} · {{ dup.party_ledger_name or '-' }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% elif not choice.purchase_ledger_verified %}
|
|
<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Reviewed purchase ledger <strong>{{ row.final_ledger_name }}</strong> was not found in the currently opened Tally company. Sync/fix the mapping before posting.</div>
|
|
{% else %}
|
|
<form method="post" action="/tools/accounting/purchase-posting/purchase/{{ row.id }}/post" class="rounded-xl border border-emerald-200 bg-emerald-50 p-4">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
|
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
|
<input type="hidden" name="page" value="{{ result.page }}" />
|
|
<input type="hidden" name="per_page" value="{{ result.per_page }}" />
|
|
|
|
<div class="text-xs font-semibold uppercase text-emerald-700">Preflight passed · {{ choice.company_name }}</div>
|
|
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
|
<label class="text-xs font-semibold text-slate-700">
|
|
Creditor / Party Ledger
|
|
<select name="party_ledger_name" required class="mt-1 w-full rounded-lg border border-emerald-300 bg-white px-2 py-2 text-sm">
|
|
<option value="">Select party ledger</option>
|
|
{% for x in choice.party_candidates %}<option value="{{ x.name }}">{{ x.name }}{% if x.gstin %} · {{ x.gstin }}{% endif %}</option>{% endfor %}
|
|
</select>
|
|
</label>
|
|
|
|
{% if row.igst > 0 %}
|
|
<label class="text-xs font-semibold text-slate-700">
|
|
Input IGST Ledger
|
|
<select name="input_igst_ledger_name" required class="mt-1 w-full rounded-lg border border-emerald-300 bg-white px-2 py-2 text-sm">
|
|
<option value="">Select IGST ledger</option>
|
|
{% for x in choice.igst_candidates %}<option value="{{ x.name }}">{{ x.name }}</option>{% endfor %}
|
|
</select>
|
|
</label>
|
|
{% else %}<input type="hidden" name="input_igst_ledger_name" value="" />{% endif %}
|
|
|
|
{% if row.cgst > 0 %}
|
|
<label class="text-xs font-semibold text-slate-700">
|
|
Input CGST Ledger
|
|
<select name="input_cgst_ledger_name" required class="mt-1 w-full rounded-lg border border-emerald-300 bg-white px-2 py-2 text-sm">
|
|
<option value="">Select CGST ledger</option>
|
|
{% for x in choice.cgst_candidates %}<option value="{{ x.name }}">{{ x.name }}</option>{% endfor %}
|
|
</select>
|
|
</label>
|
|
{% else %}<input type="hidden" name="input_cgst_ledger_name" value="" />{% endif %}
|
|
|
|
{% if row.sgst > 0 %}
|
|
<label class="text-xs font-semibold text-slate-700">
|
|
Input SGST Ledger
|
|
<select name="input_sgst_ledger_name" required class="mt-1 w-full rounded-lg border border-emerald-300 bg-white px-2 py-2 text-sm">
|
|
<option value="">Select SGST ledger</option>
|
|
{% for x in choice.sgst_candidates %}<option value="{{ x.name }}">{{ x.name }}</option>{% endfor %}
|
|
</select>
|
|
</label>
|
|
{% else %}<input type="hidden" name="input_sgst_ledger_name" value="" />{% endif %}
|
|
|
|
{% if row.cess > 0 %}
|
|
<label class="text-xs font-semibold text-slate-700">
|
|
Input Cess Ledger
|
|
<select name="input_cess_ledger_name" required class="mt-1 w-full rounded-lg border border-emerald-300 bg-white px-2 py-2 text-sm">
|
|
<option value="">Select Cess ledger</option>
|
|
{% for x in choice.cess_candidates %}<option value="{{ x.name }}">{{ x.name }}</option>{% endfor %}
|
|
</select>
|
|
</label>
|
|
{% else %}<input type="hidden" name="input_cess_ledger_name" value="" />{% endif %}
|
|
</div>
|
|
<button class="mt-4 rounded-xl bg-emerald-700 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-800">Post Reviewed Purchase to Tally</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
{% elif attempt.status in ['posting_queued','posting_claimed'] %}
|
|
<div class="rounded-xl border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800">Purchase posting job is being executed by the selected workstation. Do not submit another posting job.</div>
|
|
|
|
{% elif attempt.status == 'posted' %}
|
|
<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4">
|
|
<div class="text-sm font-semibold text-emerald-800">Posted successfully to Tally</div>
|
|
<div class="mt-2 text-sm text-slate-700">Company: {{ attempt.tally_company_name }}</div>
|
|
<div class="text-sm text-slate-700">Voucher/ID: {{ attempt.tally_voucher_number or attempt.tally_voucher_id or '-' }}</div>
|
|
<div class="text-sm text-slate-700">Reference: {{ attempt.tally_reference or row.invoice_number }}</div>
|
|
</div>
|
|
|
|
{% elif attempt.status == 'posting_indeterminate' %}
|
|
<div class="rounded-xl border border-red-300 bg-red-50 p-4">
|
|
<div class="text-sm font-bold text-red-800">Manual verification required — do not retry</div>
|
|
<div class="mt-1 text-sm text-red-700">{{ attempt.last_error }}</div>
|
|
<div class="mt-2 text-xs text-red-700">The Local Agent began execution but acknowledgement was lost. Check Tally for invoice {{ row.invoice_number }} before any further action.</div>
|
|
</div>
|
|
|
|
{% elif attempt.status == 'posting_failed' %}
|
|
<div class="rounded-xl border border-red-200 bg-red-50 p-4">
|
|
<div class="text-sm font-semibold text-red-800">Tally posting failed</div>
|
|
<div class="mt-1 text-sm text-red-700">{{ attempt.last_error }}</div>
|
|
<div class="mt-2 text-xs text-red-700">Run a new preflight after correcting the Tally-side issue. The prior financial write job is never automatically retried.</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
{% else %}
|
|
<div class="p-10 text-center text-sm text-slate-500">No accountant-reviewed purchases are available for posting.</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% if result.pages > 1 %}
|
|
<div class="flex items-center justify-between border-t border-slate-200 px-5 py-4">
|
|
<span class="text-sm text-slate-500">Page {{ result.page }} of {{ result.pages }}</span>
|
|
<div class="flex gap-2">
|
|
{% if result.page > 1 %}<a href="?client_id={{ selected_client.id }}&per_page={{ result.per_page }}&page={{ result.page - 1 }}" class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-semibold">Previous</a>{% endif %}
|
|
{% if result.page < result.pages %}<a href="?client_id={{ selected_client.id }}&per_page={{ result.per_page }}&page={{ result.page + 1 }}" class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-semibold">Next</a>{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|