Files
arrr-erp/app/modules/services/engagements_ui.py
T
2026-08-04 15:01:44 +05:30

1072 lines
42 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Form, Request
from fastapi.responses import RedirectResponse
from sqlalchemy import select
from app.core.db.common import CommonSessionLocal
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
from app.core.security.session_auth import get_current_user
from app.core.templating import templates
from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
from app.modules.services.execution import (
aqmm_task_summary_for_subscription,
update_engagement_closure_from_sources,
save_engagement_closure_confirmations,
approve_engagement_closure,
reopen_engagement_closure,
)
from app.modules.clients.models import Client
from app.modules.core.iam.models import User
from app.modules.services.client_services import (
SUBSCRIPTION_STATUSES,
assessment_year_from_financial_year,
current_financial_year,
get_enabled_firm_service,
get_existing_subscription,
get_subscription,
normalize_financial_year,
list_assignable_users,
list_clients_for_assignment,
list_enabled_services_for_assignment,
list_subscription_payload,
list_review_partners,
parse_date,
review_partner_required_for_engagement,
ensure_engagement_quality_workflow,
enforce_quality_gate_on_subscription,
request_engagement_quality_declarations,
list_engagement_quality_declarations,
respond_engagement_quality_declaration,
verify_engagement_kyc_from_permanent_documents,
get_latest_engagement_kyc_verification,
get_current_engagement_letter,
mark_engagement_letter_completed,
approve_engagement_quality_workflow,
)
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.core.rbac.permission_guard import require_permission
from app.modules.core.tenancy.year_control import redirect_if_financial_year_locked, is_row_financial_year_locked
router = APIRouter(prefix="/services/engagements", tags=["services-engagements-ui"])
def _base_ctx(request: Request, user, db, **ctx):
base = {
"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),
"subscription_statuses": SUBSCRIPTION_STATUSES,
}
base.update(ctx)
return base
def _render(request: Request, template: str, db, user, **ctx):
return templates.TemplateResponse(template, _base_ctx(request, user, db, **ctx))
def _redirect_denied():
from app.core.http_responses import ui_access_denied
return ui_access_denied()
def _has_perm(db, user, code: str) -> bool:
try:
require_permission(db, user, code)
return True
except Exception:
return False
def _active_tenant_id(request: Request, user) -> int:
return int(
request.session.get("active_tenant_id")
or request.session.get("selected_tenant_id")
or request.session.get("tenant_id")
or user.tenant_id
)
def _active_branch_id(request: Request, user, db) -> int | None:
value = request.session.get("active_branch_id")
if value in (None, "", 0, "0"):
if _has_perm(db, user, "clients.cross_branch"):
return None
return int(getattr(user, "branch_id", 0) or 0) or None
return int(value)
def _active_financial_year(request: Request) -> str:
return normalize_financial_year(
request.session.get("active_financial_year")
or getattr(request.state, "year_code", None)
)
def _locked_partner_id(db, user) -> int | None:
return int(user.id) if _has_perm(db, user, "clients.view.own_only") else None
def _can_manage_client_services(db, user) -> bool:
return _has_perm(db, user, "clients.edit")
def _can_lock_engagements(db, user) -> bool:
roles = set(get_user_roles(db, user.id))
return bool(roles.intersection({"Firm Admin", "Partner"}))
def _user_can_lock_subscription(db, user, row: ClientServiceSubscription) -> bool:
roles = set(get_user_roles(db, user.id))
if "Firm Admin" in roles:
return True
if "Partner" in roles:
client = getattr(row, "client", None)
return (
getattr(row, "assigned_partner_user_id", None) == user.id
or getattr(client, "partner_id", None) == user.id
)
return False
def _lock_subscription_row(row: ClientServiceSubscription, user) -> bool:
if getattr(row, "is_locked", False):
return False
from datetime import datetime, timezone
row.is_locked = True
row.status = "completed" if row.status == "active" else row.status
row.locked_at_utc = datetime.now(timezone.utc)
row.locked_by_user_id = user.id
row.updated_by_user_id = user.id
return True
@router.get("")
def subscription_list(
request: Request,
q: str = "",
financial_year: str = "",
include_inactive: bool = True,
locked: int = 0,
skipped: int = 0,
bulk_created: int = 0,
bulk_existing: int = 0,
bulk_skipped: int = 0,
):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.view")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
branch_id = _active_branch_id(request, user, db)
selected_financial_year = normalize_financial_year(financial_year or _active_financial_year(request))
rows = list_subscription_payload(
db,
tenant_id=tenant_id,
branch_id=branch_id,
financial_year=selected_financial_year,
q=q,
include_inactive=include_inactive,
)
return _render(
request,
"modules/services/templates/services/engagements/list.html",
db,
user,
title="Engagement Subscriptions",
rows=rows,
q=q,
financial_year=selected_financial_year,
include_inactive=include_inactive,
locked_count=locked,
skipped_count=skipped,
bulk_created_count=bulk_created,
bulk_existing_count=bulk_existing,
bulk_skipped_count=bulk_skipped,
can_manage=_can_manage_client_services(db, user),
can_lock_engagements=_can_lock_engagements(db, user),
)
finally:
db.close()
@router.get("/new")
def subscription_create_page(request: Request, client_id: int | None = None):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
branch_id = _active_branch_id(request, user, db)
clients = list_clients_for_assignment(
db,
tenant_id=tenant_id,
branch_id=branch_id,
partner_id=_locked_partner_id(db, user),
)
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
assignable_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=branch_id)
return _render(
request,
"modules/services/templates/services/engagements/form.html",
db,
user,
title="Assign Service to Client",
mode="create",
subscription=None,
clients=clients,
enabled_services=enabled_services,
assignable_users=assignable_users,
review_partners=review_partners,
selected_client_id=client_id,
financial_year=_active_financial_year(request),
)
finally:
db.close()
@router.post("/new")
def subscription_create_submit(
request: Request,
client_id: int = Form(...),
service_catalogue_id: int = Form(...),
assigned_partner_user_id: str = Form(""),
assigned_manager_user_id: str = Form(""),
assigned_staff_user_id: str = Form(""),
review_partner_user_id: str = Form(""),
financial_year: str = Form(""),
start_date: str = Form(""),
end_date: str = Form(""),
expiry_date: str = Form(""),
status: str = Form("active"),
remarks: str = Form(""),
is_active: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
selected_financial_year = normalize_financial_year(financial_year or _active_financial_year(request))
locked_response = redirect_if_financial_year_locked(db, tenant_id=tenant_id, year_code=selected_financial_year, redirect_url=f"/services/engagements?financial_year={selected_financial_year}")
if locked_response:
return locked_response
firm_selection = get_enabled_firm_service(db, tenant_id=tenant_id, service_catalogue_id=service_catalogue_id)
if not firm_selection:
return RedirectResponse(url="/services/engagements/new", status_code=303)
existing = get_existing_subscription(
db,
tenant_id=tenant_id,
client_id=client_id,
service_catalogue_id=service_catalogue_id,
financial_year=selected_financial_year,
)
client = db.get(Client, client_id)
if not client or client.tenant_id != tenant_id:
return RedirectResponse(url="/services/engagements/new", status_code=303)
if existing:
row = existing
else:
row = ClientServiceSubscription(
tenant_id=tenant_id,
client_id=client_id,
service_catalogue_id=service_catalogue_id,
financial_year=selected_financial_year,
assessment_year=assessment_year_from_financial_year(selected_financial_year),
engagement_type=getattr(firm_selection.catalogue, "engagement_type", "non_assurance") or "non_assurance",
created_by_user_id=user.id,
)
db.add(row)
if getattr(row, "is_locked", False) or is_row_financial_year_locked(db, row):
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
row.financial_year = selected_financial_year
row.assessment_year = assessment_year_from_financial_year(selected_financial_year)
if not getattr(row, "engagement_type", None):
row.engagement_type = getattr(firm_selection.catalogue, "engagement_type", "non_assurance") or "non_assurance"
row.branch_id = firm_selection.default_branch_id or getattr(user, "branch_id", None)
row.firm_service_selection_id = firm_selection.id
row.assigned_partner_user_id = int(assigned_partner_user_id) if assigned_partner_user_id.strip() else None
row.assigned_manager_user_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
row.assigned_staff_user_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
if review_partner_required_for_engagement(db, tenant_id=tenant_id, engagement_type=row.engagement_type):
row.review_partner_user_id = int(review_partner_user_id) if review_partner_user_id.strip() else getattr(client, "default_review_partner_user_id", None)
else:
row.review_partner_user_id = None
row.start_date = parse_date(start_date)
row.end_date = parse_date(end_date)
row.expiry_date = parse_date(expiry_date)
row.status = status or "active"
row.remarks = remarks.strip() or None
row.is_active = is_active is not None
row.updated_by_user_id = user.id
apply_due_date_rule_to_subscription(db, row)
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False)
enforce_quality_gate_on_subscription(row)
db.commit()
db.refresh(row)
return RedirectResponse(url=f"/services/engagements/{row.id}", status_code=303)
finally:
db.close()
@router.get("/bulk-new")
def subscription_bulk_create_page(request: Request, error: str = ""):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
branch_id = _active_branch_id(request, user, db)
clients = list_clients_for_assignment(
db,
tenant_id=tenant_id,
branch_id=branch_id,
partner_id=_locked_partner_id(db, user),
)
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
partners = list_assignable_users(db, tenant_id=tenant_id, role_names=("Partner",))
managers = list_assignable_users(db, tenant_id=tenant_id, role_names=("Branch Manager",))
staff_users = list_assignable_users(db, tenant_id=tenant_id, role_names=("Staff",))
review_partners = list_review_partners(db, tenant_id=tenant_id)
return _render(
request,
"modules/services/templates/services/engagements/bulk_form.html",
db,
user,
title="Bulk Engagement Setup",
clients=clients,
enabled_services=enabled_services,
partners=partners,
client_partner_names={row.id: (row.full_name or row.email) for row in partners},
managers=managers,
staff_users=staff_users,
review_partners=review_partners,
financial_year=_active_financial_year(request),
error_message={
"service": "Select a valid enabled firm service.",
"partner": "Select a valid active Partner.",
"manager": "The selected Manager is not available for this firm.",
"staff": "The selected Staff member is not available for this firm.",
"review_partner": "The selected Review Partner is not available for this firm.",
"clients": "Select at least one permitted client.",
}.get(error),
)
finally:
db.close()
@router.post("/bulk-new")
def subscription_bulk_create_submit(
request: Request,
client_ids: list[int] = Form([]),
service_catalogue_id: int = Form(...),
assigned_partner_user_id: int = Form(...),
assigned_manager_user_id: str = Form(""),
assigned_staff_user_id: str = Form(""),
review_partner_user_id: str = Form(""),
financial_year: str = Form(""),
remarks: str = Form(""),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
created_count = 0
existing_count = 0
skipped_count = 0
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
selected_financial_year = normalize_financial_year(financial_year or _active_financial_year(request))
locked_response = redirect_if_financial_year_locked(
db,
tenant_id=tenant_id,
year_code=selected_financial_year,
redirect_url=f"/services/engagements?financial_year={selected_financial_year}",
)
if locked_response:
return locked_response
firm_selection = get_enabled_firm_service(
db,
tenant_id=tenant_id,
service_catalogue_id=service_catalogue_id,
)
if not firm_selection:
return RedirectResponse(url="/services/engagements/bulk-new?error=service", status_code=303)
partners = list_assignable_users(db, tenant_id=tenant_id, role_names=("Partner",))
managers = list_assignable_users(db, tenant_id=tenant_id, role_names=("Branch Manager",))
staff_users = list_assignable_users(db, tenant_id=tenant_id, role_names=("Staff",))
review_partners = list_review_partners(db, tenant_id=tenant_id)
partner_ids = {row.id for row in partners}
manager_ids = {row.id for row in managers}
staff_ids = {row.id for row in staff_users}
review_partner_ids = {row.id for row in review_partners}
if assigned_partner_user_id not in partner_ids:
return RedirectResponse(url="/services/engagements/bulk-new?error=partner", status_code=303)
manager_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
staff_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
review_partner_id = int(review_partner_user_id) if review_partner_user_id.strip() else None
if manager_id is not None and manager_id not in manager_ids:
return RedirectResponse(url="/services/engagements/bulk-new?error=manager", status_code=303)
if staff_id is not None and staff_id not in staff_ids:
return RedirectResponse(url="/services/engagements/bulk-new?error=staff", status_code=303)
if review_partner_id is not None and review_partner_id not in review_partner_ids:
return RedirectResponse(url="/services/engagements/bulk-new?error=review_partner", status_code=303)
partner = db.get(User, assigned_partner_user_id)
if not partner or partner.tenant_id != tenant_id or not partner.is_active:
return RedirectResponse(url="/services/engagements/bulk-new?error=partner", status_code=303)
engagement_branch_id = partner.branch_id
allowed_clients = list_clients_for_assignment(
db,
tenant_id=tenant_id,
branch_id=_active_branch_id(request, user, db),
partner_id=_locked_partner_id(db, user),
)
allowed_client_ids = {row.id for row in allowed_clients}
selected_client_ids = []
seen_client_ids = set()
for value in client_ids:
client_id = int(value)
if client_id in seen_client_ids:
continue
seen_client_ids.add(client_id)
if client_id in allowed_client_ids:
selected_client_ids.append(client_id)
else:
skipped_count += 1
if not selected_client_ids:
return RedirectResponse(url="/services/engagements/bulk-new?error=clients", status_code=303)
engagement_type = getattr(firm_selection.catalogue, "engagement_type", "non_assurance") or "non_assurance"
requires_review_partner = review_partner_required_for_engagement(
db,
tenant_id=tenant_id,
engagement_type=engagement_type,
)
for client_id in selected_client_ids:
existing = get_existing_subscription(
db,
tenant_id=tenant_id,
client_id=client_id,
service_catalogue_id=service_catalogue_id,
financial_year=selected_financial_year,
)
if existing:
existing_count += 1
continue
client = db.get(Client, client_id)
if not client or client.tenant_id != tenant_id:
skipped_count += 1
continue
row = ClientServiceSubscription(
tenant_id=tenant_id,
branch_id=engagement_branch_id,
client_id=client_id,
service_catalogue_id=service_catalogue_id,
firm_service_selection_id=firm_selection.id,
assigned_partner_user_id=assigned_partner_user_id,
assigned_manager_user_id=manager_id,
assigned_staff_user_id=staff_id,
review_partner_user_id=(
review_partner_id or getattr(client, "default_review_partner_user_id", None)
if requires_review_partner
else None
),
financial_year=selected_financial_year,
assessment_year=assessment_year_from_financial_year(selected_financial_year),
engagement_type=engagement_type,
status="active",
remarks=remarks.strip() or None,
is_active=True,
created_by_user_id=user.id,
updated_by_user_id=user.id,
)
db.add(row)
db.flush()
apply_due_date_rule_to_subscription(db, row)
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False)
enforce_quality_gate_on_subscription(row)
created_count += 1
db.commit()
return RedirectResponse(
url=(
f"/services/engagements?financial_year={selected_financial_year}"
f"&bulk_created={created_count}&bulk_existing={existing_count}&bulk_skipped={skipped_count}"
),
status_code=303,
)
except Exception:
db.rollback()
raise
finally:
db.close()
@router.post("/bulk-lock")
def subscription_bulk_lock(
request: Request,
subscription_ids: list[int] = Form([]),
financial_year: str = Form(""),
q: str = Form(""),
include_inactive: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
locked_count = 0
skipped_count = 0
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
if not _can_lock_engagements(db, user):
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
selected_ids = [int(value) for value in subscription_ids if value]
for subscription_id in selected_ids:
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if not row or not _user_can_lock_subscription(db, user, row):
skipped_count += 1
continue
if _lock_subscription_row(row, user):
locked_count += 1
else:
skipped_count += 1
if locked_count:
db.commit()
params = []
fy = normalize_financial_year(financial_year or _active_financial_year(request))
if fy:
params.append(f"financial_year={fy}")
if q.strip():
from urllib.parse import quote_plus
params.append(f"q={quote_plus(q.strip())}")
if include_inactive:
params.append("include_inactive=true")
params.append(f"locked={locked_count}")
params.append(f"skipped={skipped_count}")
suffix = "?" + "&".join(params) if params else ""
return RedirectResponse(url=f"/services/engagements{suffix}", status_code=303)
finally:
db.close()
@router.get("/{subscription_id}")
def subscription_detail(request: Request, subscription_id: int):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.view")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if not row:
return RedirectResponse(url="/services/engagements", status_code=303)
active_fy = _active_financial_year(request)
if row.financial_year != active_fy:
return RedirectResponse(url=f"/services/engagements?financial_year={active_fy}", status_code=303)
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False)
db.flush()
declarations = list_engagement_quality_declarations(db, subscription_id=row.id)
my_pending_declarations = [d for d in declarations if d.requested_user_id == user.id and d.status == "pending"]
kyc_verification = get_latest_engagement_kyc_verification(db, subscription_id=row.id)
engagement_letter = get_current_engagement_letter(db, subscription_id=row.id)
aqmm_task_summary = aqmm_task_summary_for_subscription(db, subscription_id=row.id)
closure_checklist, closure_summary = update_engagement_closure_from_sources(db, subscription=row, actor_user_id=user.id)
tasks = db.execute(
select(ClientServiceTaskInstance)
.where(ClientServiceTaskInstance.subscription_id == row.id)
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
).scalars().all()
return _render(
request,
"modules/services/templates/services/engagements/detail.html",
db,
user,
title="Engagement Subscription",
row=row,
tasks=tasks,
declarations=declarations,
my_pending_declarations=my_pending_declarations,
kyc_verification=kyc_verification,
engagement_letter=engagement_letter,
aqmm_task_summary=aqmm_task_summary,
closure_checklist=closure_checklist,
closure_summary=closure_summary,
can_manage=_can_manage_client_services(db, user),
)
finally:
db.close()
@router.post("/{subscription_id}/aqmm/initiate")
def subscription_aqmm_initiate(request: Request, subscription_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row:
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=True)
enforce_quality_gate_on_subscription(row)
db.commit()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/aqmm/request-declarations")
def subscription_aqmm_request_declarations(request: Request, subscription_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row:
request_engagement_quality_declarations(db, subscription=row, actor_user_id=user.id)
enforce_quality_gate_on_subscription(row)
db.commit()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/aqmm/declarations/{declaration_id}/respond")
def subscription_aqmm_declaration_respond(
request: Request,
subscription_id: int,
declaration_id: int,
status: str = Form(...),
notes: str = Form(""),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
respond_engagement_quality_declaration(
db,
declaration_id=declaration_id,
current_user_id=user.id,
status=status,
notes=notes,
request=request,
)
db.commit()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/aqmm/kyc/verify")
def subscription_aqmm_kyc_verify(request: Request, subscription_id: int, notes: str = Form(""), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row:
verify_engagement_kyc_from_permanent_documents(db, subscription=row, actor_user_id=user.id, notes=notes)
enforce_quality_gate_on_subscription(row)
db.commit()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/aqmm/engagement-letter/complete")
def subscription_aqmm_engagement_letter_complete(
request: Request,
subscription_id: int,
acceptance_mode: str = Form(...),
notes: str = Form(""),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row:
mark_engagement_letter_completed(db, subscription=row, actor_user_id=user.id, acceptance_mode=acceptance_mode, notes=notes, request=request)
enforce_quality_gate_on_subscription(row)
db.commit()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/aqmm/approve")
def subscription_aqmm_approve(request: Request, subscription_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row:
try:
approve_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id)
db.commit()
except ValueError:
db.rollback()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/closure/sync")
def subscription_closure_sync(request: Request, subscription_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.view")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row:
update_engagement_closure_from_sources(db, subscription=row, actor_user_id=user.id)
db.commit()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/closure/update")
def subscription_closure_update(
request: Request,
subscription_id: int,
deliverables_sent_to_client: str | None = Form(None),
billing_reviewed: str | None = Form(None),
open_points_closed: str | None = Form(None),
client_communication_completed: str | None = Form(None),
closure_note: str = Form(""),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row:
try:
save_engagement_closure_confirmations(
db,
subscription=row,
deliverables_sent_to_client=deliverables_sent_to_client is not None,
billing_reviewed=billing_reviewed is not None,
open_points_closed=open_points_closed is not None,
client_communication_completed=client_communication_completed is not None,
closure_note=closure_note,
actor_user_id=user.id,
)
db.commit()
except ValueError:
db.rollback()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/closure/approve")
def subscription_closure_approve(request: Request, subscription_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
if not _can_lock_engagements(db, user):
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row:
try:
approve_engagement_closure(db, subscription=row, actor_user_id=user.id)
db.commit()
except ValueError:
db.rollback()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/closure/reopen")
def subscription_closure_reopen(
request: Request,
subscription_id: int,
reason: str = Form(""),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
if not _can_lock_engagements(db, user):
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row:
try:
reopen_engagement_closure(db, subscription=row, reason=reason, actor_user_id=user.id)
db.commit()
except ValueError:
db.rollback()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()
@router.get("/{subscription_id}/edit")
def subscription_edit_page(request: Request, subscription_id: int):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
branch_id = _active_branch_id(request, user, db)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if not row:
return RedirectResponse(url="/services/engagements", status_code=303)
active_fy = _active_financial_year(request)
if row.financial_year != active_fy:
return RedirectResponse(url=f"/services/engagements?financial_year={active_fy}", status_code=303)
if getattr(row, "is_locked", False) or is_row_financial_year_locked(db, row):
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
clients = list_clients_for_assignment(
db,
tenant_id=tenant_id,
branch_id=branch_id,
partner_id=_locked_partner_id(db, user),
)
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
assignable_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=branch_id)
return _render(
request,
"modules/services/templates/services/engagements/form.html",
db,
user,
title="Edit Engagement Subscription",
mode="edit",
subscription=row,
clients=clients,
enabled_services=enabled_services,
assignable_users=assignable_users,
review_partners=review_partners,
selected_client_id=row.client_id,
financial_year=row.financial_year,
)
finally:
db.close()
@router.post("/{subscription_id}/edit")
def subscription_edit_submit(
request: Request,
subscription_id: int,
assigned_partner_user_id: str = Form(""),
assigned_manager_user_id: str = Form(""),
assigned_staff_user_id: str = Form(""),
review_partner_user_id: str = Form(""),
start_date: str = Form(""),
end_date: str = Form(""),
expiry_date: str = Form(""),
status: str = Form("active"),
remarks: str = Form(""),
is_active: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if not row:
return RedirectResponse(url="/services/engagements", status_code=303)
active_fy = _active_financial_year(request)
if row.financial_year != active_fy:
return RedirectResponse(url=f"/services/engagements?financial_year={active_fy}", status_code=303)
if getattr(row, "is_locked", False) or is_row_financial_year_locked(db, row):
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
row.assigned_partner_user_id = int(assigned_partner_user_id) if assigned_partner_user_id.strip() else None
row.assigned_manager_user_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
row.assigned_staff_user_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
if review_partner_required_for_engagement(db, tenant_id=tenant_id, engagement_type=row.engagement_type):
row.review_partner_user_id = int(review_partner_user_id) if review_partner_user_id.strip() else getattr(row.client, "default_review_partner_user_id", None)
else:
row.review_partner_user_id = None
row.start_date = parse_date(start_date)
row.end_date = parse_date(end_date)
row.expiry_date = parse_date(expiry_date)
row.status = status or "active"
row.remarks = remarks.strip() or None
row.is_active = is_active is not None
row.updated_by_user_id = user.id
apply_due_date_rule_to_subscription(db, row)
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False)
enforce_quality_gate_on_subscription(row)
db.commit()
return RedirectResponse(url=f"/services/engagements/{row.id}", status_code=303)
finally:
db.close()
@router.post("/{subscription_id}/lock")
def subscription_lock(request: Request, subscription_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
if not _can_lock_engagements(db, user):
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
if row and row.financial_year != _active_financial_year(request):
return RedirectResponse(url=f"/services/engagements?financial_year={_active_financial_year(request)}", status_code=303)
if row and is_row_financial_year_locked(db, row):
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
if row and _user_can_lock_subscription(db, user, row):
if _lock_subscription_row(row, user):
db.commit()
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
finally:
db.close()