Add Phase 16 Chart of Accounts and ledger group intelligence
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
{% 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 Foundation · Phase 16</p>
|
||||
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Chart of Accounts · Groups & Ledgers</h1>
|
||||
<p class="mt-1 max-w-5xl text-sm text-slate-500">
|
||||
Read the complete mapped Tally Group and Ledger master first, resolve every ledger through its parent-group hierarchy, and maintain a client-specific accounting role before using voucher history for Purchase/Sales classification.
|
||||
</p>
|
||||
</div>
|
||||
<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 Tally Accounting</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="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">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="">All synchronized companies</option>
|
||||
{% for m in available_mappings %}
|
||||
<option value="{{ m.guid }}" {% if selected_tally_guid==m.guid %}selected{% endif %}>
|
||||
{{ m.name }}{% if m.currently_loaded %} · Open now{% endif %}
|
||||
</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 Chart</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if selected_client %}
|
||||
<section class="rounded-2xl border border-blue-200 bg-blue-50/40 p-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="font-semibold text-slate-900">Refresh from Tally Group & Ledger Master</h2>
|
||||
<p class="mt-1 text-sm text-slate-600">
|
||||
This is read-only against Tally. It refreshes the existing local .act master snapshot and copies Groups/Ledgers into the ERP Chart of Accounts for classification and review.
|
||||
</p>
|
||||
{% if last_sync %}
|
||||
<p class="mt-2 text-xs text-slate-500">Last ERP Chart sync: {{ last_sync.created_at_utc }} · {{ last_sync.group_count }} groups · {{ last_sync.ledger_count }} ledgers.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post" action="/tools/accounting/chart-of-accounts/sync" class="flex flex-wrap items-end gap-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">Currently open mapped company
|
||||
<select name="tally_guid" required class="mt-1 min-w-72 rounded-lg border border-slate-300 bg-white px-3 py-2">
|
||||
<option value="">Select company</option>
|
||||
{% for m in available_mappings %}
|
||||
{% if m.currently_loaded %}
|
||||
<option value="{{ m.guid }}" {% if selected_tally_guid==m.guid %}selected{% endif %}>{{ m.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button {% if not node_online %}disabled{% endif %} class="rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50">Refresh Chart of Accounts</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if not node_online %}
|
||||
<div class="mt-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">ERP Local Agent is not currently online for this branch.</div>
|
||||
{% elif not available_mappings %}
|
||||
<div class="mt-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">Map this ERP client to a Tally company first from the Tally Accounting page.</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||
{% for label,value in [
|
||||
('Groups',summary.groups),
|
||||
('Ledgers',summary.ledgers),
|
||||
('Classified',summary.classified),
|
||||
('Review Required',summary.review),
|
||||
('Manual Overrides',summary.manual)
|
||||
] %}
|
||||
<div class="rounded-2xl bg-white p-4 shadow-soft">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">{{ label }}</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-slate-900">{{ value }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl bg-white p-4 shadow-soft">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="?client_id={{ selected_client.id }}{% if selected_tally_guid %}&tally_guid={{ selected_tally_guid }}{% endif %}&view=ledgers"
|
||||
class="rounded-lg px-4 py-2 text-sm font-semibold {% if view=='ledgers' %}bg-slate-900 text-white{% else %}border border-slate-300 bg-white{% endif %}">Ledger Master</a>
|
||||
<a href="?client_id={{ selected_client.id }}{% if selected_tally_guid %}&tally_guid={{ selected_tally_guid }}{% endif %}&view=groups"
|
||||
class="rounded-lg px-4 py-2 text-sm font-semibold {% if view=='groups' %}bg-slate-900 text-white{% else %}border border-slate-300 bg-white{% endif %}">Group Master</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if view == 'ledgers' %}
|
||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<form method="get" class="grid gap-3 lg:grid-cols-5">
|
||||
<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="ledgers">
|
||||
|
||||
<label class="text-xs font-semibold lg:col-span-2">Search Ledger / Group / GSTIN
|
||||
<input name="q" value="{{ q }}" class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2">
|
||||
</label>
|
||||
|
||||
<label class="text-xs font-semibold">Accounting Role
|
||||
<select name="role" class="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2">
|
||||
<option value="">All roles</option>
|
||||
{% for code,label in role_options %}
|
||||
<option value="{{ code }}" {% if role_filter==code %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="text-xs font-semibold">Root Group
|
||||
<select name="root_group" class="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2">
|
||||
<option value="">All root groups</option>
|
||||
{% for root in root_groups %}
|
||||
<option value="{{ root }}" {% if root_group_filter==root %}selected{% endif %}>{{ root }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="flex items-end"><button class="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm font-semibold">Filter</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||
<div class="border-b border-slate-200 px-5 py-4">
|
||||
<h2 class="font-semibold text-slate-900">Ledger Master</h2>
|
||||
<p class="text-sm text-slate-500">{{ total }} ledger(s). Manual role overrides survive future Chart refreshes.</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
||||
<th class="px-3 py-3">Ledger</th>
|
||||
<th class="px-3 py-3">Parent / Root Group</th>
|
||||
<th class="px-3 py-3">Balances</th>
|
||||
<th class="px-3 py-3">GST / Tax</th>
|
||||
<th class="px-3 py-3">Accounting Role</th>
|
||||
<th class="px-3 py-3">Review / Override</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in ledgers %}
|
||||
{% set final_role = effective_role(row) %}
|
||||
<tr class="border-b border-slate-100 align-top">
|
||||
<td class="px-3 py-3">
|
||||
<div class="font-semibold text-slate-900">{{ row.name }}</div>
|
||||
{% if row.master_guid %}<div class="mt-1 max-w-56 truncate font-mono text-[11px] text-slate-400">{{ row.master_guid }}</div>{% endif %}
|
||||
{% if row.party_gstin %}<div class="mt-1 font-mono text-xs text-slate-500">{{ row.party_gstin }}</div>{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-3">
|
||||
<div>{{ row.parent_group_name or '—' }}</div>
|
||||
<div class="mt-1 text-xs font-semibold text-slate-500">Root: {{ row.root_group_name or '—' }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap">
|
||||
<div>Opening: ₹{{ '%.2f'|format(row.opening_balance) }}</div>
|
||||
<div>Closing: ₹{{ '%.2f'|format(row.closing_balance) }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-3">
|
||||
<div>{{ row.tax_type or '—' }}</div>
|
||||
<div class="text-xs text-slate-500">{{ row.gst_applicable or row.gst_registration_type or '—' }}</div>
|
||||
{% if row.hsn_code %}<div class="text-xs text-slate-500">HSN/SAC {{ row.hsn_code }}</div>{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-3">
|
||||
<div class="font-semibold">{{ role_labels.get(final_role, final_role) }}</div>
|
||||
{% if row.manual_role_code %}
|
||||
<span class="mt-1 inline-block rounded-full bg-emerald-50 px-2 py-1 text-xs font-semibold text-emerald-700">Manual</span>
|
||||
{% else %}
|
||||
<div class="mt-1 text-xs text-slate-500">{{ row.suggested_confidence }}% · Automatic</div>
|
||||
<div class="mt-1 max-w-72 text-xs text-slate-500">{{ row.suggestion_reason }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-3">
|
||||
<form method="post" action="/tools/accounting/chart-of-accounts/ledger/{{ row.id }}/role" class="min-w-64 space-y-2">
|
||||
<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="q" value="{{ q }}">
|
||||
<input type="hidden" name="role_filter" value="{{ role_filter }}">
|
||||
<input type="hidden" name="root_group" value="{{ root_group_filter }}">
|
||||
<input type="hidden" name="page" value="{{ page }}">
|
||||
<input type="hidden" name="per_page" value="{{ per_page }}">
|
||||
|
||||
<select name="role_code" class="w-full rounded-lg border border-slate-300 bg-white px-2 py-2 text-xs">
|
||||
<option value="">Use automatic suggestion</option>
|
||||
{% for code,label in role_options %}
|
||||
<option value="{{ code }}" {% if row.manual_role_code==code %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input name="note" value="{{ row.manual_note or '' }}" placeholder="Optional reviewer note" class="w-full rounded-lg border border-slate-300 px-2 py-2 text-xs">
|
||||
<button class="w-full rounded-lg border border-slate-300 px-3 py-2 text-xs font-semibold">Save Role</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="px-4 py-12 text-center text-slate-500">No Chart-of-Accounts ledgers have been synchronized for this selection 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">
|
||||
<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 }}&tally_guid={{ selected_tally_guid }}&view=ledgers&q={{ q }}&role={{ role_filter }}&root_group={{ root_group_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 }}&tally_guid={{ selected_tally_guid }}&view=ledgers&q={{ q }}&role={{ role_filter }}&root_group={{ root_group_filter }}&page={{ page+1 }}&per_page={{ per_page }}">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||
<div class="border-b border-slate-200 px-5 py-4">
|
||||
<h2 class="font-semibold text-slate-900">Tally Group Master</h2>
|
||||
<p class="text-sm text-slate-500">Every Group is resolved to its primary/root Tally Group. This hierarchy drives deterministic ledger-role classification.</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
||||
<th class="px-3 py-3">Group</th>
|
||||
<th class="px-3 py-3">Parent</th>
|
||||
<th class="px-3 py-3">Root Group</th>
|
||||
<th class="px-3 py-3">Depth</th>
|
||||
<th class="px-3 py-3">Tally Flags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in groups %}
|
||||
<tr class="border-b border-slate-100">
|
||||
<td class="px-3 py-3 font-semibold">{{ row.name }}</td>
|
||||
<td class="px-3 py-3">{{ row.parent_name or 'Primary' }}</td>
|
||||
<td class="px-3 py-3">{{ row.root_group_name or row.name }}</td>
|
||||
<td class="px-3 py-3">{{ row.depth }}</td>
|
||||
<td class="px-3 py-3 text-xs text-slate-500">
|
||||
{% if row.reserved_name %}Reserved: {{ row.reserved_name }}{% endif %}
|
||||
{% if row.is_revenue %}<div>Revenue: {{ row.is_revenue }}</div>{% endif %}
|
||||
{% if row.is_deemed_positive %}<div>Deemed Positive: {{ row.is_deemed_positive }}</div>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="px-4 py-12 text-center text-slate-500">No Group master synchronized yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -13,6 +13,17 @@
|
||||
<p class="mt-1 text-sm text-slate-500">Company mapping, master sync and transaction sync remain available. Accounting tools are now grouped into Purchases & Expenses and Sales & Income without changing existing routes or controls.</p>
|
||||
</div>
|
||||
<div class="w-full space-y-3 xl:max-w-5xl">
|
||||
<div class="rounded-2xl border border-blue-200 bg-blue-50/40 p-4">
|
||||
<div class="mb-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-[0.14em] text-blue-700">Accounting Foundation</div>
|
||||
<div class="text-xs text-slate-500">Build the client Chart of Accounts from Tally Groups and Ledgers before transaction-level classification.</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<div class="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user