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.automatic_voucher_service import ( create_bank_draft, create_eway_invoice_draft, ) from app.modules.accounting.chart_models import AccountingChartLedger from app.modules.accounting.chart_service import effective_role from app.modules.accounting.source_adapter_service import ( bank_rows, discover_sources, eway_rows, group_eway_invoices, ) from app.modules.accounting.ui import _find_visible_client, _require_partner, _visible_clients from app.modules.core.rbac.deps import get_user_permissions, get_user_roles router = APIRouter( prefix="/tools/accounting/voucher-engine/automatic", tags=["accounting-automatic-voucher-source-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/automatic" + ("?" + 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), "") def _source_table(valid, requested): names = {row.table_name for row in valid} if requested in names: return requested return valid[0].table_name if valid else "" @router.get("") def page( request: Request, client_id: int | None = None, tally_guid: str = "", bank_source: str = "", eway_source: str = "", direction: str = "", from_date: str = "", to_date: str = "", 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 = [] sources = {"bank": [], "eway": []} selected_bank_source = "" selected_eway_source = "" banks = [] eway_invoices = [] bank_ledgers = [] 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 not in {row["guid"] for row in company_options}: tally_guid = "" if tally_guid: bank_ledgers = list( db.execute( select(AccountingChartLedger) .where( AccountingChartLedger.tenant_id == int(scope.tenant_id), AccountingChartLedger.client_id == int(selected.id), AccountingChartLedger.tally_guid == tally_guid, ) .order_by(AccountingChartLedger.name) ).scalars().all() ) bank_ledgers = [ row for row in bank_ledgers if effective_role(row) in {"BANK", "CASH"} ] sources = discover_sources(db) selected_bank_source = _source_table(sources["bank"], bank_source) selected_eway_source = _source_table(sources["eway"], eway_source) if selected_bank_source: banks = bank_rows( db, table_name=selected_bank_source, client_id=selected.id, from_date=from_date, to_date=to_date, limit=300, ) if selected_eway_source: items = eway_rows( db, table_name=selected_eway_source, client_id=selected.id, direction=direction, from_date=from_date, to_date=to_date, limit=5000, ) eway_invoices = group_eway_invoices(items) return templates.TemplateResponse( "modules/accounting/templates/accounting/automatic_voucher_sources.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": "Automatic Accounting Preparation", "clients": clients, "selected_client": selected, "selected_tally_guid": tally_guid, "company_options": company_options, "sources": sources, "selected_bank_source": selected_bank_source, "selected_eway_source": selected_eway_source, "bank_rows": banks, "eway_invoices": eway_invoices, "bank_ledgers": bank_ledgers, "direction": direction, "from_date": from_date, "to_date": to_date, "message": message, "error": error, }, ) finally: db.close() @router.post("/bank/draft") def bank_draft( request: Request, client_id: int = Form(...), tally_guid: str = Form(...), source_table: str = Form(...), source_id: str = Form(...), bank_ledger_name: 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: raise ValueError("Client is not visible.") sources = discover_sources(db)["bank"] if source_table not in {row.table_name for row in sources}: raise ValueError("Selected bank source is not an approved discovered source.") source = next( ( row for row in bank_rows( db, table_name=source_table, client_id=client.id, limit=2000, ) if str(row.get("source_id") or "") == str(source_id) ), None, ) if source is None: raise ValueError("Bank transaction could not be reloaded from the source.") options = _company_options(db, scope.tenant_id, client.id) company_name = _company_name(options, tally_guid) if not company_name: raise ValueError("Selected Tally company is not available in Chart of Accounts.") draft = create_bank_draft( db, tenant_id=scope.tenant_id, client_id=client.id, tally_guid=tally_guid, company_name=company_name, source_row=source, bank_ledger_name=bank_ledger_name, user_id=user.id, ) return _go( client.id, tally_guid=tally_guid, bank_source=source_table, message=f"{draft.voucher_type} draft #{draft.id} created automatically from bank transaction.", ) except Exception as exc: db.rollback() return _go( client_id, tally_guid=tally_guid, bank_source=source_table, error=str(exc), ) finally: db.close() @router.post("/eway/draft") def eway_draft( request: Request, client_id: int = Form(...), tally_guid: str = Form(...), source_table: str = Form(...), direction: str = Form(...), eway_bill_no: str = Form(""), invoice_no: str = Form(""), invoice_date: str = Form(""), party_gstin: str = Form(""), party_name: 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: raise ValueError("Client is not visible.") sources = discover_sources(db)["eway"] if source_table not in {row.table_name for row in sources}: raise ValueError("Selected E-Way Bill source is not an approved discovered source.") invoices = group_eway_invoices( eway_rows( db, table_name=source_table, client_id=client.id, direction=direction, limit=10000, ) ) invoice = next( ( row for row in invoices if (row.get("eway_bill_no") or "") == eway_bill_no and (row.get("invoice_no") or "") == invoice_no and (row.get("invoice_date") or "") == invoice_date and (row.get("party_gstin") or "") == party_gstin and (row.get("party_name") or "") == party_name ), None, ) if invoice is None: raise ValueError("E-Way Bill invoice/items could not be reloaded from the source.") options = _company_options(db, scope.tenant_id, client.id) company_name = _company_name(options, tally_guid) if not company_name: raise ValueError("Selected Tally company is not available in Chart of Accounts.") draft = create_eway_invoice_draft( db, tenant_id=scope.tenant_id, client_id=client.id, tally_guid=tally_guid, company_name=company_name, invoice=invoice, user_id=user.id, ) return _go( client.id, tally_guid=tally_guid, eway_source=source_table, direction=direction, message=( f"{draft.voucher_type} draft #{draft.id} created automatically " f"with {len(draft.items)} stock-item line(s)." ), ) except Exception as exc: db.rollback() return _go( client_id, tally_guid=tally_guid, eway_source=source_table, direction=direction, error=str(exc), ) finally: db.close()