Add right-side accounting menu and sundry creditors ageing analysis

This commit is contained in:
A R R R Associates
2026-09-09 15:50:11 +05:30
parent 7f372330a5
commit 8ecf0141a0
8 changed files with 586 additions and 73 deletions
@@ -231,3 +231,179 @@ def refresh_transactions(*, node_code: str, accounting_payload: dict[str, Any],
return _unwrap(request_agent_command(
node_code, "accounting_sync_transactions", payload, timeout_seconds=330
))
def _creditor_movements(
*,
node_code: str,
accounting_payload: dict[str, Any],
from_date: str,
to_date: str,
limit: int = 50000,
) -> dict[str, Any]:
return mirror_query(
node_code=node_code,
accounting_payload=accounting_payload,
query="sundry_creditor_movements",
filters={
"from_date": str(from_date or "").strip(),
"to_date": str(to_date or "").strip(),
"limit": max(1, min(100000, int(limit or 50000))),
},
timeout_seconds=90,
)
def sundry_creditors_aging(
*,
node_code: str,
accounting_payload: dict[str, Any],
fy_start: str,
fy_end: str,
follow_up_accounting_payload: dict[str, Any] | None = None,
follow_up_start: str = "",
follow_up_end: str = "",
limit: int = 50000,
) -> dict[str, Any]:
"""Age Sundry Creditors at FY close and optionally trace later settlement.
The v3 Accounting Mirror does not yet persist Tally bill-allocation references.
Accordingly this routine performs a transparent FIFO allocation over the actual
creditor-ledger movements. Debit-side payments, sales and credit adjustments
settle the oldest credit balance first.
"""
from datetime import date as _date, timedelta as _timedelta
current = _creditor_movements(
node_code=node_code, accounting_payload=accounting_payload,
from_date=fy_start, to_date=fy_end, limit=limit,
)
later = {"ledgers": [], "rows": []}
if follow_up_accounting_payload is not None and follow_up_start and follow_up_end:
later = _creditor_movements(
node_code=node_code, accounting_payload=follow_up_accounting_payload,
from_date=follow_up_start, to_date=follow_up_end, limit=limit,
)
fy_start_date = _date.fromisoformat(fy_start)
fy_end_date = _date.fromisoformat(fy_end)
opening_date = fy_start_date - _timedelta(days=1)
ledger_meta = {str(x.get("ledger_name") or "").casefold(): x for x in (current.get("ledgers") or [])}
current_rows = current.get("rows") or []
later_rows = later.get("rows") or []
by_ledger: dict[str, list[dict[str, Any]]] = {}
for row in current_rows:
name = str(row.get("ledger_name") or "").strip()
if name:
by_ledger.setdefault(name, []).append(row)
later_by_ledger: dict[str, list[dict[str, Any]]] = {}
for row in later_rows:
name = str(row.get("ledger_name") or "").strip()
if name:
later_by_ledger.setdefault(name, []).append(row)
parties: list[dict[str, Any]] = []
details: list[dict[str, Any]] = []
for party_name in sorted({str(x.get("ledger_name") or "").strip() for x in current.get("ledgers") or [] if str(x.get("ledger_name") or "").strip()}, key=str.casefold):
meta = ledger_meta.get(party_name.casefold()) or {}
opening_raw = float(meta.get("opening_balance") or 0)
opening_credit = max(0.0, opening_raw)
lots: list[dict[str, Any]] = []
if opening_credit > 0.009:
lots.append({
"party_name": party_name, "source": "Opening Balance", "voucher_date": opening_date.isoformat(),
"voucher_type": "Opening", "voucher_number": "", "reference": "",
"original_credit": opening_credit, "remaining": opening_credit,
"outstanding_at_fy_end": 0.0, "paid_subsequently": 0.0, "final_payment_date": "",
})
def settle(amount: float, paid_on: str, track_later: bool = False):
remaining = max(0.0, float(amount or 0))
for lot in lots:
if remaining <= 0.009:
break
available = float(lot.get("remaining") or 0)
if available <= 0.009:
continue
applied = min(available, remaining)
lot["remaining"] = round(available - applied, 2)
remaining = round(remaining - applied, 2)
if track_later and lot.get("outstanding_at_fy_end", 0) > 0:
eligible = max(0.0, float(lot["outstanding_at_fy_end"]) - float(lot.get("paid_subsequently") or 0))
later_applied = min(eligible, applied)
if later_applied > 0:
lot["paid_subsequently"] = round(float(lot.get("paid_subsequently") or 0) + later_applied, 2)
if float(lot["outstanding_at_fy_end"]) - float(lot["paid_subsequently"]) <= 0.009:
lot["final_payment_date"] = paid_on
for row in sorted(by_ledger.get(party_name, []), key=lambda r: (str(r.get("voucher_date") or ""), int(r.get("line_no") or 0))):
amount = abs(float(row.get("amount") or 0))
if amount <= 0.009:
continue
side = str(row.get("dr_cr") or "").upper()
if side == "CR":
lots.append({
"party_name": party_name, "source": "Voucher", "voucher_date": str(row.get("voucher_date") or ""),
"voucher_type": str(row.get("voucher_type") or ""), "voucher_number": str(row.get("voucher_number") or ""),
"reference": str(row.get("reference") or ""), "original_credit": amount, "remaining": amount,
"outstanding_at_fy_end": 0.0, "paid_subsequently": 0.0, "final_payment_date": "",
})
elif side == "DR":
settle(amount, str(row.get("voucher_date") or ""), False)
closing_lots = [lot for lot in lots if float(lot.get("remaining") or 0) > 0.009]
if not closing_lots:
continue
for lot in closing_lots:
lot["outstanding_at_fy_end"] = round(float(lot["remaining"]), 2)
# Trace subsequent debit settlements only. Later credits create new liabilities
# and must not alter the settlement status of FY-end lots.
for row in sorted(later_by_ledger.get(party_name, []), key=lambda r: (str(r.get("voucher_date") or ""), int(r.get("line_no") or 0))):
if str(row.get("dr_cr") or "").upper() == "DR":
settle(abs(float(row.get("amount") or 0)), str(row.get("voucher_date") or ""), True)
within_180 = over_180 = paid_later = still_unpaid = 0.0
final_dates: list[str] = []
for lot in closing_lots:
lot_date = _date.fromisoformat(str(lot["voucher_date"]))
age_days = (fy_end_date - lot_date).days
bucket = ">180 Days" if age_days > 180 else "≤180 Days"
outstanding = round(float(lot["outstanding_at_fy_end"]), 2)
paid = round(min(outstanding, float(lot.get("paid_subsequently") or 0)), 2)
after = round(max(0.0, outstanding - paid), 2)
if bucket == ">180 Days":
over_180 += outstanding
paid_later += paid
still_unpaid += after
else:
within_180 += outstanding
if lot.get("final_payment_date"):
final_dates.append(str(lot["final_payment_date"]))
details.append({
**{k: lot.get(k) for k in ("party_name","source","voucher_date","voucher_type","voucher_number","reference","original_credit","final_payment_date")},
"outstanding_at_fy_end": outstanding, "age_days": age_days, "age_bucket": bucket,
"paid_subsequently": paid, "balance_after_follow_up": after, "allocation_basis": "FIFO from Accounting Mirror movements",
})
closing = round(within_180 + over_180, 2)
parties.append({
"party_name": party_name, "within_180": round(within_180,2), "over_180": round(over_180,2),
"closing_balance": closing, "over_180_paid_later": round(paid_later,2),
"over_180_still_unpaid": round(still_unpaid,2), "final_payment_date": max(final_dates) if final_dates else "",
})
summary = {
"party_count": len(parties),
"total_closing": round(sum(x["closing_balance"] for x in parties),2),
"within_180": round(sum(x["within_180"] for x in parties),2),
"over_180": round(sum(x["over_180"] for x in parties),2),
"over_180_paid_later": round(sum(x["over_180_paid_later"] for x in parties),2),
"over_180_still_unpaid": round(sum(x["over_180_still_unpaid"] for x in parties),2),
}
return {
"summary": summary, "parties": parties, "details": details,
"allocation_basis": "FIFO from Accounting Mirror movements; debit-side payments/sales/credit adjustments settle oldest balances first",
"current_mirror": current.get("mirror") or {}, "follow_up_mirror": later.get("mirror") or {},
}
@@ -0,0 +1,201 @@
from __future__ import annotations
from datetime import date
from io import BytesIO
import re
from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse
from openpyxl import Workbook
from openpyxl.styles import Font
from app.core.db.common import CommonSessionLocal
from app.core.security.csrf import get_or_create_csrf_token
from app.core.templating import templates
from app.modules.accounting.accounting_mirror_service import AccountingMirrorError, sundry_creditors_aging
from app.modules.accounting.ui import (
_accounting_storage_payload,
_active_financial_year,
_financial_year_bounds,
_find_visible_client,
_node_online,
_require_partner,
_visible_clients,
)
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.documents.services import get_active_storage_node_for_branch
router = APIRouter(prefix="/tools/accounting/sundry-creditors", tags=["accounting-creditors-aging-ui"])
def _fy_options(selected_fy: str) -> list[str]:
start, _ = _financial_year_bounds(selected_fy)
years = list(range(start.year + 1, start.year - 7, -1))
return [f"{year}-{str(year + 1)[-2:]}" for year in years]
def _resolve_fy(value: str, fallback: str) -> str:
text = str(value or "").strip()
if re.fullmatch(r"\d{4}-\d{2}", text):
_financial_year_bounds(text)
return text
return fallback
def _load_analysis(request: Request, db, user, client_id: int, financial_year: str, follow_up_fy: str):
client, clients, scope = _find_visible_client(db, request, user, client_id)
if not client:
return None, clients, scope, "Select a client visible to the active branch."
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
if not node or not _node_online(node):
return None, clients, scope, "ERP Local Agent is offline for the active branch."
fy_start, fy_end = _financial_year_bounds(financial_year)
follow_up_start = ""
follow_up_end = ""
follow_up_payload = None
if follow_up_fy:
follow_start, follow_end = _financial_year_bounds(follow_up_fy)
if follow_start <= fy_end:
return None, clients, scope, "Follow-up financial year must be after the analysis financial year."
follow_up_start = follow_start.isoformat()
follow_up_end = follow_end.isoformat()
follow_up_payload = _accounting_storage_payload(client, follow_up_fy)
payload = _accounting_storage_payload(client, financial_year)
try:
result = sundry_creditors_aging(
node_code=str(node.node_code),
accounting_payload=payload,
fy_start=fy_start.isoformat(),
fy_end=fy_end.isoformat(),
follow_up_accounting_payload=follow_up_payload,
follow_up_start=follow_up_start,
follow_up_end=follow_up_end,
limit=50000,
)
return result, clients, scope, ""
except AccountingMirrorError as exc:
return None, clients, scope, str(exc)
except Exception as exc:
return None, clients, scope, str(exc)
@router.get("")
def creditors_aging(
request: Request,
client_id: int | None = None,
financial_year: str = "",
follow_up_fy: str = "",
analyze: int = 0,
):
db = CommonSessionLocal()
try:
user, response = _require_partner(request, db, "accounting.tally.view")
if response:
return response
clients, scope = _visible_clients(db, request, user)
fallback_fy = _active_financial_year(request)
try:
selected_fy = _resolve_fy(financial_year, fallback_fy)
except Exception:
selected_fy = fallback_fy
selected_follow_up = str(follow_up_fy or "").strip()
selected_client = next((c for c in clients if client_id and int(c.id) == int(client_id)), None)
result = None
error = ""
if analyze and selected_client:
result, clients, scope, error = _load_analysis(
request, db, user, int(selected_client.id), selected_fy, selected_follow_up
)
context = {
"request": request,
"current_user": user,
"current_user_roles": get_user_roles(db, user.id),
"current_user_permissions": get_user_permissions(db, user.id),
"csrf_token": get_or_create_csrf_token(request),
"clients": clients,
"selected_client": selected_client,
"selected_fy": selected_fy,
"follow_up_fy": selected_follow_up,
"fy_options": _fy_options(selected_fy),
"analysis": result,
"error": error,
}
return templates.TemplateResponse(
"modules/accounting/templates/accounting/sundry_creditors_aging.html", context
)
finally:
db.close()
@router.get("/export.xlsx")
def creditors_aging_export(
request: Request,
client_id: int,
financial_year: str,
follow_up_fy: str = "",
):
db = CommonSessionLocal()
try:
user, response = _require_partner(request, db, "accounting.tally.view")
if response:
return response
result, _, _, error = _load_analysis(
request, db, user, int(client_id), financial_year, follow_up_fy
)
if error or not result:
from fastapi.responses import PlainTextResponse
return PlainTextResponse(error or "No analysis data is available.", status_code=400)
wb = Workbook()
ws = wb.active
ws.title = "Creditor Summary"
headers = [
"Party Name", "<=180 Days", ">180 Days", "Closing Balance",
">180 Days Paid Subsequently", ">180 Days Still Unpaid", "Final Payment Date",
]
ws.append(headers)
for cell in ws[1]:
cell.font = Font(bold=True)
for row in result.get("parties") or []:
ws.append([
row.get("party_name"), row.get("within_180"), row.get("over_180"), row.get("closing_balance"),
row.get("over_180_paid_later"), row.get("over_180_still_unpaid"), row.get("final_payment_date") or "",
])
detail = wb.create_sheet("Bill FIFO Detail")
detail_headers = [
"Party Name", "Source", "Voucher Date", "Voucher Type", "Voucher Number", "Reference",
"Original Credit", "Outstanding at FY End", "Age Days", "Age Bucket", "Paid Subsequently",
"Balance after Follow-up", "Final Payment Date", "Allocation Basis",
]
detail.append(detail_headers)
for cell in detail[1]:
cell.font = Font(bold=True)
for row in result.get("details") or []:
detail.append([
row.get("party_name"), row.get("source"), row.get("voucher_date"), row.get("voucher_type"),
row.get("voucher_number"), row.get("reference"), row.get("original_credit"),
row.get("outstanding_at_fy_end"), row.get("age_days"), row.get("age_bucket"),
row.get("paid_subsequently"), row.get("balance_after_follow_up"), row.get("final_payment_date") or "",
row.get("allocation_basis"),
])
for sheet in (ws, detail):
sheet.freeze_panes = "A2"
for col in sheet.columns:
width = min(36, max(12, max(len(str(c.value or "")) for c in col) + 2))
sheet.column_dimensions[col[0].column_letter].width = width
out = BytesIO()
wb.save(out)
out.seek(0)
filename = f"sundry_creditors_{financial_year.replace('-', '_')}.xlsx"
return StreamingResponse(
out,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
finally:
db.close()
@@ -0,0 +1,66 @@
{% 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">Accounting · Audit Analysis</p>
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Sundry Creditors – 180 Day Analysis</h1>
<p class="mt-1 max-w-4xl text-sm text-slate-500">Analyses closing Sundry Creditors into ≤180 days and &gt;180 days from the local Accounting Mirror. Settlements use FIFO because bill-reference allocations are not yet stored in the .act mirror. Debit-side payments, sales and credit adjustments in the same creditor ledger reduce the oldest outstanding balance first.</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 text-slate-700 hover:bg-slate-50">Back to Tally Accounting</a>
</div>
{% if error %}<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">{{ error }}</div>{% endif %}
<form method="get" class="grid gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm md:grid-cols-4">
<input type="hidden" name="analyze" value="1">
<label class="space-y-1 text-sm"><span class="font-semibold text-slate-700">Client</span>
<select name="client_id" required class="w-full rounded-xl border border-slate-300 px-3 py-2">
<option value="">Select client</option>
{% for c in clients %}<option value="{{ c.id }}" {% if selected_client and c.id == selected_client.id %}selected{% endif %}>{{ c.client_name }}</option>{% endfor %}
</select>
</label>
<label class="space-y-1 text-sm"><span class="font-semibold text-slate-700">Analysis FY</span>
<select name="financial_year" class="w-full rounded-xl border border-slate-300 px-3 py-2">
{% for fy in fy_options %}<option value="{{ fy }}" {% if fy == selected_fy %}selected{% endif %}>{{ fy }}</option>{% endfor %}
</select>
</label>
<label class="space-y-1 text-sm"><span class="font-semibold text-slate-700">Follow-up FY (optional)</span>
<select name="follow_up_fy" class="w-full rounded-xl border border-slate-300 px-3 py-2">
<option value="">Do not trace later payments</option>
{% for fy in fy_options %}{% if fy > selected_fy %}<option value="{{ fy }}" {% if fy == follow_up_fy %}selected{% endif %}>{{ fy }}</option>{% endif %}{% endfor %}
</select>
</label>
<div class="flex items-end"><button class="w-full rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Analyse Creditors</button></div>
</form>
{% if analysis %}
{% set s = analysis.summary or {} %}
<div class="grid gap-4 md:grid-cols-3 xl:grid-cols-6">
{% for label, value in [
('Total Closing Creditors', s.get('total_closing',0)), ('≤180 Days', s.get('within_180',0)), ('>180 Days', s.get('over_180',0)),
('>180 Paid Later', s.get('over_180_paid_later',0)), ('>180 Still Unpaid', s.get('over_180_still_unpaid',0)), ('Parties', s.get('party_count',0))] %}
<div class="rounded-2xl border border-slate-200 bg-white p-4"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">{{ label }}</div><div class="mt-2 text-xl font-semibold text-slate-900">{% if label == 'Parties' %}{{ value }}{% else %}₹ {{ '%.2f'|format(value) }}{% endif %}</div></div>
{% endfor %}
</div>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="text-xs text-slate-500">Allocation basis: <strong>{{ analysis.allocation_basis }}</strong>. Zero closing-balance creditors are excluded.</div>
<a href="/tools/accounting/sundry-creditors/export.xlsx?client_id={{ selected_client.id }}&financial_year={{ selected_fy }}{% if follow_up_fy %}&follow_up_fy={{ follow_up_fy }}{% endif %}" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-800">Export Excel</a>
</div>
<div class="overflow-x-auto rounded-2xl border border-slate-200 bg-white">
<table class="min-w-full text-sm">
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-600"><tr><th class="px-4 py-3">Party</th><th class="px-4 py-3 text-right">≤180 Days</th><th class="px-4 py-3 text-right">&gt;180 Days</th><th class="px-4 py-3 text-right">Closing</th><th class="px-4 py-3 text-right">&gt;180 Paid Later</th><th class="px-4 py-3 text-right">Still Unpaid</th><th class="px-4 py-3">Final Payment Date</th></tr></thead>
<tbody class="divide-y divide-slate-100">
{% for row in analysis.parties %}<tr><td class="px-4 py-3 font-semibold text-slate-800">{{ row.party_name }}</td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.within_180) }}</td><td class="px-4 py-3 text-right {% if row.over_180 > 0 %}font-semibold text-rose-700{% endif %}">{{ '%.2f'|format(row.over_180) }}</td><td class="px-4 py-3 text-right font-semibold">{{ '%.2f'|format(row.closing_balance) }}</td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.over_180_paid_later) }}</td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.over_180_still_unpaid) }}</td><td class="px-4 py-3">{{ row.final_payment_date or '—' }}</td></tr>{% else %}<tr><td colspan="7" class="px-4 py-8 text-center text-slate-500">No non-zero Sundry Creditors were found for the selected FY.</td></tr>{% endfor %}
</tbody>
</table>
</div>
<details class="rounded-2xl border border-slate-200 bg-white" open><summary class="cursor-pointer px-5 py-4 font-semibold text-slate-800">FIFO bill / balance detail ({{ analysis.details|length }})</summary>
<div class="overflow-x-auto border-t border-slate-100"><table class="min-w-full text-xs"><thead class="bg-slate-50 text-left font-semibold uppercase tracking-wide text-slate-600"><tr><th class="px-3 py-2">Party</th><th class="px-3 py-2">Source</th><th class="px-3 py-2">Date</th><th class="px-3 py-2">Voucher</th><th class="px-3 py-2 text-right">Original</th><th class="px-3 py-2 text-right">FY Closing</th><th class="px-3 py-2 text-right">Age</th><th class="px-3 py-2">Bucket</th><th class="px-3 py-2 text-right">Paid Later</th><th class="px-3 py-2 text-right">Follow-up Balance</th><th class="px-3 py-2">Final Paid</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in analysis.details %}<tr><td class="px-3 py-2">{{ row.party_name }}</td><td class="px-3 py-2">{{ row.source }}</td><td class="px-3 py-2">{{ row.voucher_date }}</td><td class="px-3 py-2">{{ row.voucher_type }} {{ row.voucher_number }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.original_credit) }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.outstanding_at_fy_end) }}</td><td class="px-3 py-2 text-right">{{ row.age_days }}</td><td class="px-3 py-2 {% if row.age_bucket == '>180 Days' %}font-semibold text-rose-700{% endif %}">{{ row.age_bucket }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.paid_subsequently) }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.balance_after_follow_up) }}</td><td class="px-3 py-2">{{ row.final_payment_date or '—' }}</td></tr>{% endfor %}</tbody></table></div>
</details>
{% endif %}
</div>
{% endblock %}
@@ -5,84 +5,66 @@
{% if transactions_synced %}
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">Tally accounting transactions synchronized successfully.</div>
{% endif %}
<div class="space-y-6">
<div class="relative space-y-6 xl:pr-80">
<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</p>
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Tally Accounting</h1>
<p class="mt-1 text-sm text-slate-500">Mirror Tally once into the client SQLite Accounting Mirror, then run accounting and audit analysis from the local .act database. Existing mapping and legacy sync controls remain available under Advanced 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>
<a href="/tools/accounting/master-integrity{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-rose-300 bg-white px-3 py-2 text-sm font-semibold text-rose-800">Master Integrity Engine</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/opening-balances{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-amber-300 bg-white px-3 py-2 text-sm font-semibold text-amber-800">Opening Balance Review</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>
<div class="rounded-2xl border border-slate-200 bg-white p-4">
<div class="mb-3 flex items-center justify-between gap-3">
<div>
<div class="text-xs font-semibold uppercase tracking-[0.14em] text-amber-700">Purchases & Expenses</div>
<div class="text-xs text-slate-500">Existing Phase 3–13 tools are grouped here; their URLs and behavior are unchanged.</div>
</div>
</div>
<div class="flex flex-wrap gap-2">
<a href="/tools/accounting/taxonomy" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Accounting Taxonomy</a>
<a href="/tools/accounting/historical-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">Historical Learning</a>
<a href="/tools/accounting/ledger-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">Ledger Learning</a>
<a href="/tools/accounting/gstr2b{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">GSTR-2B Intelligence</a>
<a href="/tools/accounting/purchase-enrichment{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">E-Invoice / E-Way Bill</a>
<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-3 py-2 text-sm font-semibold">Purchase Review</a>
<a href="/tools/accounting/purchase-posting{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">Purchase → Tally</a>
<a href="/tools/accounting/bank-posting{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">Bank Expenses / Payments</a>
<a href="/tools/accounting/bank-reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm font-semibold text-emerald-800">Bank Reconciliation</a>
<a href="/tools/tally/depreciation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">Depreciation (IT)</a>
<a href="/tools/accounting/cash-payments{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-orange-300 bg-white px-3 py-2 text-sm font-semibold text-orange-800">Cash Payment Compliance</a>
<a href="/tools/accounting/cash-payments/allocation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-amber-300 bg-white px-3 py-2 text-sm font-semibold text-amber-800">Cash Payment Entry Allocation</a>
<a href="/tools/accounting/tds/compliance{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-brand-600 bg-brand-600 px-3 py-2 text-sm font-semibold text-white">TDS Compliance Review</a>
<a href="/tools/accounting/tds/rules" class="rounded-xl border border-brand-300 bg-brand-50 px-3 py-2 text-sm font-semibold text-brand-700">TDS Rules</a>
<a href="/tools/accounting/tds/liability{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-brand-300 bg-brand-50 px-3 py-2 text-sm font-semibold text-brand-700">TDS Liability</a>
<a href="/tools/accounting/tds/reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-brand-300 bg-brand-50 px-3 py-2 text-sm font-semibold text-brand-700">TDS Reconciliation</a>
<a href="/tools/accounting/ai{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-indigo-300 bg-indigo-50 px-3 py-2 text-sm font-semibold text-indigo-800">Accounting AI</a>
<a href="/tools/accounting/internal-model" class="rounded-xl border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800">Internal Model</a>
</div>
</div>
<div class="rounded-2xl border border-emerald-200 bg-emerald-50/40 p-4">
<div class="mb-3">
<div class="text-xs font-semibold uppercase tracking-[0.14em] text-emerald-700">Sales & Income</div>
<div class="text-xs text-slate-500">Sales & Income now mirrors the structured Purchases & Expenses menu. Every visible button below is a working Phase 14–15 tool.</div>
</div>
<div class="flex flex-wrap gap-2">
{% if selected_client %}
<a href="/tools/accounting/sales-income?client_id={{ selected_client.id }}" class="rounded-xl bg-emerald-700 px-3 py-2 text-sm font-semibold text-white">Sales & Income Register</a>
<a href="/tools/accounting/customer-intelligence?client_id={{ selected_client.id }}" class="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm font-semibold text-emerald-800">Customer Intelligence</a>
<a href="/tools/accounting/historical-sales-learning?client_id={{ selected_client.id }}" class="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm font-semibold text-emerald-800">Historical Sales Learning</a>
<a href="/tools/accounting/sales-ledger-learning?client_id={{ selected_client.id }}" class="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm font-semibold text-emerald-800">Sales Ledger Learning</a>
{% else %}
<a href="/tools/accounting/sales-income" class="rounded-xl bg-emerald-700 px-3 py-2 text-sm font-semibold text-white">Sales & Income Register</a>
<a href="/tools/accounting/customer-intelligence" class="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm font-semibold text-emerald-800">Customer Intelligence</a>
<a href="/tools/accounting/historical-sales-learning" class="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm font-semibold text-emerald-800">Historical Sales Learning</a>
<a href="/tools/accounting/sales-ledger-learning" class="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm font-semibold text-emerald-800">Sales Ledger Learning</a>
{% endif %}
</div>
</div>
<div class="flex flex-wrap justify-end gap-2">
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}&refresh=1{% else %}?refresh=1{% endif %}" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">Refresh Tally Companies</a>
</div>
<p class="mt-1 max-w-4xl text-sm text-slate-500">Mirror Tally once into the client SQLite Accounting Mirror, then run accounting and audit analysis from the local .act database. Existing mapping, synchronization and write-back controls remain unchanged.</p>
</div>
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}&refresh=1{% else %}?refresh=1{% endif %}" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">Refresh Tally Companies</a>
</div>
<aside class="rounded-2xl border border-slate-200 bg-white p-3 shadow-lg xl:fixed xl:right-6 xl:top-24 xl:z-20 xl:max-h-[calc(100vh-7rem)] xl:w-72 xl:overflow-y-auto">
<div class="mb-2 flex items-center justify-between gap-2 px-2">
<div><div class="text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">Accounting Tools</div><div class="text-sm font-semibold text-slate-900">Menu & Submenus</div></div>
</div>
<div class="space-y-2">
<details class="rounded-xl border border-blue-200 bg-blue-50/40" open><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-blue-800">Accounting Foundation</summary><div class="space-y-1 border-t border-blue-100 p-2">
<a href="/tools/accounting/chart-of-accounts{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Chart of Accounts</a>
<a href="/tools/accounting/master-integrity{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Master Integrity Engine</a>
<a href="/tools/accounting/stock-items{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Stock Item Intelligence</a>
<a href="/tools/accounting/opening-balances{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Opening Balance Review</a>
<a href="/tools/accounting/voucher-engine{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Accounting Voucher Engine</a>
</div></details>
<details class="rounded-xl border border-amber-200 bg-amber-50/30" open><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-amber-800">Purchases & Expenses</summary><div class="space-y-1 border-t border-amber-100 p-2">
<a href="/tools/accounting/taxonomy" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Accounting Taxonomy</a>
<a href="/tools/accounting/historical-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Historical Learning</a>
<a href="/tools/accounting/ledger-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Ledger Learning</a>
<a href="/tools/accounting/gstr2b{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">GSTR-2B Intelligence</a>
<a href="/tools/accounting/purchase-enrichment{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">E-Invoice / E-Way Bill</a>
<a href="/tools/accounting/purchase-review{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Purchase Review</a>
<a href="/tools/accounting/purchase-posting{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Purchase → Tally</a>
<a href="/tools/accounting/sundry-creditors{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg bg-rose-50 px-3 py-2 text-sm font-semibold text-rose-800 hover:bg-rose-100">Sundry Creditors – 180 Days</a>
</div></details>
<details class="rounded-xl border border-emerald-200 bg-emerald-50/30"><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-emerald-800">Cash, Bank & Compliance</summary><div class="space-y-1 border-t border-emerald-100 p-2">
<a href="/tools/accounting/bank-posting{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Bank Expenses / Payments</a>
<a href="/tools/accounting/bank-reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Bank Reconciliation</a>
<a href="/tools/tally/depreciation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Depreciation (IT)</a>
<a href="/tools/accounting/cash-payments{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Cash Payment Compliance</a>
<a href="/tools/accounting/cash-payments/allocation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Cash Payment Entry Allocation</a>
<a href="/tools/accounting/tds/compliance{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">TDS Compliance Review</a>
<a href="/tools/accounting/tds/rules" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">TDS Rules</a>
<a href="/tools/accounting/tds/liability{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">TDS Liability</a>
<a href="/tools/accounting/tds/reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">TDS Reconciliation</a>
</div></details>
<details class="rounded-xl border border-emerald-200 bg-emerald-50/30"><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-emerald-800">Sales & Income</summary><div class="space-y-1 border-t border-emerald-100 p-2">
<a href="/tools/accounting/sales-income{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Sales & Income Register</a>
<a href="/tools/accounting/customer-intelligence{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Customer Intelligence</a>
<a href="/tools/accounting/historical-sales-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Historical Sales Learning</a>
<a href="/tools/accounting/sales-ledger-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Sales Ledger Learning</a>
</div></details>
<details class="rounded-xl border border-indigo-200 bg-indigo-50/30"><summary class="cursor-pointer px-3 py-2 text-sm font-semibold text-indigo-800">AI & Advanced</summary><div class="space-y-1 border-t border-indigo-100 p-2">
<a href="/tools/accounting/ai{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Accounting AI</a>
<a href="/tools/accounting/internal-model" class="block rounded-lg px-3 py-2 text-sm hover:bg-white">Internal Model</a>
</div></details>
</div>
</aside>
{% if initialized %}
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">Client accounting storage was initialized successfully.</div>
{% endif %}