from __future__ import annotations from urllib.parse import urlencode from fastapi import APIRouter, Form, Request from fastapi.responses import RedirectResponse from sqlalchemy import select from app.core.db.common import CommonSessionLocal from app.core.security.csrf import get_or_create_csrf_token, validate_csrf from app.core.templating import templates from app.modules.accounting.chart_models import AccountingChartLedger from app.modules.accounting.chart_service import ROLE_LABELS from app.modules.accounting.ui import _find_visible_client, _require_partner, _visible_clients from app.modules.accounting.voucher_engine_service import ( BUSINESS_NATURES, VOUCHER_TYPES, approve_draft, chart_ledgers, create_draft, get_draft, list_drafts, queue_post, queue_preflight, review_draft, sync_drafts, visible_workstations, void_draft, ) from app.modules.core.rbac.deps import get_user_permissions, get_user_roles router = APIRouter(prefix="/tools/accounting/voucher-engine", tags=["accounting-native-voucher-engine-ui"]) def _go(client_id=0, message="", error="", **extra): params = {} if client_id: params["client_id"] = client_id if message: params["message"] = message[:350] if error: params["error"] = error[:350] for key, value in extra.items(): if value not in ("", None): params[key] = value return RedirectResponse( "/tools/accounting/voucher-engine" + ("?" + urlencode(params) if params else ""), status_code=303, ) def _company_options(db, tenant_id, client_id): rows = list( db.execute( select(AccountingChartLedger.tally_guid, AccountingChartLedger.company_name) .where( AccountingChartLedger.tenant_id == int(tenant_id), AccountingChartLedger.client_id == int(client_id), ) .distinct() .order_by(AccountingChartLedger.company_name) ).all() ) return [{"guid": guid, "name": name or guid} for guid, name in rows if guid] def _company_name(options, tally_guid): return next((row["name"] for row in options if row["guid"] == tally_guid), "") @router.get("") def page( request: Request, client_id: int | None = None, tally_guid: str = "", status: str = "", page: int = 1, per_page: int = 20, message: str = "", error: str = "", ): db = CommonSessionLocal() try: user, denied = _require_partner(request, db, "accounting.learning.view") if denied: return denied 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) company_options = [] ledgers = [] drafts = [] total = 0 pages = 1 workstations = [] if selected: company_options = _company_options(db, scope.tenant_id, selected.id) if not tally_guid and len(company_options) == 1: tally_guid = company_options[0]["guid"] if tally_guid and tally_guid not in {row["guid"] for row in company_options}: tally_guid = "" if tally_guid: ledgers = chart_ledgers( db, tenant_id=scope.tenant_id, client_id=selected.id, tally_guid=tally_guid, ) drafts, total, page, pages = list_drafts( db, tenant_id=scope.tenant_id, client_id=selected.id, status=status, page=page, per_page=per_page, ) sync_drafts(db, drafts) drafts, total, page, pages = list_drafts( db, tenant_id=scope.tenant_id, client_id=selected.id, status=status, page=page, per_page=per_page, ) workstations = visible_workstations( db, tenant_id=scope.tenant_id, branch_id=scope.branch_id, ) return templates.TemplateResponse( "modules/accounting/templates/accounting/voucher_engine.html", { "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": "Accounting Voucher Engine", "clients": clients, "selected_client": selected, "selected_tally_guid": tally_guid, "company_options": company_options, "ledgers": ledgers, "role_labels": ROLE_LABELS, "voucher_types": VOUCHER_TYPES, "business_natures": BUSINESS_NATURES, "drafts": drafts, "total": total, "page": page, "pages": pages, "per_page": per_page, "status_filter": status, "workstations": workstations, "message": message, "error": error, }, ) finally: db.close() @router.post("/draft") def create( request: Request, client_id: int = Form(...), tally_guid: str = Form(...), voucher_type: str = Form(...), business_nature: str = Form(...), voucher_date: str = Form(...), reference: str = Form(""), narration: str = Form(""), source_type: str = Form("manual"), ledger_name: list[str] = Form(...), dr_cr: list[str] = Form(...), amount: list[float] = Form(...), line_note: list[str] = Form([]), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, denied = _require_partner(request, db, "accounting.learning.manage") if denied: return denied client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: return _go(error="Client is not visible.") options = _company_options(db, scope.tenant_id, client.id) company_name = _company_name(options, tally_guid) if not company_name: raise ValueError("Chart of Accounts for the selected Tally company is not available.") draft = create_draft( db, tenant_id=scope.tenant_id, client_id=client.id, tally_guid=tally_guid, company_name=company_name, voucher_type=voucher_type, business_nature=business_nature, voucher_date=voucher_date, reference=reference, narration=narration, source_type=source_type, ledger_names=ledger_name, dr_cr_values=dr_cr, amounts=amount, notes=line_note, user_id=user.id, ) return _go(client.id, tally_guid=tally_guid, message=f"{draft.voucher_type} voucher draft #{draft.id} created.") except Exception as exc: db.rollback() return _go(client_id, tally_guid=tally_guid, error=str(exc)) finally: db.close() def _draft_for_action(db, request, user, client_id, draft_id): client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: raise ValueError("Client is not visible.") draft = get_draft( db, tenant_id=scope.tenant_id, client_id=client.id, draft_id=draft_id, ) if not draft: raise ValueError("Voucher draft was not found.") return client, scope, draft @router.post("/draft/{draft_id}/review") def review( request: Request, draft_id: int, client_id: int = Form(...), note: str = Form(""), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, denied = _require_partner(request, db, "accounting.learning.manage") if denied: return denied client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id) review_draft(db, draft=draft, note=note, user_id=user.id) return _go(client.id, message=f"Voucher draft #{draft.id} reviewed.") except Exception as exc: db.rollback() return _go(client_id, error=str(exc)) finally: db.close() @router.post("/draft/{draft_id}/approve") def approve( request: Request, draft_id: int, client_id: int = Form(...), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, denied = _require_partner(request, db, "accounting.learning.manage") if denied: return denied client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id) approve_draft(db, draft=draft, user_id=user.id) return _go(client.id, message=f"Voucher draft #{draft.id} approved for Tally preflight.") except Exception as exc: db.rollback() return _go(client_id, error=str(exc)) finally: db.close() @router.post("/draft/{draft_id}/preflight") def preflight( request: Request, draft_id: int, client_id: int = Form(...), workstation_id: int = Form(...), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, denied = _require_partner(request, db, "accounting.learning.manage") if denied: return denied client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id) queue_preflight(db, draft=draft, workstation_id=workstation_id, user_id=user.id) return _go(client.id, message=f"Voucher draft #{draft.id} Tally preflight queued.") except Exception as exc: db.rollback() return _go(client_id, error=str(exc)) finally: db.close() @router.post("/draft/{draft_id}/post") def post( request: Request, draft_id: int, client_id: int = Form(...), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, denied = _require_partner(request, db, "accounting.learning.manage") if denied: return denied client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id) queue_post(db, draft=draft, user_id=user.id) return _go(client.id, message=f"Voucher draft #{draft.id} controlled Tally posting queued.") except Exception as exc: db.rollback() return _go(client_id, error=str(exc)) finally: db.close() @router.post("/draft/{draft_id}/void") def void( request: Request, draft_id: int, client_id: int = Form(...), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, denied = _require_partner(request, db, "accounting.learning.manage") if denied: return denied client, scope, draft = _draft_for_action(db, request, user, client_id, draft_id) void_draft(db, draft=draft) return _go(client.id, message=f"Voucher draft #{draft.id} voided.") except Exception as exc: db.rollback() return _go(client_id, error=str(exc)) finally: db.close()