403 lines
12 KiB
Python
403 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
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_reconciliation_service import (
|
|
completed_client_jobs,
|
|
list_bank_mappings,
|
|
list_runs,
|
|
queue_all_mapped_reconciliations,
|
|
queue_reconciliation,
|
|
resolve_reconciliation_item,
|
|
run_items,
|
|
save_bank_mapping,
|
|
source_accounts,
|
|
visible_bank_ledgers,
|
|
visible_workstations,
|
|
)
|
|
from app.modules.accounting.chart_models import AccountingChartLedger
|
|
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
|
|
from sqlalchemy import select
|
|
|
|
router = APIRouter(
|
|
prefix="/tools/accounting/bank-reconciliation",
|
|
tags=["accounting-bank-reconciliation-ui"],
|
|
)
|
|
|
|
|
|
def _go(client_id=0, message="", error="", **extra):
|
|
params = {}
|
|
if client_id:
|
|
params["client_id"] = client_id
|
|
if message:
|
|
params["message"] = message[:350]
|
|
if error:
|
|
params["error"] = error[:350]
|
|
for key, value in extra.items():
|
|
if value not in ("", None):
|
|
params[key] = value
|
|
return RedirectResponse(
|
|
"/tools/accounting/bank-reconciliation"
|
|
+ ("?" + urlencode(params) if params else ""),
|
|
status_code=303,
|
|
)
|
|
|
|
|
|
def _company_options(db, tenant_id, client_id):
|
|
rows = list(
|
|
db.execute(
|
|
select(AccountingChartLedger.tally_guid, AccountingChartLedger.company_name)
|
|
.where(
|
|
AccountingChartLedger.tenant_id == int(tenant_id),
|
|
AccountingChartLedger.client_id == int(client_id),
|
|
)
|
|
.distinct()
|
|
.order_by(AccountingChartLedger.company_name)
|
|
).all()
|
|
)
|
|
return [{"guid": guid, "name": name or guid} for guid, name in rows if guid]
|
|
|
|
|
|
@router.get("")
|
|
def page(
|
|
request: Request,
|
|
client_id: int | None = None,
|
|
job_id: str = "",
|
|
tally_guid: str = "",
|
|
run_id: int | None = None,
|
|
status: str = "",
|
|
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,
|
|
)
|
|
|
|
jobs = []
|
|
companies = []
|
|
bank_ledgers = []
|
|
workstations = []
|
|
runs = []
|
|
items = []
|
|
selected_run = None
|
|
accounts = []
|
|
mappings = []
|
|
run_summaries = {}
|
|
|
|
if selected:
|
|
jobs = completed_client_jobs(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=selected.id,
|
|
limit=50,
|
|
)
|
|
if job_id:
|
|
accounts = source_accounts(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=selected.id,
|
|
job_id=job_id,
|
|
)
|
|
companies = _company_options(db, scope.tenant_id, selected.id)
|
|
if not tally_guid and len(companies) == 1:
|
|
tally_guid = companies[0]["guid"]
|
|
if tally_guid:
|
|
bank_ledgers = visible_bank_ledgers(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=selected.id,
|
|
tally_guid=tally_guid,
|
|
)
|
|
mappings = list_bank_mappings(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=selected.id,
|
|
tally_guid=tally_guid,
|
|
)
|
|
workstations = visible_workstations(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
branch_id=getattr(user, "branch_id", None),
|
|
)
|
|
runs = list_runs(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=selected.id,
|
|
limit=30,
|
|
)
|
|
|
|
if run_id:
|
|
selected_run = next(
|
|
(row for row in runs if int(row.id) == int(run_id)),
|
|
None,
|
|
)
|
|
elif runs:
|
|
selected_run = runs[0]
|
|
|
|
if selected_run and selected_run.status == "completed":
|
|
items = run_items(
|
|
db,
|
|
run_id=selected_run.id,
|
|
status=status,
|
|
)
|
|
|
|
for row in runs:
|
|
try:
|
|
run_summaries[int(row.id)] = json.loads(row.summary_json or "{}")
|
|
except Exception:
|
|
run_summaries[int(row.id)] = {}
|
|
|
|
return templates.TemplateResponse(
|
|
"modules/accounting/templates/accounting/bank_reconciliation.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 Reconciliation",
|
|
"clients": clients,
|
|
"selected_client": selected,
|
|
"jobs": jobs,
|
|
"selected_job_id": job_id,
|
|
"companies": companies,
|
|
"selected_tally_guid": tally_guid,
|
|
"bank_ledgers": bank_ledgers,
|
|
"workstations": workstations,
|
|
"runs": runs,
|
|
"selected_run": selected_run,
|
|
"items": items,
|
|
"status_filter": status,
|
|
"source_accounts": accounts,
|
|
"bank_mappings": mappings,
|
|
"run_summaries": run_summaries,
|
|
"message": message,
|
|
"error": error,
|
|
},
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/run")
|
|
def create_run(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
source_job_id: str = Form(...),
|
|
tally_guid: str = Form(...),
|
|
company_name: str = Form(...),
|
|
bank_ledger_name: str = Form(...),
|
|
account_number: str = Form(""),
|
|
date_tolerance_days: int = Form(15),
|
|
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, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _go(error="Client is not visible.")
|
|
|
|
run = queue_reconciliation(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
source_job_id=source_job_id,
|
|
tally_guid=tally_guid,
|
|
company_name=company_name,
|
|
bank_ledger_name=bank_ledger_name,
|
|
workstation_id=workstation_id,
|
|
user_id=user.id,
|
|
account_number=account_number,
|
|
date_tolerance_days=date_tolerance_days,
|
|
)
|
|
return _go(
|
|
client.id,
|
|
run_id=run.id,
|
|
tally_guid=tally_guid,
|
|
message=(
|
|
f"Bank reconciliation run #{run.id} queued. "
|
|
"The Local Agent will read the selected Tally bank ledger; no voucher is posted."
|
|
),
|
|
)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(
|
|
client_id,
|
|
tally_guid=tally_guid,
|
|
error=str(exc),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/mapping")
|
|
def save_mapping(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
source_job_id: str = Form(""),
|
|
tally_guid: str = Form(...),
|
|
company_name: str = Form(...),
|
|
account_number: str = Form(...),
|
|
bank_name: str = Form(""),
|
|
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, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _go(error="Client is not visible.")
|
|
|
|
save_bank_mapping(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
account_number=account_number,
|
|
bank_name=bank_name,
|
|
tally_guid=tally_guid,
|
|
company_name=company_name,
|
|
bank_ledger_name=bank_ledger_name,
|
|
user_id=user.id,
|
|
)
|
|
return _go(
|
|
client.id,
|
|
job_id=source_job_id,
|
|
tally_guid=tally_guid,
|
|
message=f"Bank account {account_number} mapped to Tally ledger '{bank_ledger_name}'.",
|
|
)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, job_id=source_job_id, tally_guid=tally_guid, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/run-all")
|
|
def create_all_runs(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
source_job_id: str = Form(...),
|
|
tally_guid: str = Form(...),
|
|
company_name: str = Form(...),
|
|
date_tolerance_days: int = Form(15),
|
|
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, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _go(error="Client is not visible.")
|
|
|
|
runs = queue_all_mapped_reconciliations(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
source_job_id=source_job_id,
|
|
tally_guid=tally_guid,
|
|
company_name=company_name,
|
|
workstation_id=workstation_id,
|
|
user_id=user.id,
|
|
date_tolerance_days=date_tolerance_days,
|
|
)
|
|
return _go(
|
|
client.id,
|
|
job_id=source_job_id,
|
|
tally_guid=tally_guid,
|
|
run_id=(runs[0].id if runs else None),
|
|
message=f"Queued {len(runs)} reconciliation run(s), one for each mapped bank account.",
|
|
)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, job_id=source_job_id, tally_guid=tally_guid, error=str(exc))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/runs/{run_id}/items/{item_id}/resolve")
|
|
def resolve_item(
|
|
request: Request,
|
|
run_id: int,
|
|
item_id: int,
|
|
client_id: int = Form(...),
|
|
tally_guid: str = Form(""),
|
|
action: str = Form(...),
|
|
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, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _go(error="Client is not visible.")
|
|
|
|
run = next(
|
|
(
|
|
row
|
|
for row in list_runs(
|
|
db,
|
|
tenant_id=scope.tenant_id,
|
|
client_id=client.id,
|
|
limit=100,
|
|
)
|
|
if int(row.id) == int(run_id)
|
|
),
|
|
None,
|
|
)
|
|
if not run:
|
|
raise ValueError("Reconciliation run was not found.")
|
|
|
|
resolve_reconciliation_item(
|
|
db,
|
|
run_id=run.id,
|
|
item_id=item_id,
|
|
action=action,
|
|
note=note,
|
|
user_id=user.id,
|
|
)
|
|
return _go(
|
|
client.id,
|
|
tally_guid=(tally_guid or run.tally_guid),
|
|
run_id=run.id,
|
|
message="Reconciliation review decision saved.",
|
|
)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return _go(client_id, tally_guid=tally_guid, run_id=run_id, error=str(exc))
|
|
finally:
|
|
db.close()
|