Add Phase 21 stock item master mapping intelligence
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
{% 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 · Phase 21</p>
|
||||
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Stock Item Intelligence</h1>
|
||||
<p class="mt-1 max-w-5xl text-sm text-slate-500">
|
||||
Mirror Tally Stock Groups, Stock Items and Units into ERP, learn client-specific source-description mappings, and convert source UQC/units safely before item-wise Purchase or Sales vouchers reach Tally.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/tools/accounting/voucher-engine{% 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">Voucher Engine</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 md:grid-cols-[1.4fr_1.4fr_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 client in clients %}
|
||||
<option value="{{ client.id }}" {% if selected_client and selected_client.id==client.id %}selected{% endif %}>{{ client.client_code }} · {{ client.client_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm font-semibold">Mapped Tally Company
|
||||
<select name="tally_guid" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2">
|
||||
<option value="">Select mapped company</option>
|
||||
{% for company in companies %}
|
||||
<option value="{{ company.guid }}" {% if selected_tally_guid==company.guid %}selected{% endif %}>{{ company.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<div class="flex items-end"><button class="rounded-xl bg-brand-600 px-5 py-2 text-sm font-semibold text-white">Open</button></div>
|
||||
</form>
|
||||
|
||||
{% if selected_client and selected_tally_guid %}
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="text-sm text-slate-600">
|
||||
{% if last_sync %}Last sync {{ last_sync.created_at_utc }} · {{ last_sync.stock_item_count }} items · {{ last_sync.stock_group_count }} groups · {{ last_sync.unit_count }} units{% else %}No ERP Stock Master snapshot yet.{% endif %}
|
||||
</div>
|
||||
<form method="post" action="/tools/accounting/stock-items/sync">
|
||||
<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 }}">
|
||||
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white" {% if not node_online %}disabled{% endif %}>Synchronize Stock Master from Tally</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if selected_client and selected_tally_guid %}
|
||||
<section class="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<div class="rounded-2xl bg-white p-4 shadow-soft"><div class="text-xs text-slate-500">Stock Items</div><div class="mt-1 text-2xl font-bold">{{ summary.items }}</div></div>
|
||||
<div class="rounded-2xl bg-white p-4 shadow-soft"><div class="text-xs text-slate-500">Stock Groups</div><div class="mt-1 text-2xl font-bold">{{ summary.groups }}</div></div>
|
||||
<div class="rounded-2xl bg-white p-4 shadow-soft"><div class="text-xs text-slate-500">Units</div><div class="mt-1 text-2xl font-bold">{{ summary.units }}</div></div>
|
||||
<div class="rounded-2xl bg-white p-4 shadow-soft"><div class="text-xs text-slate-500">Learned Item Mappings</div><div class="mt-1 text-2xl font-bold">{{ summary.mappings }}</div></div>
|
||||
<div class="rounded-2xl bg-white p-4 shadow-soft"><div class="text-xs text-slate-500">Unit Mappings</div><div class="mt-1 text-2xl font-bold">{{ summary.unit_mappings }}</div></div>
|
||||
</section>
|
||||
|
||||
<nav class="flex flex-wrap gap-2">
|
||||
{% for code,label in [('items','Stock Master'),('review','Needs Mapping'),('mappings','Mappings'),('units','Units')] %}
|
||||
<a href="?client_id={{ selected_client.id }}&tally_guid={{ selected_tally_guid }}&view={{ code }}" class="rounded-xl px-4 py-2 text-sm font-semibold {% if view==code %}bg-brand-600 text-white{% else %}border border-slate-300 bg-white text-slate-700{% endif %}">{{ label }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{% if view=='items' %}
|
||||
<section class="rounded-2xl bg-white shadow-soft">
|
||||
<div class="border-b border-slate-200 p-5">
|
||||
<h2 class="font-semibold">Tally Stock Item Master</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Read-only ERP mirror of the selected Tally company's current Stock Items.</p>
|
||||
<form method="get" class="mt-4 grid gap-3 md:grid-cols-4">
|
||||
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
|
||||
<input type="hidden" name="tally_guid" value="{{ selected_tally_guid }}">
|
||||
<input type="hidden" name="view" value="items">
|
||||
<input name="q" value="{{ q }}" placeholder="Search item / group / HSN" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
|
||||
<select name="group" class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm">
|
||||
<option value="">All Stock Groups</option>
|
||||
{% for row in groups %}<option value="{{ row.name }}" {% if group_filter==row.name %}selected{% endif %}>{{ row.name }}</option>{% endfor %}
|
||||
</select>
|
||||
<input name="hsn" value="{{ hsn_filter }}" placeholder="HSN filter" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
|
||||
<button class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-semibold">Filter</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead><tr class="bg-slate-50 text-left text-xs uppercase text-slate-500"><th class="px-4 py-3">Stock Item</th><th class="px-4 py-3">Group</th><th class="px-4 py-3">Unit</th><th class="px-4 py-3">HSN</th><th class="px-4 py-3">GST</th><th class="px-4 py-3 text-right">Opening Qty</th><th class="px-4 py-3 text-right">Opening Value</th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr class="border-t border-slate-100">
|
||||
<td class="px-4 py-3"><div class="font-semibold">{{ item.name }}</div>{% if item.category_name %}<div class="text-xs text-slate-500">Category: {{ item.category_name }}</div>{% endif %}</td>
|
||||
<td class="px-4 py-3">{{ item.parent_group_name or '-' }}</td>
|
||||
<td class="px-4 py-3">{{ item.base_units or '-' }}{% if item.additional_units %}<div class="text-xs text-slate-500">Alt: {{ item.additional_units }}</div>{% endif %}</td>
|
||||
<td class="px-4 py-3">{{ item.hsn_code or '-' }}</td>
|
||||
<td class="px-4 py-3">{{ item.gst_applicable or '-' }}{% if item.gst_type_of_supply %}<div class="text-xs text-slate-500">{{ item.gst_type_of_supply }}</div>{% endif %}</td>
|
||||
<td class="px-4 py-3 text-right">{{ item.opening_balance }}</td>
|
||||
<td class="px-4 py-3 text-right">₹{{ '%.2f'|format(item.opening_value) }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="px-4 py-10 text-center text-slate-500">No Stock Items synchronized yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if pages > 1 %}
|
||||
<div class="flex items-center justify-between border-t border-slate-200 p-4 text-sm">
|
||||
<div>{{ total }} item(s) · Page {{ page }} of {{ pages }}</div>
|
||||
<div class="flex gap-2">
|
||||
{% if page>1 %}<a class="rounded-lg border border-slate-300 px-3 py-2" href="?client_id={{ selected_client.id }}&tally_guid={{ selected_tally_guid }}&view=items&q={{ q }}&group={{ group_filter }}&hsn={{ hsn_filter }}&page={{ page-1 }}">Previous</a>{% endif %}
|
||||
{% if page<pages %}<a class="rounded-lg border border-slate-300 px-3 py-2" href="?client_id={{ selected_client.id }}&tally_guid={{ selected_tally_guid }}&view=items&q={{ q }}&group={{ group_filter }}&hsn={{ hsn_filter }}&page={{ page+1 }}">Next</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% elif view=='review' %}
|
||||
<section class="rounded-2xl bg-white shadow-soft">
|
||||
<div class="border-b border-slate-200 p-5">
|
||||
<h2 class="font-semibold">Needs Stock Item Mapping</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Unresolved item descriptions already present in source-backed voucher evidence. Loose fuzzy matches are never auto-confirmed.</p>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-100">
|
||||
{% for row in review_rows %}
|
||||
<article class="p-5">
|
||||
<div class="grid gap-4 lg:grid-cols-[1.2fr_1fr]">
|
||||
<div>
|
||||
<div class="flex flex-wrap gap-2"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold">{{ row.source_type|upper }}</span>{% if row.hsn %}<span class="rounded-full bg-blue-50 px-2 py-1 text-xs font-semibold text-blue-700">HSN {{ row.hsn }}</span>{% endif %}</div>
|
||||
<h3 class="mt-2 font-semibold">{{ row.description }}</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ row.party or 'No party-specific mapping' }}{% if row.source_unit %} · Source unit {{ row.source_unit }}{% endif %}</p>
|
||||
<p class="mt-2 text-xs text-amber-700">{{ row.reason }}{% if row.confidence %} · best candidate {{ row.confidence }}%{% endif %}</p>
|
||||
{% if row.candidates %}
|
||||
<div class="mt-3 rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs">
|
||||
<div class="font-semibold">Suggested candidates</div>
|
||||
{% for candidate in row.candidates[:5] %}<div class="mt-1">{{ candidate.confidence }}% · {{ candidate.name }} · {{ candidate.group or '-' }} · HSN {{ candidate.hsn or '-' }} · {{ candidate.unit or '-' }}</div>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<form method="post" action="/tools/accounting/stock-items/mapping" 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="tally_guid" value="{{ selected_tally_guid }}">
|
||||
<input type="hidden" name="source_type" value="{{ row.source_type }}">
|
||||
<input type="hidden" name="party_key" value="{{ row.party }}">
|
||||
<input type="hidden" name="source_description" value="{{ row.description }}">
|
||||
<input type="hidden" name="hsn_sac" value="{{ row.hsn }}">
|
||||
<label class="text-xs font-semibold">Map to Tally Stock Item
|
||||
<select name="stock_item_id" required class="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2">
|
||||
<option value="">Choose existing Tally stock item</option>
|
||||
{% for candidate in row.candidates %}<option value="{{ candidate.id }}">{{ candidate.name }} · {{ candidate.group or '-' }} · HSN {{ candidate.hsn or '-' }}</option>{% endfor %}
|
||||
<optgroup label="All synchronized items">
|
||||
{% for item in all_item_options %}<option value="{{ item.id }}">{{ item.name }} · {{ item.parent_group_name or '-' }} · HSN {{ item.hsn_code or '-' }}</option>{% endfor %}
|
||||
</optgroup>
|
||||
</select>
|
||||
</label>
|
||||
<label class="mt-3 block text-xs font-semibold">Mapping Note
|
||||
<input name="note" class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2" placeholder="Optional reviewer note">
|
||||
</label>
|
||||
<button class="mt-3 rounded-lg bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Confirm Mapping</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<div class="p-10 text-center text-slate-500">No unresolved stock-item descriptions found in current voucher source evidence.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% elif view=='mappings' %}
|
||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold">Add / Update Mapping Manually</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Use this when a source item has not yet generated a review row, or when preparing mappings before E-Way Bill/E-Invoice migration.</p>
|
||||
<form method="post" action="/tools/accounting/stock-items/mapping" class="mt-4 grid gap-3 md:grid-cols-2 lg:grid-cols-4">
|
||||
<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 }}">
|
||||
<input name="source_type" value="generic" placeholder="Source type" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="party_key" placeholder="Supplier/customer (optional)" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="source_description" required placeholder="Source item description" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
|
||||
<input name="hsn_sac" placeholder="HSN/SAC (optional)" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
|
||||
<select name="stock_item_id" required class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm lg:col-span-2"><option value="">Mapped Tally Stock Item</option>{% for item in all_item_options %}<option value="{{ item.id }}">{{ item.name }} · {{ item.parent_group_name or '-' }} · HSN {{ item.hsn_code or '-' }}</option>{% endfor %}</select>
|
||||
<input name="note" placeholder="Mapping note" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
|
||||
<button class="rounded-lg bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Save Mapping</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="rounded-2xl bg-white shadow-soft">
|
||||
<div class="border-b border-slate-200 p-5"><h2 class="font-semibold">Learned Stock Item Mappings</h2><p class="mt-1 text-sm text-slate-500">Confirmed mappings are client/company/source scoped and take precedence over name similarity.</p></div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead><tr class="bg-slate-50 text-left text-xs uppercase text-slate-500"><th class="px-4 py-3">Source</th><th class="px-4 py-3">Party</th><th class="px-4 py-3">Source Description</th><th class="px-4 py-3">HSN</th><th class="px-4 py-3">Mapped Tally Item</th><th class="px-4 py-3">Origin</th></tr></thead>
|
||||
<tbody>
|
||||
{% for mapping,item in mappings %}
|
||||
<tr class="border-t border-slate-100"><td class="px-4 py-3">{{ mapping.source_type|upper }}</td><td class="px-4 py-3">{{ mapping.party_key or 'Generic' }}</td><td class="px-4 py-3 font-semibold">{{ mapping.source_description }}</td><td class="px-4 py-3">{{ mapping.hsn_sac or '-' }}</td><td class="px-4 py-3">{{ item.name }}<div class="text-xs text-slate-500">{{ item.parent_group_name }} · {{ item.base_units }}</div></td><td class="px-4 py-3">{{ mapping.mapping_origin|title }} · {{ mapping.confidence }}%</td></tr>
|
||||
{% else %}<tr><td colspan="6" class="px-4 py-10 text-center text-slate-500">No learned stock-item mappings yet.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% elif view=='units' %}
|
||||
<section class="grid gap-5 lg:grid-cols-2">
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold">Tally Units</h2>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<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">Unit</th><th class="px-3 py-2">Original</th><th class="px-3 py-2">Conversion</th></tr></thead><tbody>{% for unit in units %}<tr class="border-t"><td class="px-3 py-2 font-semibold">{{ unit.name }}</td><td class="px-3 py-2">{{ unit.original_name or '-' }}</td><td class="px-3 py-2">{{ unit.conversion or '-' }}{% if unit.base_units or unit.additional_units %}<div class="text-xs text-slate-500">{{ unit.base_units }} → {{ unit.additional_units }}</div>{% endif %}</td></tr>{% else %}<tr><td colspan="3" class="p-6 text-center text-slate-500">No units synchronized.</td></tr>{% endfor %}</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold">Add / Update Source Unit Mapping</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Quantity factor means: source quantity × factor = Tally quantity. Example: 1 MT → 1000 Kg uses factor 1000.</p>
|
||||
<form method="post" action="/tools/accounting/stock-items/unit-mapping" class="mt-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="tally_guid" value="{{ selected_tally_guid }}">
|
||||
<input name="source_unit" required placeholder="Source unit, e.g. MT" class="w-full rounded-lg border border-slate-300 px-3 py-2">
|
||||
<select name="target_unit_id" required class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2"><option value="">Target Tally unit</option>{% for unit in units %}<option value="{{ unit.id }}">{{ unit.name }}</option>{% endfor %}</select>
|
||||
<input type="number" step="0.000001" min="0.000001" name="quantity_factor" value="1" required class="w-full rounded-lg border border-slate-300 px-3 py-2">
|
||||
<input name="note" placeholder="Optional note" class="w-full rounded-lg border border-slate-300 px-3 py-2">
|
||||
<button class="rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Save Unit Mapping</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold">Saved Unit Mappings</h2>
|
||||
<div class="mt-3 space-y-2">{% for mapping,unit in unit_mappings %}<div class="rounded-lg border border-slate-200 p-3 text-sm"><span class="font-semibold">{{ mapping.source_unit }}</span> × {{ mapping.quantity_factor }} → <span class="font-semibold">{{ unit.name }}</span>{% if mapping.note %}<div class="text-xs text-slate-500">{{ mapping.note }}</div>{% endif %}</div>{% else %}<div class="text-sm text-slate-500">No explicit unit mappings yet. Exact normalized units still resolve automatically.</div>{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -20,6 +20,7 @@
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/tools/accounting/chart-of-accounts{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl bg-blue-700 px-3 py-2 text-sm font-semibold text-white">Chart of Accounts - Groups & Ledgers</a>
|
||||
<a href="/tools/accounting/stock-items{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-indigo-300 bg-white px-3 py-2 text-sm font-semibold text-indigo-800">Stock Item Intelligence</a>
|
||||
<a href="/tools/accounting/voucher-engine{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-blue-300 bg-white px-3 py-2 text-sm font-semibold text-blue-800">Accounting Voucher Engine</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user