Files
arrr-erp/app/modules/accounting/purchase_review_ui.py
T
2026-08-22 15:54:25 +05:30

93 lines
6.7 KiB
Python

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
from app.modules.accounting.ledger_learning_service import available_tally_guids
from app.modules.accounting.purchase_review_service import (
review_queue, review_counts, return_periods, nature_maps_for_rows,
explanations_for_rows, mappings_by_nature, review_one, bulk_confirm,
)
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/purchase-review",tags=["accounting-purchase-review-ui"])
def _qs(client_id, filters, page):
params={"client_id":client_id,"tally_guid":filters.get("tally_guid", ""),"status":filters.get("status","all"),"confidence":filters.get("confidence","all"),"source":filters.get("source","all"),"supplier":filters.get("supplier",""),"return_period":filters.get("return_period",""),"per_page":filters.get("per_page",25),"page":page}
return "/tools/accounting/purchase-review?"+urlencode(params)
def _redirect(client_id, filters, *, message="", error="", page=None):
params=dict(filters); params["client_id"]=client_id; params["page"]=page or filters.get("page",1)
if message: params["message"]=message[:220]
if error: params["error"]=error[:220]
return RedirectResponse(url="/tools/accounting/purchase-review?"+urlencode(params),status_code=303)
@router.get("")
def board(request:Request,client_id:int|None=None,tally_guid:str="",status:str="all",confidence:str="all",source:str="all",supplier:str="",return_period:str="",page:int=1,per_page:int=25,message:str="",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)
result=counts=None; natures=[]; nature_by_id={}; mappings={}; explanations={}; periods=[]; companies=[]; prev_url=next_url=""
filters={"tally_guid":tally_guid,"status":status,"confidence":confidence,"source":source,"supplier":supplier,"return_period":return_period,"per_page":per_page,"page":page}
if selected:
result=review_queue(db,tenant_id=scope.tenant_id,client_id=selected.id,tally_guid=tally_guid,status=status,confidence=confidence,source=source,supplier=supplier,return_period=return_period,page=page,per_page=per_page)
counts=review_counts(db,tenant_id=scope.tenant_id,client_id=selected.id)
natures=active_natures(db,scope.tenant_id)
nature_by_id=nature_maps_for_rows(db,result["rows"])
mappings=mappings_by_nature(db,scope.tenant_id,selected.id,tally_guid)
explanations=explanations_for_rows(result["rows"])
periods=return_periods(db,tenant_id=scope.tenant_id,client_id=selected.id)
companies=available_tally_guids(db,scope.tenant_id,selected.id)
if result["page"]>1:prev_url=_qs(selected.id,filters,result["page"]-1)
if result["page"]<result["pages"]:next_url=_qs(selected.id,filters,result["page"]+1)
return templates.TemplateResponse("modules/accounting/templates/accounting/purchase_review.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":"Purchase Review Board","clients":clients,"selected_client":selected,"result":result,"counts":counts,"natures":natures,"nature_by_id":nature_by_id,"mappings_by_nature":mappings,"explanations":explanations,"return_periods":periods,"tally_companies":companies,"filters":filters,"prev_url":prev_url,"next_url":next_url,"message":message,"error":error})
finally:db.close()
@router.post("/purchase/{purchase_id}/review")
def review_purchase_row(request:Request,purchase_id:int,client_id:int=Form(...),final_nature_id:int=Form(...),final_ledger_name:str=Form(""),tally_guid:str=Form(""),status:str=Form("all"),confidence:str=Form("all"),source:str=Form("all"),supplier:str=Form(""),return_period:str=Form(""),page:int=Form(1),per_page:int=Form(25),csrf_token:str=Form(...)):
validate_csrf(request,csrf_token); db=CommonSessionLocal(); filters={"tally_guid":tally_guid,"status":status,"confidence":confidence,"source":source,"supplier":supplier,"return_period":return_period,"page":page,"per_page":per_page}
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()
row=review_one(db,tenant_id=scope.tenant_id,client_id=client.id,purchase_id=purchase_id,final_nature_id=final_nature_id,final_ledger_name=final_ledger_name,user_id=user.id)
return _redirect(client.id,filters,message=f"Invoice {row.invoice_number} reviewed.")
except Exception as exc:
db.rollback(); return _redirect(client_id,filters,error=str(exc))
finally:db.close()
@router.post("/bulk-confirm")
async def bulk_confirm_rows(request:Request):
form=await request.form(); validate_csrf(request,str(form.get("csrf_token") or ""))
client_id=int(form.get("client_id") or 0); filters={"tally_guid":str(form.get("tally_guid") or ""),"status":str(form.get("status") or "all"),"confidence":str(form.get("confidence") or "all"),"source":str(form.get("source") or "all"),"supplier":str(form.get("supplier") or ""),"return_period":str(form.get("return_period") or ""),"page":int(form.get("page") or 1),"per_page":int(form.get("per_page") or 25)}
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()
count=bulk_confirm(db,tenant_id=scope.tenant_id,client_id=client.id,purchase_ids=form.getlist("purchase_ids"),user_id=user.id)
return _redirect(client.id,filters,message=f"Confirmed {count} high-confidence purchase(s).")
except Exception as exc:
db.rollback(); return _redirect(client_id,filters,error=str(exc))
finally:db.close()