Files
arrr-erp/app/modules/accounting/templates/accounting/voucher_engine.html
T
2026-08-23 18:45:21 +05:30

363 lines
21 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-3">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Accounting Engine · Phase 17</p>
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Common Native Tally Voucher Engine</h1>
<p class="mt-1 max-w-5xl text-sm text-slate-500">
Prepare one balanced accounting transaction and post it with the correct native Tally voucher type: Purchase, Sales, Receipt, Payment, Contra or Journal. Existing specialized accounting tools remain unchanged and can progressively feed this common engine.
</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/tools/accounting/voucher-engine/automatic{% 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">Automatic Preparation</a>
<a href="/tools/accounting/chart-of-accounts{% 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">Chart of Accounts</a>
<a href="/tools/tally{% 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">Back to Accounting</a>
</div>
</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="get" class="grid gap-4 lg:grid-cols-[1fr_1fr_auto]">
<label class="text-sm font-semibold">ERP Client
<select name="client_id" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2">
<option value="">Select client</option>
{% for c in clients %}
<option value="{{ c.id }}" {% if selected_client and selected_client.id==c.id %}selected{% endif %}>{{ c.client_name }}</option>
{% endfor %}
</select>
</label>
<label class="text-sm font-semibold">Tally Company / Chart of Accounts
<select name="tally_guid" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2">
<option value="">Select company</option>
{% for c in company_options %}
<option value="{{ c.guid }}" {% if selected_tally_guid==c.guid %}selected{% endif %}>{{ c.name }}</option>
{% endfor %}
</select>
</label>
<div class="flex items-end"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Open Engine</button></div>
</form>
</section>
{% if selected_client and selected_tally_guid %}
<section class="rounded-2xl border border-blue-200 bg-blue-50/40 p-5">
<h2 class="font-semibold text-slate-900">Native Tally voucher selection</h2>
<div class="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3 text-sm">
<div class="rounded-xl bg-white p-3"><b>Purchase</b><br><span class="text-slate-500">Supplier bills, purchases, direct/indirect expenses and asset bills.</span></div>
<div class="rounded-xl bg-white p-3"><b>Sales</b><br><span class="text-slate-500">Customer invoices, sales and billed direct/indirect income.</span></div>
<div class="rounded-xl bg-white p-3"><b>Receipt</b><br><span class="text-slate-500">Money received into Bank/Cash from customers, income, loans or capital.</span></div>
<div class="rounded-xl bg-white p-3"><b>Payment</b><br><span class="text-slate-500">Money paid from Bank/Cash to suppliers, expenses, assets or loans.</span></div>
<div class="rounded-xl bg-white p-3"><b>Contra</b><br><span class="text-slate-500">Bank-to-bank, bank-to-cash and cash-to-bank transfers.</span></div>
<div class="rounded-xl bg-white p-3"><b>Journal</b><br><span class="text-slate-500">Accruals, provisions, depreciation, reclassification and adjustments.</span></div>
</div>
</section>
<section class="rounded-2xl bg-white p-5 shadow-soft">
<div class="mb-4">
<h2 class="font-semibold text-slate-900">Manual / Adjustment Voucher Draft</h2>
<p class="text-sm text-slate-500">Automatic Bank/E-Way Bill preparation is available above. Use this manual form for adjustments, provisions, accruals, depreciation, reclassification, audit adjustments or unresolved source transactions. Every ledger must exist in the Chart of Accounts and Debit must equal Credit.</p>
</div>
<form method="post" action="/tools/accounting/voucher-engine/draft" id="voucherDraftForm" class="space-y-5">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
<input type="hidden" name="tally_guid" value="{{ selected_tally_guid }}">
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
<label class="text-xs font-semibold">Business Nature
<select name="business_nature" id="businessNature" class="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2">
{% for code,label in business_natures %}<option value="{{ code }}">{{ label }}</option>{% endfor %}
</select>
</label>
<label class="text-xs font-semibold">Tally Voucher Type
<select name="voucher_type" id="voucherType" class="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2">
{% for v in voucher_types %}<option value="{{ v }}">{{ v }}</option>{% endfor %}
</select>
</label>
<label class="text-xs font-semibold">Voucher Date
<input type="date" name="voucher_date" required class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2">
</label>
<label class="text-xs font-semibold">Reference
<input name="reference" class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2">
</label>
<label class="text-xs font-semibold">Source
<select name="source_type" class="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2">
<option value="manual">Manual</option>
<option value="bank">Bank Analyzer</option>
<option value="gstr2b">GSTR-2B</option>
<option value="sales">Sales / E-Invoice / GSTR-1</option>
<option value="audit_adjustment">Audit Adjustment</option>
</select>
</label>
</div>
<label class="block text-xs font-semibold">Narration
<textarea name="narration" rows="2" class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"></textarea>
</label>
<div>
<div class="mb-2 flex items-center justify-between">
<div>
<h3 class="font-semibold">Voucher Lines</h3>
<p class="text-xs text-slate-500">Add as many balanced Debit/Credit lines as needed.</p>
</div>
<button type="button" id="addLine" class="rounded-lg border border-slate-300 px-3 py-2 text-xs font-semibold">Add Line</button>
</div>
<div id="lineRows" class="space-y-2">
{% for index in range(2) %}
<div class="voucher-line grid gap-2 rounded-xl border border-slate-200 p-3 lg:grid-cols-[1fr_100px_160px_1fr_auto]">
<select name="ledger_name" required class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm">
<option value="">Select ledger</option>
{% for ledger in ledgers %}
{% set role = ledger.manual_role_code or ledger.suggested_role_code %}
<option value="{{ ledger.name }}">{{ ledger.name }} · {{ role_labels.get(role, role) }}</option>
{% endfor %}
</select>
<select name="dr_cr" required class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm">
<option value="DR" {% if index==0 %}selected{% endif %}>Debit</option>
<option value="CR" {% if index==1 %}selected{% endif %}>Credit</option>
</select>
<input name="amount" type="number" min="0.01" step="0.01" required placeholder="Amount" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
<input name="line_note" placeholder="Line note (optional)" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
<button type="button" class="remove-line rounded-lg border border-red-200 px-3 py-2 text-xs font-semibold text-red-700">Remove</button>
</div>
{% endfor %}
</div>
<div class="mt-3 flex flex-wrap justify-end gap-5 text-sm font-semibold">
<span>Debit: ₹<span id="debitTotal">0.00</span></span>
<span>Credit: ₹<span id="creditTotal">0.00</span></span>
<span id="balanceState" class="text-amber-700">Not balanced</span>
</div>
</div>
<button class="w-full rounded-xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white">Create Balanced Voucher Draft</button>
</form>
</section>
{% elif selected_client %}
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
Refresh the client's Chart of Accounts and select a mapped Tally company before creating vouchers.
</div>
{% endif %}
{% if selected_client %}
<section class="rounded-2xl bg-white shadow-soft">
<div class="border-b border-slate-200 px-5 py-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="font-semibold">Voucher Review & Posting Queue</h2>
<p class="text-sm text-slate-500">{{ total }} draft(s). Controlled flow: Draft → Reviewed → Approved → Tally Preflight → Post.</p>
</div>
<form method="get" class="flex gap-2">
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
<select name="status" class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm">
<option value="">All statuses</option>
{% for s in ['draft','reviewed','approved','preflight_queued','preflight_claimed','preflight_ready','preflight_failed','posting_queued','posting_claimed','posted','posting_failed','void'] %}
<option value="{{ s }}" {% if status_filter==s %}selected{% endif %}>{{ s|replace('_',' ')|title }}</option>
{% endfor %}
</select>
<button class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-semibold">Filter</button>
</form>
</div>
</div>
<div class="divide-y divide-slate-100">
{% for draft in drafts %}
<article class="p-5">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-full bg-slate-900 px-2 py-1 text-xs font-semibold text-white">{{ draft.voucher_type }}</span>
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold">{{ draft.status|replace('_',' ')|title }}</span>
<span class="text-xs text-slate-500">Draft #{{ draft.id }}</span>
</div>
<div class="mt-2 font-semibold">{{ draft.company_name }} · {{ draft.voucher_date }}{% if draft.reference %} · Ref {{ draft.reference }}{% endif %}</div>
<div class="mt-1 text-sm text-slate-500">Nature: {{ draft.business_nature|replace('_',' ')|title }} · Source: {{ draft.source_type }} · Audit use: {{ draft.audit_area or 'General Ledger' }}</div>
{% if draft.narration %}<div class="mt-1 text-sm text-slate-500">{{ draft.narration }}</div>{% endif %}
</div>
<div class="text-right">
<div class="text-lg font-semibold">₹{{ '%.2f'|format(draft.total_debit) }}</div>
{% if draft.tally_voucher_number %}<div class="text-xs text-emerald-700">Tally voucher {{ draft.tally_voucher_number }}</div>{% endif %}
</div>
</div>
<div class="mt-4 overflow-x-auto rounded-xl border border-slate-200">
<table class="min-w-full text-sm">
<thead><tr class="bg-slate-50 text-left text-xs uppercase text-slate-500"><th class="px-3 py-2">Ledger</th><th class="px-3 py-2">Role</th><th class="px-3 py-2">Dr/Cr</th><th class="px-3 py-2 text-right">Amount</th></tr></thead>
<tbody>
{% for line in draft.lines %}
<tr class="border-t border-slate-100">
<td class="px-3 py-2 font-semibold">{{ line.ledger_name }}</td>
<td class="px-3 py-2">{{ role_labels.get(line.ledger_role_code, line.ledger_role_code) }}</td>
<td class="px-3 py-2">{{ line.dr_cr }}</td>
<td class="px-3 py-2 text-right">₹{{ '%.2f'|format(line.amount) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if draft.items %}
<div class="mt-3 overflow-x-auto rounded-xl border border-blue-200">
<div class="border-b border-blue-100 bg-blue-50 px-3 py-2 text-xs font-semibold uppercase tracking-wider text-blue-800">Stock-item allocation</div>
<table class="min-w-full text-xs">
<thead><tr class="bg-slate-50 text-left uppercase text-slate-500"><th class="px-3 py-2">Stock Item</th><th class="px-3 py-2">HSN</th><th class="px-3 py-2 text-right">Qty</th><th class="px-3 py-2">Unit</th><th class="px-3 py-2 text-right">Rate</th><th class="px-3 py-2 text-right">Amount</th><th class="px-3 py-2">Accounting Ledger</th></tr></thead>
<tbody>
{% for item in draft.items %}
<tr class="border-t border-slate-100">
<td class="px-3 py-2 font-semibold">{{ item.stock_item_name }}</td>
<td class="px-3 py-2">{{ item.hsn_sac or '-' }}</td>
<td class="px-3 py-2 text-right">{{ item.quantity }}</td>
<td class="px-3 py-2">{{ item.unit or '-' }}</td>
<td class="px-3 py-2 text-right">{{ '%.4f'|format(item.rate) }}</td>
<td class="px-3 py-2 text-right">₹{{ '%.2f'|format(item.amount) }}</td>
<td class="px-3 py-2">{{ item.accounting_ledger_name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if draft.last_error %}<div class="mt-3 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800">{{ draft.last_error }}</div>{% endif %}
{% if draft.status == 'preflight_ready' %}<div class="mt-3 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-800">Tally preflight completed. Ledger existence, balancing and duplicate checks completed.</div>{% endif %}
<div class="mt-4 flex flex-wrap gap-2">
{% if draft.status == 'draft' %}
<form method="post" action="/tools/accounting/voucher-engine/draft/{{ draft.id }}/review" class="flex gap-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}">
<input name="note" placeholder="Review note" class="rounded-lg border border-slate-300 px-3 py-2 text-xs">
<button class="rounded-lg bg-blue-700 px-3 py-2 text-xs font-semibold text-white">Mark Reviewed</button>
</form>
{% endif %}
{% if draft.status == 'reviewed' %}
<form method="post" action="/tools/accounting/voucher-engine/draft/{{ draft.id }}/approve">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}">
<button class="rounded-lg bg-emerald-700 px-3 py-2 text-xs font-semibold text-white">Approve</button>
</form>
{% endif %}
{% if draft.status in ['approved','preflight_failed'] %}
<form method="post" action="/tools/accounting/voucher-engine/draft/{{ draft.id }}/preflight" class="flex gap-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}">
<select name="workstation_id" required class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs">
<option value="">Select workstation</option>
{% for ws in workstations %}<option value="{{ ws.id }}">{{ ws.machine_name }} · Agent {{ ws.agent_version or '-' }}</option>{% endfor %}
</select>
<button class="rounded-lg bg-slate-900 px-3 py-2 text-xs font-semibold text-white">Run Tally Preflight</button>
</form>
{% endif %}
{% if draft.status == 'preflight_ready' %}
<form method="post" action="/tools/accounting/voucher-engine/draft/{{ draft.id }}/post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}">
<button class="rounded-lg bg-red-700 px-3 py-2 text-xs font-semibold text-white" onclick="return confirm('Post this approved {{ draft.voucher_type }} voucher to Tally?')">Post {{ draft.voucher_type }} to Tally</button>
</form>
{% endif %}
{% if draft.status not in ['posted','void','preflight_queued','preflight_claimed','posting_queued','posting_claimed'] %}
<form method="post" action="/tools/accounting/voucher-engine/draft/{{ draft.id }}/void">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}">
<button class="rounded-lg border border-slate-300 px-3 py-2 text-xs font-semibold">Void Draft</button>
</form>
{% endif %}
</div>
</article>
{% else %}
<div class="p-10 text-center text-slate-500">No voucher drafts for this client yet.</div>
{% endfor %}
</div>
{% if pages > 1 %}
<div class="flex items-center justify-between border-t border-slate-200 p-4 text-sm">
<span>Page {{ page }} of {{ pages }}</span>
<div class="flex gap-2">
{% if page > 1 %}<a class="rounded-lg border border-slate-300 px-3 py-1" href="?client_id={{ selected_client.id }}&status={{ status_filter }}&page={{ page-1 }}&per_page={{ per_page }}">Previous</a>{% endif %}
{% if page < pages %}<a class="rounded-lg border border-slate-300 px-3 py-1" href="?client_id={{ selected_client.id }}&status={{ status_filter }}&page={{ page+1 }}&per_page={{ per_page }}">Next</a>{% endif %}
</div>
</div>
{% endif %}
</section>
{% endif %}
</div>
{% if selected_client and selected_tally_guid %}
<script>
(function () {
const container = document.getElementById("lineRows");
const addButton = document.getElementById("addLine");
const nature = document.getElementById("businessNature");
const voucherType = document.getElementById("voucherType");
const debitTotal = document.getElementById("debitTotal");
const creditTotal = document.getElementById("creditTotal");
const balanceState = document.getElementById("balanceState");
const voucherMap = {
PURCHASE: "Purchase",
DIRECT_EXPENSE: "Purchase",
INDIRECT_EXPENSE: "Purchase",
SALES: "Sales",
DIRECT_INCOME: "Sales",
INDIRECT_INCOME: "Sales",
RECEIPT: "Receipt",
PAYMENT: "Payment",
CONTRA: "Contra",
ADJUSTMENT: "Journal"
};
function updateTotals() {
let dr = 0, cr = 0;
container.querySelectorAll(".voucher-line").forEach(function (row) {
const side = row.querySelector('[name="dr_cr"]').value;
const amount = Number(row.querySelector('[name="amount"]').value || 0);
if (side === "DR") dr += amount; else if (side === "CR") cr += amount;
});
debitTotal.textContent = dr.toFixed(2);
creditTotal.textContent = cr.toFixed(2);
const ok = dr > 0 && cr > 0 && Math.abs(dr - cr) < 0.01;
balanceState.textContent = ok ? "Balanced" : "Not balanced";
balanceState.className = ok ? "text-emerald-700" : "text-amber-700";
}
function wire(row) {
row.querySelectorAll("input,select").forEach(function (el) {
el.addEventListener("input", updateTotals);
el.addEventListener("change", updateTotals);
});
row.querySelector(".remove-line").addEventListener("click", function () {
if (container.querySelectorAll(".voucher-line").length <= 2) return;
row.remove();
updateTotals();
});
}
container.querySelectorAll(".voucher-line").forEach(wire);
addButton.addEventListener("click", function () {
const source = container.querySelector(".voucher-line");
const clone = source.cloneNode(true);
clone.querySelector('[name="ledger_name"]').value = "";
clone.querySelector('[name="dr_cr"]').value = "DR";
clone.querySelector('[name="amount"]').value = "";
clone.querySelector('[name="line_note"]').value = "";
container.appendChild(clone);
wire(clone);
updateTotals();
});
nature.addEventListener("change", function () {
voucherType.value = voucherMap[nature.value] || "Journal";
});
updateTotals();
})();
</script>
{% endif %}
{% endblock %}