from __future__ import annotations from urllib.parse import urlencode from fastapi import APIRouter, File, Form, Request, UploadFile from fastapi.responses import RedirectResponse 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.sales_service import import_sales_file, list_sales, review_sales from app.modules.accounting.sales_taxonomy import sales_income_natures 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/sales-income", tags=["accounting-sales-income-ui"], ) def _go(client_id=0, message="", error="", **filters): query = {} if client_id: query["client_id"] = client_id if message: query["message"] = message[:300] if error: query["error"] = error[:300] for key, value in filters.items(): if value not in (None, "", "all"): query[key] = value return RedirectResponse( "/tools/accounting/sales-income" + ("?" + urlencode(query) if query else ""), status_code=303, ) @router.get("") def page( request: Request, client_id: int | None = None, status: str = "all", customer: str = "", period: str = "", page: int = 1, per_page: int = 25, 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( (client for client in clients if client_id and int(client.id) == int(client_id)), None, ) rows = [] total = 0 pages = 1 if selected: rows, total, page, pages = list_sales( db, tenant_id=scope.tenant_id, client_id=selected.id, status=status, customer=customer, period=period, page=page, per_page=per_page, ) return templates.TemplateResponse( "modules/accounting/templates/accounting/sales_income.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": "Sales & Income", "clients": clients, "selected_client": selected, "rows": rows, "total": total, "page": page, "pages": pages, "per_page": per_page, "status_filter": status, "customer_filter": customer, "period_filter": period, "natures": sales_income_natures(db, scope.tenant_id), "message": message, "error": error, }, ) finally: db.close() @router.post("/import") async def import_file( request: Request, client_id: int = Form(...), source_type: str = Form("outward_import"), csrf_token: str = Form(...), file: UploadFile = File(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, denied = _require_partner(request, db, "accounting.learning.manage") if denied: return denied client, _, scope = _find_visible_client(db, request, user, client_id) if not client: return _go(error="Client is not visible.") payload = await file.read() if not payload: raise ValueError("Uploaded Sales & Income file is empty.") if len(payload) > 25 * 1024 * 1024: raise ValueError("Sales & Income import file exceeds the 25 MB limit.") result = import_sales_file( db, tenant_id=scope.tenant_id, client_id=client.id, filename=file.filename or "sales.xlsx", payload=payload, user_id=user.id, source_type=(source_type or "outward_import").strip()[:30], ) return _go( client.id, message=( f"Imported {result['inserted']} sales/income row(s); " f"{result['duplicates']} duplicate(s) skipped." ), ) except Exception as exc: db.rollback() return _go(client_id, error=str(exc)) finally: db.close() @router.post("/{row_id}/review") def review( request: Request, row_id: int, client_id: int = Form(...), final_nature_id: int = Form(...), final_ledger_name: str = Form(""), review_note: str = Form(""), status: str = Form("all"), customer: str = Form(""), period: str = Form(""), page: int = Form(1), per_page: int = Form(25), 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 = _find_visible_client(db, request, user, client_id) if not client: return _go(error="Client is not visible.") review_sales( db, tenant_id=scope.tenant_id, client_id=client.id, row_id=row_id, final_nature_id=final_nature_id, final_ledger_name=final_ledger_name, review_note=review_note, user_id=user.id, ) return _go( client.id, message="Sales / income accounting treatment reviewed.", status=status, customer=customer, period=period, page=page, per_page=per_page, ) except Exception as exc: db.rollback() return _go( client_id, error=str(exc), status=status, customer=customer, period=period, page=page, per_page=per_page, ) finally: db.close()