Add client acceptance controls for AQMM and peer review workflow

This commit is contained in:
A R R R Associates
2026-07-06 18:37:34 +05:30
parent 561de117e4
commit aa32579084
10 changed files with 604 additions and 5 deletions
+127 -2
View File
@@ -10,7 +10,7 @@ from app.core.security.session_auth import get_current_user
from app.core.templating import templates
from app.modules.clients import repository
from app.modules.clients.access import build_scope, can_view_client_row
from app.modules.clients.constants import CLIENT_CATEGORY_OPTIONS, CLIENT_STATUS, CLIENT_TYPES, RISK_CATEGORIES
from app.modules.clients.constants import CLIENT_ACCEPTANCE_STATUS, CLIENT_CATEGORY_OPTIONS, CLIENT_STATUS, CLIENT_TYPES, RISK_CATEGORIES
from app.modules.clients.filters import ClientListFilters
from app.modules.clients.import_service import (
build_client_import_template_bytes,
@@ -22,6 +22,7 @@ from app.modules.clients.import_service import (
from app.modules.clients.schemas import ClientCreate, ClientUpdate
from app.modules.clients.service import (
activate_client_service,
approve_client_acceptance_service,
archive_client_service,
create_client_service,
deactivate_client_service,
@@ -29,7 +30,9 @@ from app.modules.clients.service import (
get_client_or_404,
list_client_audit_logs,
list_clients_payload,
reject_client_acceptance_service,
restore_client_service,
mark_client_acceptance_pending_service,
update_client_service,
update_client_self_profile_service,
)
@@ -77,6 +80,7 @@ def _base_ctx(request: Request, user, db, **ctx):
"client_statuses": CLIENT_STATUS,
"client_categories": CLIENT_CATEGORY_OPTIONS,
"risk_categories": RISK_CATEGORIES,
"client_acceptance_statuses": CLIENT_ACCEPTANCE_STATUS,
}
base.update(ctx)
return base
@@ -179,6 +183,15 @@ def _build_form_payload(request: Request, user, scope, *, include_client_code: b
"risk_category": form.get("risk_category"),
"onboarding_date": form.get("onboarding_date") or None,
"closing_date": form.get("closing_date") or None,
"acceptance_status": form.get("acceptance_status") or "pending_review",
"acceptance_required": _form_bool(form.get("acceptance_required")),
"independence_check_completed": _form_bool(form.get("independence_check_completed")),
"conflict_check_completed": _form_bool(form.get("conflict_check_completed")),
"kyc_completed": _form_bool(form.get("kyc_completed")),
"engagement_letter_required": _form_bool(form.get("engagement_letter_required")),
"engagement_letter_received": _form_bool(form.get("engagement_letter_received")),
"acceptance_review_notes": form.get("acceptance_review_notes"),
"acceptance_rejection_reason": form.get("acceptance_rejection_reason"),
"notes": form.get("notes"),
"gst_applicable": _form_bool(form.get("gst_applicable")),
"income_tax_applicable": _form_bool(form.get("income_tax_applicable")),
@@ -535,10 +548,13 @@ def client_new_page(request: Request):
"status": "active",
"client_type": "Other",
"country": "India",
"engagement_mode": "internal_managed",
"engagement_mode": "hybrid" if form_mode == "firm_admin" else "internal_managed",
"partner_id": scope.locked_partner_id or getattr(user, "id", None),
"branch_id": scope.branch_id,
"tenant_id": scope.tenant_id,
"acceptance_status": "pending_review",
"acceptance_required": True,
"engagement_letter_required": True,
}
return _render(
@@ -645,6 +661,8 @@ def client_detail(request: Request, client_id: int):
can_activate=has("clients.activate"),
can_archive=has("clients.archive"),
can_restore=has("clients.restore"),
can_manage_acceptance=has("clients.acceptance.manage"),
can_approve_acceptance=has("clients.acceptance.approve"),
)
finally:
db.close()
@@ -757,6 +775,113 @@ async def client_update(request: Request, client_id: int):
db.close()
@router.post("/{client_id}/acceptance/approve")
async def client_acceptance_approve(request: Request, client_id: int):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
has = _has_perm_factory(db, user)
if not has("clients.acceptance.approve"):
return _redirect_denied()
role_names = _role_names(db, user)
scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names)
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
row = get_client_or_404(
db,
client_id=client_id,
tenant_id=scope.tenant_id,
branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients,
)
try:
row = approve_client_acceptance_service(db, row=row, actor_user_id=user.id, review_notes=form.get("acceptance_review_notes"))
return RedirectResponse(url=f"/clients/{row.id}", status_code=303)
except Exception as exc:
return _render(
request,
"modules/clients/templates/clients/detail.html",
db,
user,
title=f"Client • {row.client_name}",
row=repository.get_client_detail_payload(db, client_id),
audit_logs=list_client_audit_logs(db, row=type("Tmp", (), {"id": client_id})(), limit=10) if has("clients.audit_log.view") else [],
scope=scope,
can_edit=has("clients.edit"),
can_deactivate=has("clients.deactivate"),
can_activate=has("clients.activate"),
can_archive=has("clients.archive"),
can_restore=has("clients.restore"),
can_manage_acceptance=has("clients.acceptance.manage"),
can_approve_acceptance=has("clients.acceptance.approve"),
form_errors=_field_errors(exc),
)
finally:
db.close()
@router.post("/{client_id}/acceptance/reject")
async def client_acceptance_reject(request: Request, client_id: int):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
has = _has_perm_factory(db, user)
if not has("clients.acceptance.approve"):
return _redirect_denied()
role_names = _role_names(db, user)
scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names)
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
row = get_client_or_404(
db,
client_id=client_id,
tenant_id=scope.tenant_id,
branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients,
)
row = reject_client_acceptance_service(db, row=row, actor_user_id=user.id, rejection_reason=form.get("acceptance_rejection_reason"))
return RedirectResponse(url=f"/clients/{row.id}", status_code=303)
finally:
db.close()
@router.post("/{client_id}/acceptance/pending")
async def client_acceptance_pending(request: Request, client_id: int):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
has = _has_perm_factory(db, user)
if not has("clients.acceptance.manage"):
return _redirect_denied()
role_names = _role_names(db, user)
scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names)
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
row = get_client_or_404(
db,
client_id=client_id,
tenant_id=scope.tenant_id,
branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients,
)
row = mark_client_acceptance_pending_service(db, row=row, actor_user_id=user.id, review_notes=form.get("acceptance_review_notes"))
return RedirectResponse(url=f"/clients/{row.id}", status_code=303)
finally:
db.close()
@router.post("/{client_id}/deactivate")
async def client_deactivate(request: Request, client_id: int):
db = CommonSessionLocal()