From 395f4eed1a5e6adc01d3af3293c8506778939ef2 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Sun, 23 Aug 2026 00:10:26 +0530 Subject: [PATCH] Expand Phase 15 Sales and Income accounting menus --- app/modules/accounting/sales_menu_ui.py | 105 ++++++++++++++++++ .../accounting/customer_intelligence.html | 1 + .../accounting/historical_sales_learning.html | 1 + .../accounting/sales_ledger_learning.html | 1 + .../templates/accounting/tally.html | 15 ++- app/ui/app.py | 2 + 6 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 app/modules/accounting/sales_menu_ui.py create mode 100644 app/modules/accounting/templates/accounting/customer_intelligence.html create mode 100644 app/modules/accounting/templates/accounting/historical_sales_learning.html create mode 100644 app/modules/accounting/templates/accounting/sales_ledger_learning.html diff --git a/app/modules/accounting/sales_menu_ui.py b/app/modules/accounting/sales_menu_ui.py new file mode 100644 index 0000000..15b927d --- /dev/null +++ b/app/modules/accounting/sales_menu_ui.py @@ -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() diff --git a/app/modules/accounting/templates/accounting/customer_intelligence.html b/app/modules/accounting/templates/accounting/customer_intelligence.html new file mode 100644 index 0000000..dbb55d4 --- /dev/null +++ b/app/modules/accounting/templates/accounting/customer_intelligence.html @@ -0,0 +1 @@ +{% extends "ui/templates/base/layout.html" %}{% block content %}

Sales & Income

Customer Intelligence

GSTIN-first customer identity and reviewer-confirmed customer → Sales/Income ledger learning.

{% if selected_client %}
{% for label,value in [('Customer mappings',summary.mapping_count),('Reviewed',summary.review_confirmed),('High confidence',summary.high_confidence),('Historical pairs',summary.historical_rows)] %}
{{ label }}
{{ value }}
{% endfor %}
{% for m in mappings %}{% else %}{% endfor %}
CustomerGSTINTally CustomerSales LedgerEvidenceConfidence
{{ m.customer_name }}{{ m.customer_gstin or '—' }}{{ m.tally_customer_ledger_name or '—' }}{{ m.sales_ledger_name or '—' }}{{ m.evidence_count }} · {{ m.source }}{{ m.confidence_percent }}%
No customer mappings yet.
{% endif %}
{% endblock %} \ No newline at end of file diff --git a/app/modules/accounting/templates/accounting/historical_sales_learning.html b/app/modules/accounting/templates/accounting/historical_sales_learning.html new file mode 100644 index 0000000..84ffe63 --- /dev/null +++ b/app/modules/accounting/templates/accounting/historical_sales_learning.html @@ -0,0 +1 @@ +{% extends "ui/templates/base/layout.html" %}{% block content %}

Sales & Income

Historical Sales Learning

Read-only Tally Sales-voucher evidence. Purchase history remains separate.

Collect Sales History
{% if selected_client %}
{% for e in evidence %}{% else %}{% endfor %}
Customer LedgerSales/Income LedgerVoucherCountAmountLast Used
{{ e.party_ledger_name }}{{ e.counter_ledger_name }}{{ e.voucher_type_name }}{{ e.voucher_count }}₹{{ '%.2f'|format(e.absolute_amount_total) }}{{ e.last_voucher_date or '—' }}
No historical Sales evidence yet.
{% endif %}
{% endblock %} \ No newline at end of file diff --git a/app/modules/accounting/templates/accounting/sales_ledger_learning.html b/app/modules/accounting/templates/accounting/sales_ledger_learning.html new file mode 100644 index 0000000..295c8bf --- /dev/null +++ b/app/modules/accounting/templates/accounting/sales_ledger_learning.html @@ -0,0 +1 @@ +{% extends "ui/templates/base/layout.html" %}{% block content %}

Sales & Income

Sales Ledger Learning

Combined reviewer-confirmed customer mappings and historical Tally Sales-ledger evidence.

{% if selected_client %}
{% for item in learning_rows %}{% set m=item.mapping %}{% set h=item.history %}{% else %}{% endfor %}
CustomerConfirmed Sales LedgerHistorical LedgerHistorical VouchersReviewed EvidenceConfidence
{{ m.customer_name }}{{ m.sales_ledger_name or '—' }}{{ h.counter_ledger_name if h else '—' }}{{ h.voucher_count if h else 0 }}{{ m.evidence_count }}{{ m.confidence_percent }}%
No learned sales-ledger mappings yet.
{% endif %}
{% endblock %} \ No newline at end of file diff --git a/app/modules/accounting/templates/accounting/tally.html b/app/modules/accounting/templates/accounting/tally.html index 3178781..67eca99 100644 --- a/app/modules/accounting/templates/accounting/tally.html +++ b/app/modules/accounting/templates/accounting/tally.html @@ -38,11 +38,20 @@
Sales & Income
-
Phase 14 starts the outward-sales/customer/income workflow. Existing accounting infrastructure is reused.
+
Sales & Income now mirrors the structured Purchases & Expenses menu. Every visible button below is a working Phase 14–15 tool.
diff --git a/app/ui/app.py b/app/ui/app.py index 08d8bda..8c9b202 100644 --- a/app/ui/app.py +++ b/app/ui/app.py @@ -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)