Files
arrr-erp/app/modules/accounting/bank_ui.py
T
2026-08-22 21:52:44 +05:30

192 lines
8.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.bank_service import (
ai_assist_transaction, confirm_review, import_completed_job, internal_model_predict_transaction, preflight_choices, queue_post, queue_preflight,
queue_rows, sync_posting, visible_workstations,
)
from app.modules.accounting.ledger_learning_service import active_natures
from app.modules.accounting.ui import _find_visible_client, _require_partner, _visible_clients
from app.modules.bank_statement_analyzer.models import BankStatementAnalysisJob
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from sqlalchemy import select
router = APIRouter(prefix="/tools/accounting/bank-posting", tags=["accounting-bank-posting-ui"])
def _go(client_id=0, message="", error=""):
q = {"client_id": client_id} if client_id else {}
if message: q["message"] = message[:250]
if error: q["error"] = error[:250]
return RedirectResponse("/tools/accounting/bank-posting" + ("?" + urlencode(q) if q else ""), status_code=303)
@router.get("")
def page(request: Request, client_id: int | None = None, status: 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((c for c in clients if client_id and int(c.id) == int(client_id)), None)
rows = []; total = 0; pages = 1
choices = {}; workstations = []
if selected:
rows, total, page, pages = queue_rows(db, tenant_id=scope.tenant_id, client_id=selected.id, status=status, page=page, per_page=per_page)
for row in rows:
sync_posting(db, row)
choices = {r.id: preflight_choices(r) for r in rows if r.preflight_result_json}
workstations = visible_workstations(db, scope.tenant_id, getattr(user, "branch_id", None))
jobs = list(db.execute(select(BankStatementAnalysisJob).where(
BankStatementAnalysisJob.user_id == user.id,
BankStatementAnalysisJob.status == "completed",
).order_by(BankStatementAnalysisJob.completed_at_utc.desc()).limit(30)).scalars().all())
return templates.TemplateResponse("modules/accounting/templates/accounting/bank_posting.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": "Bank to Tally", "clients": clients, "selected_client": selected,
"rows": rows, "total": total, "page": page, "pages": pages, "per_page": per_page,
"status_filter": status, "jobs": jobs, "natures": active_natures(db, scope.tenant_id),
"choices": choices, "workstations": workstations, "message": message, "error": error,
})
finally:
db.close()
@router.post("/import")
def import_job(request: Request, client_id: int = Form(...), job_id: str = Form(...), 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.")
added, skipped = import_completed_job(db, tenant_id=scope.tenant_id, client_id=client.id, job_id=job_id, user_id=user.id)
return _go(client.id, message=f"Imported {added} bank transaction(s); {skipped} duplicate fingerprint(s) skipped.")
except Exception as exc:
db.rollback(); return _go(client_id, error=str(exc))
finally: db.close()
@router.post("/{tx_id}/review")
def review(request: Request, tx_id: int, client_id: int = Form(...), nature_id: int | None = Form(None), ledger_name: str = Form(""), voucher_type: str = Form(...), review_note: str = Form(""), 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)
confirm_review(db, tx_id=tx_id, tenant_id=scope.tenant_id, client_id=client.id, nature_id=nature_id, ledger_name=ledger_name, voucher_type=voucher_type, user_id=user.id, note=review_note)
return _go(client.id, message="Bank transaction review saved.")
except Exception as exc:
db.rollback(); return _go(client_id, error=str(exc))
finally: db.close()
@router.post("/{tx_id}/preflight")
def preflight(request: Request, tx_id: int, client_id: int = Form(...), workstation_id: int = Form(...), 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)
queue_preflight(db, tx_id=tx_id, tenant_id=scope.tenant_id, client_id=client.id, workstation_id=workstation_id, user_id=user.id)
return _go(client.id, message="Bank posting preflight queued.")
except Exception as exc:
db.rollback(); return _go(client_id, error=str(exc))
finally: db.close()
@router.post("/{tx_id}/post")
def post(request: Request, tx_id: int, client_id: int = Form(...), bank_ledger_name: str = Form(...), counter_ledger_name: str = Form(""), other_bank_ledger_name: str = Form(""), 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)
queue_post(db, tx_id=tx_id, tenant_id=scope.tenant_id, client_id=client.id, bank_ledger_name=bank_ledger_name, counter_ledger_name=counter_ledger_name, other_bank_ledger_name=other_bank_ledger_name, user_id=user.id)
return _go(client.id, message="Controlled bank voucher job queued.")
except Exception as exc:
db.rollback(); return _go(client_id, error=str(exc))
finally: db.close()
@router.post("/{tx_id}/ai-assist")
def ai_assist(
request: Request,
tx_id: int,
client_id: int = Form(...),
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.")
decision = ai_assist_transaction(
db,
tx_id=tx_id,
tenant_id=scope.tenant_id,
client_id=client.id,
user_id=user.id,
)
return _go(
client.id,
message=f"AI semantic fallback completed at {decision.combined_confidence}% combined confidence.",
)
except Exception as exc:
db.rollback()
return _go(client_id, error=str(exc))
finally:
db.close()
@router.post("/{tx_id}/internal-predict")
def internal_predict(
request: Request,
tx_id: int,
client_id: int = Form(...),
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.")
prediction = internal_model_predict_transaction(
db,
tx_id=tx_id,
tenant_id=scope.tenant_id,
client_id=client.id,
force_shadow=True,
)
return _go(
client.id,
message=f"Internal model shadow prediction recorded at {prediction.predicted_probability * 100:.1f}%.",
)
except Exception as exc:
db.rollback()
return _go(client_id, error=str(exc))
finally:
db.close()