Add Phase 20 stored bank reuse and richer reconciliation

This commit is contained in:
A R R R Associates
2026-08-24 18:58:58 +05:30
parent 769add8cb8
commit b8fbaf55ec
12 changed files with 1419 additions and 78 deletions
@@ -11,9 +11,14 @@ 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,
)
@@ -91,6 +96,9 @@ def page(
runs = []
items = []
selected_run = None
accounts = []
mappings = []
run_summaries = {}
if selected:
jobs = completed_client_jobs(
@@ -99,6 +107,13 @@ def page(
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"]
@@ -109,6 +124,12 @@ def page(
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,
@@ -136,6 +157,12 @@ def page(
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",
{
@@ -157,6 +184,9 @@ def page(
"selected_run": selected_run,
"items": items,
"status_filter": status,
"source_accounts": accounts,
"bank_mappings": mappings,
"run_summaries": run_summaries,
"message": message,
"error": error,
},
@@ -173,6 +203,8 @@ def create_run(
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(...),
):
@@ -197,6 +229,8 @@ def create_run(
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,
@@ -216,3 +250,153 @@ def create_run(
)
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()