Add Phase 11 bank to Tally with multi-bank contra intelligence

This commit is contained in:
A R R R Associates
2026-08-22 19:59:40 +05:30
parent 215ad2c6c6
commit 8748ee9539
16 changed files with 1140 additions and 28 deletions
+123
View File
@@ -0,0 +1,123 @@
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 (
confirm_review, import_completed_job, 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()