diff --git a/app/modules/accounting/accounting_mirror_service.py b/app/modules/accounting/accounting_mirror_service.py index e975682..0fd5f32 100644 --- a/app/modules/accounting/accounting_mirror_service.py +++ b/app/modules/accounting/accounting_mirror_service.py @@ -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 {}, + } diff --git a/app/modules/accounting/creditors_aging_ui.py b/app/modules/accounting/creditors_aging_ui.py new file mode 100644 index 0000000..b1f6866 --- /dev/null +++ b/app/modules/accounting/creditors_aging_ui.py @@ -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() diff --git a/app/modules/accounting/templates/accounting/sundry_creditors_aging.html b/app/modules/accounting/templates/accounting/sundry_creditors_aging.html new file mode 100644 index 0000000..349bdea --- /dev/null +++ b/app/modules/accounting/templates/accounting/sundry_creditors_aging.html @@ -0,0 +1,66 @@ +{% extends "ui/templates/base/layout.html" %} +{% block content %} +
+
+
+

Accounting · Audit Analysis

+

Sundry Creditors – 180 Day Analysis

+

Analyses closing Sundry Creditors into ≤180 days and >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.

+
+ Back to Tally Accounting +
+ + {% if error %}
{{ error }}
{% endif %} + +
+ + + + +
+
+ + {% if analysis %} + {% set s = analysis.summary or {} %} +
+ {% 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))] %} +
{{ label }}
{% if label == 'Parties' %}{{ value }}{% else %}₹ {{ '%.2f'|format(value) }}{% endif %}
+ {% endfor %} +
+ +
+
Allocation basis: {{ analysis.allocation_basis }}. Zero closing-balance creditors are excluded.
+ Export Excel +
+ +
+ + + + {% for row in analysis.parties %}{% else %}{% endfor %} + +
Party≤180 Days>180 DaysClosing>180 Paid LaterStill UnpaidFinal Payment Date
{{ row.party_name }}{{ '%.2f'|format(row.within_180) }}{{ '%.2f'|format(row.over_180) }}{{ '%.2f'|format(row.closing_balance) }}{{ '%.2f'|format(row.over_180_paid_later) }}{{ '%.2f'|format(row.over_180_still_unpaid) }}{{ row.final_payment_date or '—' }}
No non-zero Sundry Creditors were found for the selected FY.
+
+ +
FIFO bill / balance detail ({{ analysis.details|length }}) +
{% for row in analysis.details %}{% endfor %}
PartySourceDateVoucherOriginalFY ClosingAgeBucketPaid LaterFollow-up BalanceFinal Paid
{{ row.party_name }}{{ row.source }}{{ row.voucher_date }}{{ row.voucher_type }} {{ row.voucher_number }}{{ '%.2f'|format(row.original_credit) }}{{ '%.2f'|format(row.outstanding_at_fy_end) }}{{ row.age_days }}{{ row.age_bucket }}{{ '%.2f'|format(row.paid_subsequently) }}{{ '%.2f'|format(row.balance_after_follow_up) }}{{ row.final_payment_date or '—' }}
+
+ {% endif %} +
+{% endblock %} diff --git a/app/modules/accounting/templates/accounting/tally.html b/app/modules/accounting/templates/accounting/tally.html index 56862ec..a00e034 100644 --- a/app/modules/accounting/templates/accounting/tally.html +++ b/app/modules/accounting/templates/accounting/tally.html @@ -5,84 +5,66 @@ {% if transactions_synced %}
Tally accounting transactions synchronized successfully.
{% endif %} -
+

Tools · Accounting

Tally Accounting

-

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.

-
-
-
-
-
Accounting Foundation
-
Build the client Chart of Accounts from Tally Groups and Ledgers before transaction-level classification.
-
- -
- - - - -
-
-
Sales & Income
-
Sales & Income now mirrors the structured Purchases & Expenses menu. Every visible button below is a working Phase 14–15 tool.
-
- -
- - +

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.

