Expand Phase 15 Sales and Income accounting menus

This commit is contained in:
A R R R Associates
2026-08-23 00:10:26 +05:30
parent 230fe7ffc5
commit 395f4eed1a
6 changed files with 122 additions and 3 deletions
+105
View File
@@ -0,0 +1,105 @@
from __future__ import annotations
from fastapi import APIRouter, Request
from sqlalchemy import select
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.sales_learning_models import AccountingSalesHistoricalRun
from app.modules.accounting.sales_learning_service import historical_sales_rows, ingest_completed_sales_run, learning_summary, sales_mappings
from app.modules.accounting.ui import _require_partner, _visible_clients
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
router = APIRouter(prefix="/tools/accounting", tags=["accounting-sales-menu-ui"])
def _context(db, request, user, title, client_id):
clients, scope = _visible_clients(db, request, user)
selected = next((c for c in clients if client_id and int(c.id) == int(client_id)), None)
return selected, scope, {
"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),
"title": title,
"clients": clients,
"selected_client": selected,
}
def _sync_runs(db, tenant_id, client_id):
runs = list(db.execute(select(AccountingSalesHistoricalRun).where(
AccountingSalesHistoricalRun.tenant_id == int(tenant_id),
AccountingSalesHistoricalRun.client_id == int(client_id),
).order_by(AccountingSalesHistoricalRun.id.desc()).limit(20)).scalars().all())
for run in runs:
if run.status not in {"completed", "failed"}:
ingest_completed_sales_run(db, run)
return runs
@router.get("/customer-intelligence")
def customer_intelligence(request: Request, client_id: int | None = None):
db = CommonSessionLocal()
try:
user, denied = _require_partner(request, db, "accounting.learning.view")
if denied: return denied
selected, scope, ctx = _context(db, request, user, "Customer Intelligence", client_id)
mappings = []
summary = {"mapping_count": 0, "historical_rows": 0, "review_confirmed": 0, "high_confidence": 0}
if selected:
mappings = sales_mappings(db, tenant_id=scope.tenant_id, client_id=selected.id)
summary = learning_summary(db, tenant_id=scope.tenant_id, client_id=selected.id)
ctx.update(mappings=mappings, summary=summary)
return templates.TemplateResponse("modules/accounting/templates/accounting/customer_intelligence.html", ctx)
finally:
db.close()
@router.get("/historical-sales-learning")
def historical_sales_learning(request: Request, client_id: int | None = None):
db = CommonSessionLocal()
try:
user, denied = _require_partner(request, db, "accounting.learning.view")
if denied: return denied
selected, scope, ctx = _context(db, request, user, "Historical Sales Learning", client_id)
evidence, runs = [], []
summary = {"mapping_count": 0, "historical_rows": 0, "review_confirmed": 0, "high_confidence": 0}
if selected:
runs = _sync_runs(db, scope.tenant_id, selected.id)
evidence = historical_sales_rows(db, tenant_id=scope.tenant_id, client_id=selected.id)[:500]
summary = learning_summary(db, tenant_id=scope.tenant_id, client_id=selected.id)
ctx.update(evidence=evidence, runs=runs, summary=summary)
return templates.TemplateResponse("modules/accounting/templates/accounting/historical_sales_learning.html", ctx)
finally:
db.close()
@router.get("/sales-ledger-learning")
def sales_ledger_learning(request: Request, client_id: int | None = None):
db = CommonSessionLocal()
try:
user, denied = _require_partner(request, db, "accounting.learning.view")
if denied: return denied
selected, scope, ctx = _context(db, request, user, "Sales Ledger Learning", client_id)
mappings, evidence = [], []
summary = {"mapping_count": 0, "historical_rows": 0, "review_confirmed": 0, "high_confidence": 0}
if selected:
_sync_runs(db, scope.tenant_id, selected.id)
mappings = sales_mappings(db, tenant_id=scope.tenant_id, client_id=selected.id)
evidence = historical_sales_rows(db, tenant_id=scope.tenant_id, client_id=selected.id)[:500]
summary = learning_summary(db, tenant_id=scope.tenant_id, client_id=selected.id)
hist = {}
for e in evidence:
hist.setdefault(str(e.party_ledger_name or '').strip().casefold(), []).append(e)
rows = []
for m in mappings:
key = str(m.tally_customer_ledger_name or m.customer_name or '').strip().casefold()
h = hist.get(key, [])
rows.append({"mapping": m, "history": h[0] if h else None, "history_count": len(h)})
ctx.update(learning_rows=rows, summary=summary)
return templates.TemplateResponse("modules/accounting/templates/accounting/sales_ledger_learning.html", ctx)
finally:
db.close()
@@ -0,0 +1 @@
{% extends "ui/templates/base/layout.html" %}{% block content %}<div class="space-y-6"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-emerald-700">Sales & Income</p><h1 class="mt-1 text-2xl font-semibold">Customer Intelligence</h1><p class="mt-1 text-sm text-slate-500">GSTIN-first customer identity and reviewer-confirmed customer → Sales/Income ledger learning.</p></div><section class="rounded-2xl bg-white p-5 shadow-soft"><form method="get" class="grid gap-4 md:grid-cols-[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><div class="flex items-end"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Open</button></div></form></section>{% if selected_client %}<section class="grid gap-4 sm:grid-cols-4">{% for label,value in [('Customer mappings',summary.mapping_count),('Reviewed',summary.review_confirmed),('High confidence',summary.high_confidence),('Historical pairs',summary.historical_rows)] %}<div class="rounded-2xl bg-white p-4 shadow-soft"><div class="text-xs uppercase text-slate-500">{{ label }}</div><div class="mt-2 text-2xl font-semibold">{{ value }}</div></div>{% endfor %}</section><section class="rounded-2xl bg-white shadow-soft overflow-x-auto"><table class="min-w-full text-sm"><thead><tr class="border-b bg-slate-50 text-left text-xs uppercase text-slate-500"><th class="px-3 py-3">Customer</th><th class="px-3 py-3">GSTIN</th><th class="px-3 py-3">Tally Customer</th><th class="px-3 py-3">Sales Ledger</th><th class="px-3 py-3">Evidence</th><th class="px-3 py-3">Confidence</th></tr></thead><tbody>{% for m in mappings %}<tr class="border-b"><td class="px-3 py-3 font-semibold">{{ m.customer_name }}</td><td class="px-3 py-3">{{ m.customer_gstin or '—' }}</td><td class="px-3 py-3">{{ m.tally_customer_ledger_name or '—' }}</td><td class="px-3 py-3">{{ m.sales_ledger_name or '—' }}</td><td class="px-3 py-3">{{ m.evidence_count }} · {{ m.source }}</td><td class="px-3 py-3">{{ m.confidence_percent }}%</td></tr>{% else %}<tr><td colspan="6" class="p-8 text-center text-slate-500">No customer mappings yet.</td></tr>{% endfor %}</tbody></table></section>{% endif %}</div>{% endblock %}
@@ -0,0 +1 @@
{% extends "ui/templates/base/layout.html" %}{% block content %}<div class="space-y-6"><div class="flex justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-emerald-700">Sales & Income</p><h1 class="mt-1 text-2xl font-semibold">Historical Sales Learning</h1><p class="mt-1 text-sm text-slate-500">Read-only Tally Sales-voucher evidence. Purchase history remains separate.</p></div><a href="/tools/accounting/sales-learning{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-800 h-fit">Collect Sales History</a></div><section class="rounded-2xl bg-white p-5 shadow-soft"><form method="get" class="grid gap-4 md:grid-cols-[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><div class="flex items-end"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Open</button></div></form></section>{% if selected_client %}<section class="rounded-2xl bg-white shadow-soft overflow-x-auto"><table class="min-w-full text-sm"><thead><tr class="border-b bg-slate-50 text-left text-xs uppercase text-slate-500"><th class="px-3 py-3">Customer Ledger</th><th class="px-3 py-3">Sales/Income Ledger</th><th class="px-3 py-3">Voucher</th><th class="px-3 py-3">Count</th><th class="px-3 py-3">Amount</th><th class="px-3 py-3">Last Used</th></tr></thead><tbody>{% for e in evidence %}<tr class="border-b"><td class="px-3 py-3 font-semibold">{{ e.party_ledger_name }}</td><td class="px-3 py-3">{{ e.counter_ledger_name }}</td><td class="px-3 py-3">{{ e.voucher_type_name }}</td><td class="px-3 py-3">{{ e.voucher_count }}</td><td class="px-3 py-3">₹{{ '%.2f'|format(e.absolute_amount_total) }}</td><td class="px-3 py-3">{{ e.last_voucher_date or '—' }}</td></tr>{% else %}<tr><td colspan="6" class="p-8 text-center text-slate-500">No historical Sales evidence yet.</td></tr>{% endfor %}</tbody></table></section>{% endif %}</div>{% endblock %}
@@ -0,0 +1 @@
{% extends "ui/templates/base/layout.html" %}{% block content %}<div class="space-y-6"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-emerald-700">Sales & Income</p><h1 class="mt-1 text-2xl font-semibold">Sales Ledger Learning</h1><p class="mt-1 text-sm text-slate-500">Combined reviewer-confirmed customer mappings and historical Tally Sales-ledger evidence.</p></div><section class="rounded-2xl bg-white p-5 shadow-soft"><form method="get" class="grid gap-4 md:grid-cols-[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><div class="flex items-end"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Open</button></div></form></section>{% if selected_client %}<section class="rounded-2xl bg-white shadow-soft overflow-x-auto"><table class="min-w-full text-sm"><thead><tr class="border-b bg-slate-50 text-left text-xs uppercase text-slate-500"><th class="px-3 py-3">Customer</th><th class="px-3 py-3">Confirmed Sales Ledger</th><th class="px-3 py-3">Historical Ledger</th><th class="px-3 py-3">Historical Vouchers</th><th class="px-3 py-3">Reviewed Evidence</th><th class="px-3 py-3">Confidence</th></tr></thead><tbody>{% for item in learning_rows %}{% set m=item.mapping %}{% set h=item.history %}<tr class="border-b"><td class="px-3 py-3 font-semibold">{{ m.customer_name }}</td><td class="px-3 py-3">{{ m.sales_ledger_name or '—' }}</td><td class="px-3 py-3">{{ h.counter_ledger_name if h else '—' }}</td><td class="px-3 py-3">{{ h.voucher_count if h else 0 }}</td><td class="px-3 py-3">{{ m.evidence_count }}</td><td class="px-3 py-3">{{ m.confidence_percent }}%</td></tr>{% else %}<tr><td colspan="6" class="p-8 text-center text-slate-500">No learned sales-ledger mappings yet.</td></tr>{% endfor %}</tbody></table></section>{% endif %}</div>{% endblock %}
@@ -38,11 +38,20 @@
<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">Phase 14 starts the outward-sales/customer/income workflow. Existing accounting infrastructure is reused.</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 Foundation</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 Foundation</a>{% endif %}
{% if selected_client %}<a href="/tools/accounting/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">Customer & Sales Ledger Intelligence</a>{% else %}<a href="/tools/accounting/sales-learning" class="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm font-semibold text-emerald-800">Customer & Sales Ledger Intelligence</a>{% endif %}
{% 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>
+2
View File
@@ -49,6 +49,7 @@ from app.modules.accounting.ai_ui import router as accounting_ai_ui_router
from app.modules.accounting.internal_model_ui import router as accounting_internal_model_ui_router
from app.modules.accounting.sales_ui import router as accounting_sales_income_ui_router
from app.modules.accounting.sales_learning_ui import router as accounting_sales_learning_ui_router
from app.modules.accounting.sales_menu_ui import router as accounting_sales_menu_ui_router
from app.modules.registrations.ui import router as registrations_ui_router
from app.modules.credential_vault.ui import router as credential_vault_ui_router
from app.modules.client_identity.ui import router as client_identity_ui_router
@@ -84,6 +85,7 @@ def mount_ui(app: FastAPI) -> None:
app.include_router(accounting_internal_model_ui_router)
app.include_router(accounting_sales_income_ui_router)
app.include_router(accounting_sales_learning_ui_router)
app.include_router(accounting_sales_menu_ui_router)
app.include_router(work_tracker_ui_router)
app.include_router(billing_ui_router)
app.include_router(platform_billing_ui_router)