from __future__ import annotations import json from datetime import date, datetime, timezone 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.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.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,"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()