+ Refresh Tally Companies
+ + {% if initialized %}
Client accounting storage was initialized successfully.
{% endif %} diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py index 2b2e164..a14a06e 100644 --- a/app/modules/documents/agent_package.py +++ b/app/modules/documents/agent_package.py @@ -4,7 +4,7 @@ import io from pathlib import Path import zipfile -ERP_LOCAL_AGENT_VERSION = "1.26.10" +ERP_LOCAL_AGENT_VERSION = "1.26.11" ERP_LOCAL_AGENT_NAME = "ERP Local Agent" RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime" _DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0) diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py index c16f55a..3ea7852 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py @@ -1,2 +1,2 @@ -__version__ = "1.26.10" +__version__ = "1.26.11" AGENT_NAME = "ERP Local Agent" diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py index 92dfdae..f48eda9 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py @@ -4180,6 +4180,92 @@ class AgentCommandProcessor: f"SELECT * FROM {table} WHERE stock_item_name=? COLLATE NOCASE ORDER BY applicable_from,history_position", (item,)).fetchall()] return {"rows":rows,"count":len(rows)} + if query == "sundry_creditor_movements": + creditor_limit = max(1, min(100000, int(filters.get("limit") or 50000))) + # Resolve Sundry Creditors and all descendant groups from the mirror + # master hierarchy. Older mirrors may not have group_master; in that + # case direct-parent ledgers still remain analyzable. + group_parent = {} + try: + for gr in db.execute("SELECT name,parent FROM group_master").fetchall(): + name = str(gr["name"] or "").strip() + if name: + group_parent[name.casefold()] = str(gr["parent"] or "").strip() + except Exception: + group_parent = {} + + def is_creditor_group(name: str) -> bool: + current = str(name or "").strip() + seen = set() + while current and current.casefold() not in seen: + key = current.casefold() + seen.add(key) + if key in {"sundry creditors", "sundry creditor"}: + return True + current = group_parent.get(key, "") + return False + + ledgers = [] + for lr in db.execute( + "SELECT ledger_name,parent_group,opening_balance,closing_balance FROM ledger_master ORDER BY ledger_name" + ).fetchall(): + parent = str(lr["parent_group"] or "").strip() + if is_creditor_group(parent): + ledgers.append(dict(lr)) + # Direct parent fallback for mirrors where group_master was not + # available during an older extraction. + if not ledgers: + ledgers = [dict(r) for r in db.execute( + """SELECT ledger_name,parent_group,opening_balance,closing_balance + FROM ledger_master + WHERE LOWER(TRIM(parent_group)) IN ('sundry creditors','sundry creditor') + ORDER BY ledger_name""" + ).fetchall()] + + names = [str(x.get("ledger_name") or "").strip() for x in ledgers if str(x.get("ledger_name") or "").strip()] + rows = [] + if names: + placeholders = ",".join("?" for _ in names) + clauses = [f"LOWER(ledger_name) IN ({placeholders})"] + params = [x.casefold() for x in names] + if from_date: + clauses.append("voucher_date>=?") + params.append(from_date) + if to_date: + clauses.append("voucher_date<=?") + params.append(to_date) + sql = ( + "SELECT voucher_guid,line_no,voucher_date,voucher_type,voucher_number,party_ledger," + "ledger_name,dr_cr,amount,raw_amount FROM voucher_ledger_entry WHERE " + + " AND ".join(clauses) + + " ORDER BY ledger_name,voucher_date,line_no LIMIT ?" + ) + params.append(creditor_limit) + rows = [dict(r) for r in db.execute(sql, tuple(params)).fetchall()] + # Voucher reference/narration are intentionally joined from the + # voucher header so the exported audit trail remains readable. + if rows: + vmap = {} + guids = list(dict.fromkeys(str(r.get("voucher_guid") or "") for r in rows if r.get("voucher_guid"))) + for pos in range(0, len(guids), 800): + chunk = guids[pos:pos+800] + ph = ",".join("?" for _ in chunk) + for vr in db.execute( + f"SELECT voucher_guid,reference,narration FROM voucher WHERE voucher_guid IN ({ph})", + tuple(chunk), + ).fetchall(): + vmap[str(vr["voucher_guid"] or "")] = dict(vr) + for row in rows: + head = vmap.get(str(row.get("voucher_guid") or ""), {}) + row["reference"] = str(head.get("reference") or "") + row["narration"] = str(head.get("narration") or "") + return { + "ledgers": ledgers, + "rows": rows, + "count": len(rows), + "truncated": len(rows) >= creditor_limit, + "mirror": self.tally.mirror.status(client_id).get("mirror") or {}, + } if query == "voucher": guid=str(filters.get("voucher_guid") or "").strip() v=db.execute("SELECT * FROM voucher WHERE voucher_guid=?",(guid,)).fetchone() diff --git a/app/ui/app.py b/app/ui/app.py index 4a95467..6b86bf9 100644 --- a/app/ui/app.py +++ b/app/ui/app.py @@ -60,6 +60,7 @@ from app.modules.accounting.opening_balance_ui import router as accounting_openi from app.modules.accounting.automatic_voucher_ui import router as accounting_automatic_voucher_ui_router from app.modules.accounting.master_integrity_ui import router as accounting_master_integrity_ui_router from app.modules.accounting.cash_payment_ui import router as accounting_cash_payment_ui_router +from app.modules.accounting.creditors_aging_ui import router as accounting_creditors_aging_ui_router from app.modules.accounting.tds_ui import router as accounting_tds_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 @@ -105,6 +106,7 @@ def mount_ui(app: FastAPI) -> None: app.include_router(accounting_automatic_voucher_ui_router) app.include_router(accounting_master_integrity_ui_router) app.include_router(accounting_cash_payment_ui_router) + app.include_router(accounting_creditors_aging_ui_router) app.include_router(accounting_tds_ui_router) app.include_router(work_tracker_ui_router) app.include_router(billing_ui_router)