Files
arrr-erp/app/modules/accounting/historical_learning_ui.py
T
2026-08-23 12:00:21 +05:30

159 lines
8.6 KiB
Python

from __future__ import annotations
import json
from datetime import date, datetime, timezone
from fastapi import APIRouter, Form, Request
from fastapi.responses import JSONResponse, 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.historical_learning_models import AccountingHistoricalLearningRun
from app.modules.accounting.historical_learning_service import active_natures, classifiable_ledgers, evidence_rows, ingest_completed_run, ledger_mappings, party_suggestions, remove_ledger_mapping, save_ledger_mapping
from app.modules.accounting.ui import _accounting_storage_payload, _find_visible_client, _require_partner, _visible_clients
from app.modules.accounting.historical_sync_support import eligible_tally_workstations, resolve_workstation_company, workstation_companies
from app.modules.accounting.historical_progress import historical_run_status
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.core.rbac.permission_guard import require_permission
from app.modules.documents.agent_jobs import enqueue_agent_job
from app.modules.documents.models import ERPAgentJob, ERPWorkstationAgent
router = APIRouter(prefix="/tools/accounting/historical-learning", tags=["accounting-historical-learning-ui"])
def _denied():
from app.core.http_responses import ui_access_denied
return ui_access_denied()
def _fy_dates():
today=date.today(); y=today.year if today.month>=4 else today.year-1
return date(y,4,1).isoformat(), today.isoformat()
def _sync_runs(db, tenant_id: int, client_id: int):
runs=list(db.execute(select(AccountingHistoricalLearningRun).where(AccountingHistoricalLearningRun.tenant_id==tenant_id, AccountingHistoricalLearningRun.client_id==client_id).order_by(AccountingHistoricalLearningRun.id.desc()).limit(10)).scalars().all())
for run in runs:
if run.status not in {"completed","failed"}: ingest_completed_run(db, run)
return runs
@router.get("")
def historical_learning(request: Request, client_id: int | None=None, collected: int=0, mapped: 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)
runs=[]; evid=[]; ledgers=[]; suggestions=[]; mappings=[]
workstations=eligible_tally_workstations(db, scope.tenant_id, scope.branch_id)
if selected:
runs=_sync_runs(db,scope.tenant_id,selected.id)
evid=evidence_rows(db,scope.tenant_id,selected.id)
ledgers=classifiable_ledgers(db,scope.tenant_id,selected.id)
suggestions=party_suggestions(db,scope.tenant_id,selected.id)
mappings=ledger_mappings(db,scope.tenant_id,selected.id)
df,dt=_fy_dates()
return templates.TemplateResponse("modules/accounting/templates/accounting/historical_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":"Historical Tally Learning","clients":clients,"selected_client":selected,"workstations":workstations,"workstation_companies":{w.id:workstation_companies(w) for w in workstations},
"runs":runs,"latest_run":runs[0] if runs else None,"evidence":evid,"classifiable_ledgers":ledgers,"suggestions":suggestions,"mappings":mappings,"natures":active_natures(db,scope.tenant_id),
"date_from":df,"date_to":dt,"collected":bool(collected),"mapped":bool(mapped),"error":error,
})
finally: db.close()
@router.post("/collect")
def collect(request: Request, client_id: int=Form(...), workstation_id: int=Form(...), tally_guid: str=Form(...), company_name: str=Form(""), date_from: str=Form(""), date_to: 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:return _denied()
guid=str(tally_guid or "").strip()
if not guid:
return RedirectResponse(url=f"/tools/accounting/historical-learning?client_id={client_id}&error=company_required",status_code=303)
workstation, company = resolve_workstation_company(
db,
workstation_id=workstation_id,
tenant_id=scope.tenant_id,
branch_id=scope.branch_id,
tally_guid=guid,
)
payload=_accounting_storage_payload(client); payload.update({
"tenant_id":scope.tenant_id,
"requested_by_user_id":user.id,
"tally_guid":guid,
"company_name":str(company.get("name") or company_name or "").strip(),
"date_from":date_from,
"date_to":date_to,
"voucher_scope":"purchase",
"refresh_transactions":True,
})
job=enqueue_agent_job(db,workstation_agent_id=workstation.id,action="accounting_historical_evidence",payload=payload,priority=6,max_attempts=2,created_by_user_id=user.id)
db.flush()
run=AccountingHistoricalLearningRun(tenant_id=scope.tenant_id,client_id=client.id,workstation_agent_id=workstation.id,agent_job_id=job.id,tally_guid=guid,company_name=str(company.get("name") or company_name or ""),date_from=date_from or None,date_to=date_to or None,status="queued",requested_by_user_id=user.id)
db.add(run); db.commit()
return RedirectResponse(url=f"/tools/accounting/historical-learning?client_id={client_id}&collected=1",status_code=303)
except Exception as exc:
db.rollback(); return RedirectResponse(url=f"/tools/accounting/historical-learning?client_id={client_id}&error={str(exc)[:120]}",status_code=303)
finally: db.close()
@router.post("/map-ledger")
def map_ledger(request: Request, client_id: int=Form(...), tally_guid: str=Form(...), company_name: str=Form(""), ledger_name: str=Form(...), parent_group: str=Form(""), nature_id: int=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:return _denied()
save_ledger_mapping(db,tenant_id=scope.tenant_id,client_id=client.id,tally_guid=tally_guid,company_name=company_name,ledger_name=ledger_name,parent_group=parent_group,nature_id=nature_id,user_id=user.id)
return RedirectResponse(url=f"/tools/accounting/historical-learning?client_id={client_id}&mapped=1",status_code=303)
finally: db.close()
@router.post("/unmap-ledger")
def unmap_ledger(request: Request, client_id: int=Form(...), mapping_id: int=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:return _denied()
remove_ledger_mapping(db,tenant_id=scope.tenant_id,client_id=client.id,mapping_id=mapping_id)
return RedirectResponse(url=f"/tools/accounting/historical-learning?client_id={client_id}",status_code=303)
finally: db.close()
@router.get("/run/{run_id}/status")
def run_status(request: Request, run_id: int):
db = CommonSessionLocal()
try:
user, response = _require_partner(request, db, "accounting.learning.view")
if response:
return JSONResponse({"ok": False, "error": "Access denied."}, status_code=403)
clients, scope = _visible_clients(db, request, user)
visible_client_ids = {int(c.id) for c in clients}
run = db.get(AccountingHistoricalLearningRun, int(run_id))
if (
not run
or int(run.tenant_id) != int(scope.tenant_id)
or int(run.client_id) not in visible_client_ids
):
return JSONResponse({"ok": False, "error": "Historical Purchase run not found."}, status_code=404)
if run.status not in {"completed", "failed"}:
ingest_completed_run(db, run)
return JSONResponse({"ok": True, **historical_run_status(db, run)})
finally:
db.close()