Refine opening balance review with summary and exception workflow

This commit is contained in:
A R R R Associates
2026-08-25 22:28:52 +05:30
parent 6c9a97c1b3
commit 39ab5ad51f
2 changed files with 358 additions and 55 deletions
+116 -10
View File
@@ -70,13 +70,48 @@ def _company_rows(tally):
return rows
def _summary_counts(raw):
raw = raw or {}
ledgers = raw.get("ledgers") or {}
stock = raw.get("stock_items") or {}
return {
"ledgers": {
"matched": int(ledgers.get("matched") or 0),
"difference": int(ledgers.get("difference") or 0),
"missing_in_current_year": int(ledgers.get("missing_in_current_year") or 0),
"new_in_current_year": int(ledgers.get("new_in_current_year") or 0),
},
"stock_items": {
"matched": int(stock.get("matched") or 0),
"quantity_difference": int(stock.get("quantity_difference") or 0),
"value_difference": int(stock.get("value_difference") or 0),
"quantity_and_value_difference": int(stock.get("quantity_and_value_difference") or 0),
"unit_difference": int(stock.get("unit_difference") or 0),
"missing_in_current_year": int(stock.get("missing_in_current_year") or 0),
"new_in_current_year": int(stock.get("new_in_current_year") or 0),
},
}
def _page_rows(rows, page, per_page=50):
per_page = max(20, min(200, int(per_page or 50)))
total = len(rows)
pages = max(1, (total + per_page - 1) // per_page)
page = max(1, min(int(page or 1), pages))
start = (page - 1) * per_page
return rows[start:start + per_page], total, page, pages
@router.get("")
def page(
request: Request,
client_id: int | None = None,
run_id: int | None = None,
tab: str = "ledgers",
tab: str = "summary",
status: str = "",
page: int = 1,
per_page: int = 50,
show_zero_new: int = 0,
message: str = "",
error: str = "",
):
@@ -118,7 +153,11 @@ def page(
selected_run = None
ledgers = []
stocks = []
summary = {}
ledger_action_rows = []
stock_action_rows = []
summary = _summary_counts({})
detail_total = 0
detail_pages = 1
if selected:
runs = list_runs(
@@ -134,13 +173,73 @@ def page(
if selected_run:
try:
summary = json.loads(selected_run.summary_json or "{}")
summary = _summary_counts(json.loads(selected_run.summary_json or "{}"))
except Exception:
summary = {}
if tab == "stock":
stocks = stock_items(db, run_id=selected_run.id, status=status)
summary = _summary_counts({})
if tab == "summary":
ledger_action_rows = ledger_items(
db,
run_id=selected_run.id,
status="difference",
)
stock_action_rows = []
for action_status in (
"quantity_difference",
"value_difference",
"quantity_and_value_difference",
"unit_difference",
):
stock_action_rows.extend(
stock_items(
db,
run_id=selected_run.id,
status=action_status,
)
)
ledger_action_rows = ledger_action_rows[:100]
stock_action_rows = stock_action_rows[:100]
elif tab == "stock":
detail_status = status or "quantity_and_value_difference"
rows = stock_items(
db,
run_id=selected_run.id,
status=detail_status,
)
if detail_status == "new_in_current_year" and not show_zero_new:
rows = [
row for row in rows
if abs(float(row.current_opening_qty or 0)) > 0.000001
or abs(float(row.current_opening_value or 0)) > 0.005
]
stocks, detail_total, page, detail_pages = _page_rows(
rows,
page,
per_page,
)
status = detail_status
else:
ledgers = ledger_items(db, run_id=selected_run.id, status=status)
detail_status = status or "difference"
rows = ledger_items(
db,
run_id=selected_run.id,
status=detail_status,
)
if detail_status == "new_in_current_year" and not show_zero_new:
rows = [
row for row in rows
if abs(float(row.current_opening_balance or 0)) > 0.005
]
ledgers, detail_total, page, detail_pages = _page_rows(
rows,
page,
per_page,
)
status = detail_status
valid_tab = tab if tab in {"summary", "ledgers", "stock"} else "summary"
return templates.TemplateResponse(
"modules/accounting/templates/accounting/opening_balances.html",
@@ -160,9 +259,16 @@ def page(
"selected_run": selected_run,
"ledgers": ledgers,
"stocks": stocks,
"ledger_action_rows": ledger_action_rows,
"stock_action_rows": stock_action_rows,
"summary": summary,
"tab": tab if tab in {"ledgers", "stock"} else "ledgers",
"tab": valid_tab,
"status_filter": status,
"page": page,
"pages": detail_pages,
"per_page": per_page,
"detail_total": detail_total,
"show_zero_new": bool(show_zero_new),
"message": message,
"error": command_error,
},
@@ -226,7 +332,7 @@ def compare(
return _go(
client.id,
run_id=run.id,
tab="ledgers",
tab="summary",
message=(
f"Opening balance comparison #{run.id} completed: "
f"{run.ledger_count} ledger row(s), {run.stock_item_count} stock row(s)."
@@ -368,7 +474,7 @@ async def apply_corrections(
return _go(
client.id,
run_id=run.id,
tab="ledgers",
tab="summary",
message=f"Tally correction completed: {verified} verified, {failed} failed/blocked.",
)
except Exception as exc:
@@ -3,10 +3,10 @@
<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 21A</p>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Accounting · Phase 21A v2</p>
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Opening Balance Review & Correction</h1>
<p class="mt-1 max-w-5xl text-sm text-slate-500">
Compare previous-year Tally closing balances with current-year opening balances for Ledgers and Stock Items. Safe selected differences can be applied directly to current-year Tally masters and are re-read for verification.
Summary-first review of previous-year closing balances against current-year openings. Matched and new zero-balance masters stay out of the main review; actionable differences are shown immediately and retain the existing controlled Tally correction workflow.
</p>
</div>
<div class="flex flex-wrap gap-2">
@@ -50,7 +50,6 @@
</label>
<div class="flex items-end"><button class="rounded-xl bg-slate-900 px-5 py-2 text-sm font-semibold text-white" {% if not node_online %}disabled{% endif %}>Compare Opening Balances</button></div>
</form>
<p class="mt-2 text-xs text-slate-500">Phase 21A v1 intentionally requires separate previous-year and current-year Tally companies. This avoids treating a current company's latest closing balance as the prior-year audited closing.</p>
{% endif %}
</section>
@@ -68,7 +67,7 @@
<td class="px-4 py-3">{{ run.current_company_name }}</td>
<td class="px-4 py-3">{{ run.ledger_count }}</td>
<td class="px-4 py-3">{{ run.stock_item_count }}</td>
<td class="px-4 py-3 text-right"><a class="font-semibold text-brand-700 hover:underline" href="?client_id={{ selected_client.id }}&run_id={{ run.id }}">Open</a></td>
<td class="px-4 py-3 text-right"><a class="font-semibold text-brand-700 hover:underline" href="?client_id={{ selected_client.id }}&run_id={{ run.id }}&tab=summary">Open Summary</a></td>
</tr>
{% endfor %}
</tbody>
@@ -78,29 +77,177 @@
{% endif %}
{% if selected_run %}
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
<strong>Direct Tally correction control:</strong> only matched current-year masters with a genuine opening difference are eligible. Missing/new masters, ambiguous matches and stock unit differences are never changed automatically. Every applied row is re-read from Tally and marked Verified or Failed.
<section class="rounded-2xl bg-white p-5 shadow-soft">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">Run #{{ selected_run.id }}</p>
<h2 class="mt-1 text-lg font-semibold">{{ selected_run.previous_company_name }} closing → {{ selected_run.current_company_name }} opening</h2>
</div>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=summary" class="rounded-xl {% if tab=='summary' %}bg-brand-600 text-white{% else %}border border-slate-300 bg-white{% endif %} px-4 py-2 text-sm font-semibold">Summary</a>
</div>
</section>
{% if tab=='summary' %}
<section class="space-y-4">
<div>
<h2 class="text-lg font-semibold text-slate-900">Ledger Opening Summary</h2>
<p class="text-sm text-slate-500">Matched records are summarized only. Click a category when you need its detailed report.</p>
</div>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status=matched" class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div class="text-xs font-semibold uppercase text-emerald-700">Matched</div><div class="mt-1 text-3xl font-bold text-emerald-900">{{ summary.ledgers.matched }}</div><div class="mt-2 text-xs text-emerald-700">View detailed report</div>
</a>
<a href="#ledger-actions" class="rounded-2xl border border-amber-300 bg-amber-50 p-4">
<div class="text-xs font-semibold uppercase text-amber-700">Differences</div><div class="mt-1 text-3xl font-bold text-amber-900">{{ summary.ledgers.difference }}</div><div class="mt-2 text-xs text-amber-700">Review & apply below</div>
</a>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status=missing_in_current_year" class="rounded-2xl border border-red-200 bg-red-50 p-4">
<div class="text-xs font-semibold uppercase text-red-700">Missing in Current Year</div><div class="mt-1 text-3xl font-bold text-red-900">{{ summary.ledgers.missing_in_current_year }}</div><div class="mt-2 text-xs text-red-700">Review details</div>
</a>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status=new_in_current_year" class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
<div class="text-xs font-semibold uppercase text-slate-600">New in Current Year</div><div class="mt-1 text-3xl font-bold text-slate-900">{{ summary.ledgers.new_in_current_year }}</div><div class="mt-2 text-xs text-slate-600">Zero-balance rows hidden by default</div>
</a>
</div>
<nav class="flex gap-2">
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers" class="rounded-xl px-4 py-2 text-sm font-semibold {% if tab=='ledgers' %}bg-brand-600 text-white{% else %}border border-slate-300 bg-white{% endif %}">Ledger Openings</a>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock" class="rounded-xl px-4 py-2 text-sm font-semibold {% if tab=='stock' %}bg-brand-600 text-white{% else %}border border-slate-300 bg-white{% endif %}">Stock Openings</a>
</nav>
<div class="pt-3">
<h2 class="text-lg font-semibold text-slate-900">Stock Opening Summary</h2>
<p class="text-sm text-slate-500">Quantity, value and unit exceptions are separated so the reviewer sees only the issue that needs attention.</p>
</div>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=matched" class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div class="text-xs font-semibold uppercase text-emerald-700">Matched</div><div class="mt-1 text-3xl font-bold text-emerald-900">{{ summary.stock_items.matched }}</div><div class="mt-2 text-xs text-emerald-700">View detailed report</div>
</a>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=quantity_difference" class="rounded-2xl border border-amber-200 bg-amber-50 p-4">
<div class="text-xs font-semibold uppercase text-amber-700">Quantity Difference</div><div class="mt-1 text-3xl font-bold text-amber-900">{{ summary.stock_items.quantity_difference }}</div><div class="mt-2 text-xs text-amber-700">Review details</div>
</a>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=value_difference" class="rounded-2xl border border-amber-200 bg-amber-50 p-4">
<div class="text-xs font-semibold uppercase text-amber-700">Value Difference</div><div class="mt-1 text-3xl font-bold text-amber-900">{{ summary.stock_items.value_difference }}</div><div class="mt-2 text-xs text-amber-700">Review details</div>
</a>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=quantity_and_value_difference" class="rounded-2xl border border-orange-300 bg-orange-50 p-4">
<div class="text-xs font-semibold uppercase text-orange-700">Qty + Value Difference</div><div class="mt-1 text-3xl font-bold text-orange-900">{{ summary.stock_items.quantity_and_value_difference }}</div><div class="mt-2 text-xs text-orange-700">Review & apply</div>
</a>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=unit_difference" class="rounded-2xl border border-red-200 bg-red-50 p-4">
<div class="text-xs font-semibold uppercase text-red-700">Unit Difference</div><div class="mt-1 text-3xl font-bold text-red-900">{{ summary.stock_items.unit_difference }}</div><div class="mt-2 text-xs text-red-700">Blocked from automatic correction</div>
</a>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=missing_in_current_year" class="rounded-2xl border border-red-200 bg-red-50 p-4">
<div class="text-xs font-semibold uppercase text-red-700">Missing in Current Year</div><div class="mt-1 text-3xl font-bold text-red-900">{{ summary.stock_items.missing_in_current_year }}</div><div class="mt-2 text-xs text-red-700">Review details</div>
</a>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=new_in_current_year" class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
<div class="text-xs font-semibold uppercase text-slate-600">New in Current Year</div><div class="mt-1 text-3xl font-bold text-slate-900">{{ summary.stock_items.new_in_current_year }}</div><div class="mt-2 text-xs text-slate-600">Zero-balance rows hidden by default</div>
</a>
</div>
</section>
<form method="post" action="/tools/accounting/opening-balances/apply" class="space-y-4">
<form method="post" action="/tools/accounting/opening-balances/apply" 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="run_id" value="{{ selected_run.id }}">
{% if tab=='ledgers' %}
<section class="rounded-2xl bg-white shadow-soft">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 p-5">
<div><h2 class="font-semibold">Ledger Opening Comparison</h2><p class="text-sm text-slate-500">{{ selected_run.previous_company_name }} closing → {{ selected_run.current_company_name }} opening</p></div>
<select onchange="location.href='?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status='+encodeURIComponent(this.value)" class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm">
<option value="">All statuses</option>
{% for value,label in [('matched','Matched'),('difference','Difference'),('missing_in_current_year','Missing in Current Year'),('new_in_current_year','New in Current Year')] %}<option value="{{ value }}" {% if status_filter==value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
<section id="ledger-actions" class="rounded-2xl bg-white shadow-soft">
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-slate-200 p-5">
<div>
<h2 class="font-semibold">Ledger Differences — Action Required</h2>
<p class="text-sm text-slate-500">Only ledgers with an actual previous-closing/current-opening difference are shown here.</p>
</div>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status=difference" class="text-sm font-semibold text-brand-700 hover:underline">Open detailed report</a>
</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-3 py-2">Apply</th><th class="px-3 py-2">Previous Ledger</th><th class="px-3 py-2 text-right">PY Closing</th><th class="px-3 py-2">Current Ledger</th><th class="px-3 py-2 text-right">CY Opening</th><th class="px-3 py-2 text-right">Difference</th><th class="px-3 py-2">Match</th><th class="px-3 py-2">Correction</th></tr></thead>
<tbody>
{% for item in ledger_action_rows %}
<tr class="border-t border-slate-100">
<td class="px-3 py-3"><input type="checkbox" name="ledger_ids" value="{{ item.id }}"></td>
<td class="px-3 py-3"><div class="font-semibold">{{ item.previous_name }}</div><div class="text-xs text-slate-500">{{ item.previous_group }}</div></td>
<td class="px-3 py-3 text-right">{{ '%.2f'|format(item.previous_closing_balance) }}</td>
<td class="px-3 py-3"><div class="font-semibold">{{ item.current_name }}</div><div class="text-xs text-slate-500">{{ item.current_group }}</div></td>
<td class="px-3 py-3 text-right">{{ '%.2f'|format(item.current_opening_balance) }}</td>
<td class="px-3 py-3 text-right font-semibold">{{ '%.2f'|format(0 if item.difference|abs < 0.005 else item.difference) }}</td>
<td class="px-3 py-3">{{ item.match_method|replace('_',' ')|title }} · {{ item.confidence }}%</td>
<td class="px-3 py-3">{{ item.correction_status|replace('_',' ')|title }}{% if item.correction_note %}<div class="max-w-xs text-xs text-slate-500">{{ item.correction_note }}</div>{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="8" class="p-8 text-center text-emerald-700">No Ledger opening differences require action.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if summary.ledgers.difference > ledger_action_rows|length %}<div class="border-t border-slate-200 p-3 text-xs text-slate-500">Showing the first {{ ledger_action_rows|length }} differences here. Open the detailed report to review the remaining rows.</div>{% endif %}
</section>
<section class="rounded-2xl bg-white shadow-soft">
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-slate-200 p-5">
<div>
<h2 class="font-semibold">Stock Opening Differences — Action Required</h2>
<p class="text-sm text-slate-500">Quantity/value differences can be selected for correction. Unit differences are shown but remain blocked.</p>
</div>
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=quantity_and_value_difference" class="text-sm font-semibold text-brand-700 hover:underline">Open stock details</a>
</div>
<div class="overflow-x-auto">
<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">Apply</th><th class="px-3 py-2">Previous Stock Item</th><th class="px-3 py-2">PY Closing</th><th class="px-3 py-2">Current Stock Item</th><th class="px-3 py-2">CY Opening</th><th class="px-3 py-2">Difference</th><th class="px-3 py-2">Status</th><th class="px-3 py-2">Correction</th></tr></thead>
<tbody>
{% for item in stock_action_rows %}
<tr class="border-t border-slate-100 align-top">
<td class="px-3 py-3">{% if item.match_status != 'unit_difference' and item.previous_unit|lower == item.current_unit|lower %}<input type="checkbox" name="stock_ids" value="{{ item.id }}">{% else %}<span class="text-red-600">Blocked</span>{% endif %}</td>
<td class="px-3 py-3"><div class="font-semibold">{{ item.previous_name }}</div><div class="text-slate-500">{{ item.previous_group }} · HSN {{ item.previous_hsn or '-' }}</div></td>
<td class="px-3 py-3"><div>{{ item.previous_closing_qty }} {{ item.previous_unit }}</div><div>₹{{ '%.2f'|format(item.previous_closing_value) }}</div></td>
<td class="px-3 py-3"><div class="font-semibold">{{ item.current_name }}</div><div class="text-slate-500">{{ item.current_group }} · HSN {{ item.current_hsn or '-' }}</div></td>
<td class="px-3 py-3"><div>{{ item.current_opening_qty }} {{ item.current_unit }}</div><div>₹{{ '%.2f'|format(item.current_opening_value) }}</div></td>
<td class="px-3 py-3"><div>Qty {{ 0 if item.quantity_difference|abs < 0.000001 else item.quantity_difference }}</div><div>Value ₹{{ '%.2f'|format(0 if item.value_difference|abs < 0.005 else item.value_difference) }}</div></td>
<td class="px-3 py-3">{{ item.match_status|replace('_',' ')|title }}<div class="text-slate-500">{{ item.match_method|replace('_',' ')|title }} · {{ item.confidence }}%</div></td>
<td class="px-3 py-3">{{ item.correction_status|replace('_',' ')|title }}{% if item.correction_note %}<div class="max-w-xs text-slate-500">{{ item.correction_note }}</div>{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="8" class="p-8 text-center text-emerald-700">No Stock Item opening differences require action.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% if ledger_action_rows or stock_action_rows %}
<section class="rounded-2xl border border-red-200 bg-red-50 p-5">
<h3 class="font-semibold text-red-900">Apply selected safe corrections directly to current-year Tally</h3>
<p class="mt-1 text-sm text-red-800">Existing safeguards are unchanged: current openings are rechecked immediately before write and every altered master is re-read from Tally afterward.</p>
<div class="mt-3 flex flex-wrap gap-3">
<input name="confirm_text" required placeholder="APPLY OPENING BALANCES" class="min-w-[280px] rounded-lg border border-red-300 bg-white px-3 py-2 text-sm">
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected Changes</button>
</div>
</section>
{% endif %}
</form>
{% elif tab=='ledgers' %}
<section class="rounded-2xl bg-white shadow-soft">
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-slate-200 p-5">
<div>
<h2 class="font-semibold">Ledger Detailed Report</h2>
<p class="text-sm text-slate-500">{{ status_filter|replace('_',' ')|title }} · {{ detail_total }} row(s). This detailed list is opened only on demand.</p>
</div>
<div class="flex flex-wrap gap-2">
{% for value,label in [('matched','Matched'),('difference','Differences'),('missing_in_current_year','Missing'),('new_in_current_year','New')] %}
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status={{ value }}" class="rounded-lg {% if status_filter==value %}bg-brand-600 text-white{% else %}border border-slate-300 bg-white{% endif %} px-3 py-2 text-xs font-semibold">{{ label }}</a>
{% endfor %}
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=summary" class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-semibold">Back to Summary</a>
</div>
</div>
{% if status_filter=='new_in_current_year' %}
<div class="border-b border-slate-200 px-5 py-3 text-xs text-slate-600">
{% if show_zero_new %}
Zero-balance new Ledgers are currently visible.
<a class="font-semibold text-brand-700" href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status=new_in_current_year">Hide zero-balance new Ledgers</a>
{% else %}
Zero-balance new Ledgers are hidden by default.
<a class="font-semibold text-brand-700" href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status=new_in_current_year&show_zero_new=1">Show them</a>
{% endif %}
</div>
{% endif %}
<form method="post" action="/tools/accounting/opening-balances/apply">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
<input type="hidden" name="run_id" value="{{ selected_run.id }}">
<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-3 py-2">Apply</th><th class="px-3 py-2">Previous Ledger</th><th class="px-3 py-2 text-right">PY Closing</th><th class="px-3 py-2">Current Ledger</th><th class="px-3 py-2 text-right">CY Opening</th><th class="px-3 py-2 text-right">Difference</th><th class="px-3 py-2">Status</th><th class="px-3 py-2">Correction</th></tr></thead>
@@ -112,25 +259,66 @@
<td class="px-3 py-3 text-right">{{ '%.2f'|format(item.previous_closing_balance) }}</td>
<td class="px-3 py-3"><div class="font-semibold">{{ item.current_name or '—' }}</div><div class="text-xs text-slate-500">{{ item.current_group }}</div></td>
<td class="px-3 py-3 text-right">{{ '%.2f'|format(item.current_opening_balance) }}</td>
<td class="px-3 py-3 text-right font-semibold">{{ '%.2f'|format(item.difference) }}</td>
<td class="px-3 py-3 text-right font-semibold">{{ '%.2f'|format(0 if item.difference|abs < 0.005 else item.difference) }}</td>
<td class="px-3 py-3">{{ item.match_status|replace('_',' ')|title }}{% if item.match_method %}<div class="text-xs text-slate-500">{{ item.match_method|replace('_',' ')|title }} · {{ item.confidence }}%</div>{% endif %}</td>
<td class="px-3 py-3">{{ item.correction_status|replace('_',' ')|title }}{% if item.correction_note %}<div class="max-w-xs text-xs text-slate-500">{{ item.correction_note }}</div>{% endif %}</td>
</tr>
{% else %}<tr><td colspan="8" class="p-8 text-center text-slate-500">No ledger rows for this filter.</td></tr>{% endfor %}
{% else %}<tr><td colspan="8" class="p-8 text-center text-slate-500">No Ledger rows for this category.</td></tr>{% endfor %}
</tbody>
</table>
</div>
{% if status_filter=='difference' and ledgers %}
<div class="border-t border-slate-200 p-4">
<div class="flex flex-wrap gap-3">
<input name="confirm_text" required placeholder="APPLY OPENING BALANCES" class="min-w-[280px] rounded-lg border border-red-300 px-3 py-2 text-sm">
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected Changes</button>
</div>
</div>
{% endif %}
</form>
{% 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-2" href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status={{ status_filter }}&page={{ page-1 }}{% if show_zero_new %}&show_zero_new=1{% endif %}">Previous</a>{% endif %}
{% if page<pages %}<a class="rounded-lg border border-slate-300 px-3 py-2" href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=ledgers&status={{ status_filter }}&page={{ page+1 }}{% if show_zero_new %}&show_zero_new=1{% endif %}">Next</a>{% endif %}
</div>
</div>
{% endif %}
</section>
{% else %}
{% elif tab=='stock' %}
<section class="rounded-2xl bg-white shadow-soft">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 p-5">
<div><h2 class="font-semibold">Stock Item Opening Comparison</h2><p class="text-sm text-slate-500">Quantity, unit and value are checked separately.</p></div>
<select onchange="location.href='?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status='+encodeURIComponent(this.value)" class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm">
<option value="">All statuses</option>
{% for value,label in [('matched','Matched'),('quantity_difference','Quantity Difference'),('value_difference','Value Difference'),('quantity_and_value_difference','Quantity + Value Difference'),('unit_difference','Unit Difference'),('missing_in_current_year','Missing in Current Year'),('new_in_current_year','New in Current Year')] %}<option value="{{ value }}" {% if status_filter==value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-slate-200 p-5">
<div>
<h2 class="font-semibold">Stock Item Detailed Report</h2>
<p class="text-sm text-slate-500">{{ status_filter|replace('_',' ')|title }} · {{ detail_total }} row(s).</p>
</div>
<div class="flex flex-wrap gap-2">
{% for value,label in [('matched','Matched'),('quantity_difference','Qty Difference'),('value_difference','Value Difference'),('quantity_and_value_difference','Qty + Value'),('unit_difference','Unit Difference'),('missing_in_current_year','Missing'),('new_in_current_year','New')] %}
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status={{ value }}" class="rounded-lg {% if status_filter==value %}bg-brand-600 text-white{% else %}border border-slate-300 bg-white{% endif %} px-3 py-2 text-xs font-semibold">{{ label }}</a>
{% endfor %}
<a href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=summary" class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-semibold">Back to Summary</a>
</div>
</div>
{% if status_filter=='new_in_current_year' %}
<div class="border-b border-slate-200 px-5 py-3 text-xs text-slate-600">
{% if show_zero_new %}
Zero-balance new Stock Items are currently visible.
<a class="font-semibold text-brand-700" href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=new_in_current_year">Hide zero-balance new Stock Items</a>
{% else %}
Zero-balance new Stock Items are hidden by default.
<a class="font-semibold text-brand-700" href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status=new_in_current_year&show_zero_new=1">Show them</a>
{% endif %}
</div>
{% endif %}
<form method="post" action="/tools/accounting/opening-balances/apply">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
<input type="hidden" name="run_id" value="{{ selected_run.id }}">
<div class="overflow-x-auto">
<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">Apply</th><th class="px-3 py-2">Previous Stock Item</th><th class="px-3 py-2">PY Closing</th><th class="px-3 py-2">Current Stock Item</th><th class="px-3 py-2">CY Opening</th><th class="px-3 py-2">Difference</th><th class="px-3 py-2">Status</th><th class="px-3 py-2">Correction</th></tr></thead>
@@ -142,26 +330,35 @@
<td class="px-3 py-3"><div>{{ item.previous_closing_qty }} {{ item.previous_unit }}</div><div>₹{{ '%.2f'|format(item.previous_closing_value) }}</div></td>
<td class="px-3 py-3"><div class="font-semibold">{{ item.current_name or '—' }}</div><div class="text-slate-500">{{ item.current_group }} · HSN {{ item.current_hsn or '-' }}</div></td>
<td class="px-3 py-3"><div>{{ item.current_opening_qty }} {{ item.current_unit }}</div><div>₹{{ '%.2f'|format(item.current_opening_value) }}</div></td>
<td class="px-3 py-3"><div>Qty {{ item.quantity_difference }}</div><div>Value ₹{{ '%.2f'|format(item.value_difference) }}</div></td>
<td class="px-3 py-3"><div>Qty {{ 0 if item.quantity_difference|abs < 0.000001 else item.quantity_difference }}</div><div>Value ₹{{ '%.2f'|format(0 if item.value_difference|abs < 0.005 else item.value_difference) }}</div></td>
<td class="px-3 py-3">{{ item.match_status|replace('_',' ')|title }}{% if item.match_method %}<div class="text-slate-500">{{ item.match_method|replace('_',' ')|title }} · {{ item.confidence }}%</div>{% endif %}</td>
<td class="px-3 py-3">{{ item.correction_status|replace('_',' ')|title }}{% if item.correction_note %}<div class="max-w-xs text-slate-500">{{ item.correction_note }}</div>{% endif %}</td>
</tr>
{% else %}<tr><td colspan="8" class="p-8 text-center text-slate-500">No stock rows for this filter.</td></tr>{% endfor %}
{% else %}<tr><td colspan="8" class="p-8 text-center text-slate-500">No Stock Item rows for this category.</td></tr>{% endfor %}
</tbody>
</table>
</div>
{% if status_filter in ['quantity_difference','value_difference','quantity_and_value_difference'] and stocks %}
<div class="border-t border-slate-200 p-4">
<div class="flex flex-wrap gap-3">
<input name="confirm_text" required placeholder="APPLY OPENING BALANCES" class="min-w-[280px] rounded-lg border border-red-300 px-3 py-2 text-sm">
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected Changes</button>
</div>
</div>
{% endif %}
</form>
{% 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-2" href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status={{ status_filter }}&page={{ page-1 }}{% if show_zero_new %}&show_zero_new=1{% endif %}">Previous</a>{% endif %}
{% if page<pages %}<a class="rounded-lg border border-slate-300 px-3 py-2" href="?client_id={{ selected_client.id }}&run_id={{ selected_run.id }}&tab=stock&status={{ status_filter }}&page={{ page+1 }}{% if show_zero_new %}&show_zero_new=1{% endif %}">Next</a>{% endif %}
</div>
</div>
{% endif %}
</section>
{% endif %}
<section class="rounded-2xl border border-red-200 bg-red-50 p-5">
<h3 class="font-semibold text-red-900">Apply selected safe corrections directly to current-year Tally</h3>
<p class="mt-1 text-sm text-red-800">The Local Agent preflights the current opening against this comparison, applies only unchanged masters, and re-reads each master afterward. Type the confirmation phrase exactly.</p>
<div class="mt-3 flex flex-wrap gap-3">
<input name="confirm_text" required placeholder="APPLY OPENING BALANCES" class="min-w-[280px] rounded-lg border border-red-300 bg-white px-3 py-2 text-sm">
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected to Tally</button>
</div>
</section>
</form>
{% endif %}
</div>
{% endblock %}