from __future__ import annotations from urllib.parse import urlencode from fastapi import APIRouter, Form, Request 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.historical_learning_service import active_natures, ledger_mappings from app.modules.accounting.ledger_learning_service import ( available_tally_guids, learning_summary, rank_suggestions, recent_events, record_review, ) from app.modules.accounting.ui import _find_visible_client, _require_partner, _visible_clients from app.modules.clients.business_profile_service import get_profile from app.modules.core.rbac.deps import get_user_permissions, get_user_roles router = APIRouter(prefix="/tools/accounting/ledger-learning", tags=["accounting-ledger-learning-ui"]) def _float_or_none(value): try: return float(value) if str(value or "").strip() else None except Exception: return None @router.get("") def ledger_learning( request: Request, client_id: int | None = None, tally_guid: str = "", supplier_name: str = "", supplier_gstin: str = "", hsn_code: str = "", description: str = "", amount: str = "", reviewed: int = 0, error: str = "", ): db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.learning.view") if response: return response 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) suggestions = [] mappings = [] tally_companies = [] profile = None summary = [] events = [] if selected: profile = get_profile(db, selected.id) mappings = ledger_mappings(db, scope.tenant_id, selected.id, tally_guid) tally_companies = available_tally_guids(db, scope.tenant_id, selected.id) summary = learning_summary(db, scope.tenant_id, selected.id) events = recent_events(db, scope.tenant_id, selected.id, 20) if any([supplier_name.strip(), supplier_gstin.strip(), hsn_code.strip(), description.strip()]): suggestions = rank_suggestions( db, tenant_id=scope.tenant_id, client_id=selected.id, tally_guid=tally_guid.strip(), supplier_name=supplier_name, supplier_gstin=supplier_gstin, hsn_code=hsn_code, description=description, amount=_float_or_none(amount), ) return templates.TemplateResponse("modules/accounting/templates/accounting/ledger_learning.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": "Self-improving Ledger Selection", "clients": clients, "selected_client": selected, "profile": profile, "tally_companies": tally_companies, "mappings": mappings, "natures": active_natures(db, scope.tenant_id), "suggestions": suggestions, "learning_summary": summary, "events": events, "query": { "tally_guid": tally_guid, "supplier_name": supplier_name, "supplier_gstin": supplier_gstin, "hsn_code": hsn_code, "description": description, "amount": amount, }, "reviewed": bool(reviewed), "error": error, }) finally: db.close() @router.post("/review") def review( request: Request, client_id: int = Form(...), tally_guid: str = Form(""), supplier_name: str = Form(""), supplier_gstin: str = Form(""), hsn_code: str = Form(""), description: str = Form(""), amount: str = Form(""), suggested_nature_id: str = Form(""), suggested_ledger_name: str = Form(""), suggested_confidence: int = Form(0), final_nature_id: int = Form(...), final_ledger_name: str = Form(""), explanation: str = Form(""), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.learning.manage") if response: return response client, _, scope = _find_visible_client(db, request, user, client_id) if not client: from app.core.http_responses import ui_access_denied return ui_access_denied() suggested_id = int(suggested_nature_id) if str(suggested_nature_id or "").isdigit() else None explanations = [x.strip() for x in str(explanation or "").split("||") if x.strip()] record_review( db, tenant_id=scope.tenant_id, client_id=client.id, tally_guid=tally_guid.strip(), supplier_name=supplier_name, supplier_gstin=supplier_gstin, hsn_code=hsn_code, description=description, amount=_float_or_none(amount), suggested_nature_id=suggested_id, suggested_ledger_name=suggested_ledger_name, suggested_confidence=suggested_confidence, final_nature_id=final_nature_id, final_ledger_name=final_ledger_name, user_id=user.id, explanation=explanations, ) params = { "client_id": client_id, "tally_guid": tally_guid, "supplier_name": supplier_name, "supplier_gstin": supplier_gstin, "hsn_code": hsn_code, "description": description, "amount": amount, "reviewed": 1, } return RedirectResponse(url="/tools/accounting/ledger-learning?" + urlencode(params), status_code=303) except Exception as exc: db.rollback() params = {"client_id": client_id, "error": str(exc)[:120]} return RedirectResponse(url="/tools/accounting/ledger-learning?" + urlencode(params), status_code=303) finally: db.close()