Align bulk subscriptions with bulk engagement setup

This commit is contained in:
A R R R Associates
2026-08-05 18:31:30 +05:30
parent 47cd5c2cf3
commit 9b307ebcdd
3 changed files with 876 additions and 1 deletions
+376
View File
@@ -13,12 +13,29 @@ from app.core.security.session_auth import get_current_user
from app.core.templating import templates
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
from app.modules.services.client_services import (
SUBSCRIPTION_STATUSES,
assessment_year_from_financial_year,
attach_engagement_to_plan,
ensure_engagement_quality_workflow,
enforce_quality_gate_on_subscription,
get_enabled_firm_service,
get_existing_subscription,
get_or_create_client_service_plan,
list_assignable_users,
list_clients_for_assignment,
list_enabled_services_for_assignment,
list_review_partners,
normalize_financial_year,
normalize_period_label,
parse_date,
period_choices_for_service,
recurrence_requires_period,
review_partner_required_for_engagement,
)
from app.modules.clients.models import Client
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
from app.modules.services.models import (
ClientServicePlan,
ClientServiceSubscription,
@@ -44,6 +61,18 @@ def _branch_id(request, user):
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 _partner_scope_id(db, user) -> int | None:
roles = set(get_user_roles(db, user.id))
return int(user.id) if "Partner" in roles else None
def _ctx(request, db, user, **extra):
data = {
"request": request,
@@ -189,6 +218,353 @@ def subscription_master_list(request: Request, q: str = "", include_inactive: bo
db.close()
@router.get("/bulk")
def subscription_bulk_page(
request: Request,
error: str = "",
subscriptions_created: int = 0,
subscriptions_reused: int = 0,
engagements_created: int = 0,
engagements_skipped: int = 0,
):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse("/login", 303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _denied()
tenant_id = _tenant_id(request, user)
branch_id = _branch_id(request, user)
clients = list_clients_for_assignment(
db,
tenant_id=tenant_id,
branch_id=branch_id,
partner_id=_partner_scope_id(db, user),
)
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
partners = list_assignable_users(
db, tenant_id=tenant_id, branch_id=branch_id, role_names=("Partner",)
)
managers = list_assignable_users(
db, tenant_id=tenant_id, branch_id=branch_id, role_names=("Branch Manager",)
)
staff_users = list_assignable_users(
db, tenant_id=tenant_id, branch_id=branch_id, role_names=("Staff",)
)
review_partners = list_review_partners(
db, tenant_id=tenant_id, branch_id=branch_id
)
error_messages = {
"clients": "Select at least one permitted client.",
"service": "Select a valid enabled firm service.",
"partner": "Select a valid Engagement Partner.",
"performing_partner": "Select a valid Performing Partner.",
"manager": "Select a valid Manager.",
"staff": "Select a valid Staff member.",
"review_partner": "Select a valid Review Partner.",
"review_partner_required": "Review Partner is mandatory for this assurance service.",
"review_partner_independence": "Review Partner must differ from the Engagement and Performing Partners.",
"period": "Select a valid month or quarter.",
"generation": "Select a valid engagement generation option.",
"financial_year": "The selected financial year is locked.",
}
return templates.TemplateResponse(
"modules/services/templates/services/subscriptions/bulk.html",
_ctx(
request,
db,
user,
title="Bulk Client Subscriptions",
clients=clients,
enabled_services=enabled_services,
partners=partners,
managers=managers,
staff_users=staff_users,
review_partners=review_partners,
client_partner_names={
row.id: (row.full_name or row.email)
for row in partners
},
financial_year=_active_financial_year(request),
error_message=error_messages.get(error, ""),
subscriptions_created=subscriptions_created,
subscriptions_reused=subscriptions_reused,
engagements_created=engagements_created,
engagements_skipped=engagements_skipped,
),
)
finally:
db.close()
@router.post("/bulk")
def subscription_bulk_submit(
request: Request,
client_ids: list[int] = Form([]),
service_catalogue_id: int = Form(...),
default_partner_user_id: int = Form(...),
default_performing_partner_user_id: str = Form(""),
default_manager_user_id: str = Form(""),
default_staff_user_id: str = Form(""),
default_review_partner_user_id: str = Form(""),
effective_from: str = Form(""),
effective_to: str = Form(""),
auto_generate_periods: str | None = Form(None),
remarks: str = Form(""),
generation_mode: str = Form("subscription_only"),
financial_year: str = Form(""),
period_label: str = Form(""),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
subscriptions_created = 0
subscriptions_reused = 0
engagements_created = 0
engagements_skipped = 0
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse("/login", 303)
try:
require_permission(db, user, "clients.edit")
except Exception:
return _denied()
tenant_id = _tenant_id(request, user)
branch_id = _branch_id(request, user)
permitted_clients = list_clients_for_assignment(
db,
tenant_id=tenant_id,
branch_id=branch_id,
partner_id=_partner_scope_id(db, user),
)
permitted_client_map = {row.id: row for row in permitted_clients}
selected_client_ids = list(dict.fromkeys(int(value) for value in client_ids))
if not selected_client_ids or any(value not in permitted_client_map for value in selected_client_ids):
return RedirectResponse("/services/subscriptions/bulk?error=clients", 303)
firm_selection = get_enabled_firm_service(
db,
tenant_id=tenant_id,
service_catalogue_id=service_catalogue_id,
)
if not firm_selection:
return RedirectResponse("/services/subscriptions/bulk?error=service", 303)
plan_branch_id = (
branch_id
or getattr(firm_selection, "default_branch_id", None)
or getattr(user, "branch_id", None)
)
partners = list_assignable_users(
db, tenant_id=tenant_id, branch_id=plan_branch_id, role_names=("Partner",)
)
managers = list_assignable_users(
db, tenant_id=tenant_id, branch_id=plan_branch_id, role_names=("Branch Manager",)
)
staff_users = list_assignable_users(
db, tenant_id=tenant_id, branch_id=plan_branch_id, role_names=("Staff",)
)
review_partners = list_review_partners(
db, tenant_id=tenant_id, branch_id=plan_branch_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 default_partner_user_id not in partner_ids:
return RedirectResponse("/services/subscriptions/bulk?error=partner", 303)
performing_partner_id = _optional_int(default_performing_partner_user_id) or default_partner_user_id
if performing_partner_id not in partner_ids:
return RedirectResponse("/services/subscriptions/bulk?error=performing_partner", 303)
manager_id = _optional_int(default_manager_user_id)
if manager_id is not None and manager_id not in manager_ids:
return RedirectResponse("/services/subscriptions/bulk?error=manager", 303)
staff_id = _optional_int(default_staff_user_id)
if staff_id is not None and staff_id not in staff_ids:
return RedirectResponse("/services/subscriptions/bulk?error=staff", 303)
review_partner_id = _optional_int(default_review_partner_user_id)
if review_partner_id is not None and review_partner_id not in review_partner_ids:
return RedirectResponse("/services/subscriptions/bulk?error=review_partner", 303)
engagement_type = (
getattr(firm_selection.catalogue, "engagement_type", None)
or "non_assurance"
)
review_required = review_partner_required_for_engagement(
db, tenant_id=tenant_id, engagement_type=engagement_type
)
if review_required and review_partner_id is None:
return RedirectResponse("/services/subscriptions/bulk?error=review_partner_required", 303)
if review_required and review_partner_id in {default_partner_user_id, performing_partner_id}:
return RedirectResponse("/services/subscriptions/bulk?error=review_partner_independence", 303)
if generation_mode not in {"subscription_only", "current_period", "all_periods"}:
return RedirectResponse("/services/subscriptions/bulk?error=generation", 303)
selected_financial_year = normalize_financial_year(
financial_year or _active_financial_year(request)
)
if generation_mode != "subscription_only":
locked_response = redirect_if_financial_year_locked(
db,
tenant_id=tenant_id,
year_code=selected_financial_year,
redirect_url="/services/subscriptions/bulk?error=financial_year",
)
if locked_response:
return locked_response
recurrence_type = getattr(firm_selection.catalogue, "recurrence_type", None)
if generation_mode == "subscription_only":
requested_periods: list[str] = []
elif recurrence_requires_period(recurrence_type):
if generation_mode == "all_periods":
requested_periods = [
code for code, _label in period_choices_for_service(
selected_financial_year, recurrence_type
)
]
else:
try:
requested_periods = [
normalize_period_label(
period_label,
financial_year=selected_financial_year,
recurrence_type=recurrence_type,
)
]
except ValueError:
return RedirectResponse("/services/subscriptions/bulk?error=period", 303)
else:
requested_periods = [""]
plan_effective_from = parse_date(effective_from)
plan_effective_to = parse_date(effective_to)
if plan_effective_from and plan_effective_to and plan_effective_to < plan_effective_from:
return RedirectResponse("/services/subscriptions/bulk?error=period", 303)
for client_id in selected_client_ids:
client = permitted_client_map[client_id]
existing_plan = db.execute(
select(ClientServicePlan).where(
ClientServicePlan.tenant_id == tenant_id,
ClientServicePlan.client_id == client.id,
ClientServicePlan.service_catalogue_id == service_catalogue_id,
)
).scalar_one_or_none()
plan = get_or_create_client_service_plan(
db,
tenant_id=tenant_id,
client=client,
catalogue=firm_selection.catalogue,
firm_selection=firm_selection,
branch_id=plan_branch_id or getattr(client, "branch_id", None),
partner_user_id=default_partner_user_id,
performing_partner_user_id=performing_partner_id,
manager_user_id=manager_id,
staff_user_id=staff_id,
review_partner_user_id=review_partner_id,
actor_user_id=user.id,
remarks=remarks.strip() or None,
)
plan.effective_from = plan_effective_from
plan.effective_to = plan_effective_to
plan.auto_generate_periods = auto_generate_periods is not None
plan.status = "active"
plan.is_active = True
plan.updated_by_user_id = user.id
if remarks.strip():
plan.remarks = remarks.strip()
if existing_plan is None:
subscriptions_created += 1
else:
subscriptions_reused += 1
for requested_period in requested_periods:
existing = get_existing_subscription(
db,
tenant_id=tenant_id,
client_id=client.id,
service_catalogue_id=service_catalogue_id,
financial_year=selected_financial_year,
period_label=requested_period,
)
if existing:
if existing.service_plan_id is None:
existing.service_plan_id = plan.id
engagements_skipped += 1
continue
engagement = ClientServiceSubscription(
tenant_id=tenant_id,
branch_id=plan.branch_id or getattr(client, "branch_id", None),
service_plan_id=plan.id,
client_id=client.id,
service_catalogue_id=service_catalogue_id,
firm_service_selection_id=firm_selection.id,
assigned_partner_user_id=default_partner_user_id,
performing_partner_user_id=performing_partner_id,
assigned_manager_user_id=manager_id,
assigned_staff_user_id=staff_id,
review_partner_user_id=review_partner_id if review_required else None,
financial_year=selected_financial_year,
period_label=requested_period,
assessment_year=assessment_year_from_financial_year(selected_financial_year),
engagement_type=engagement_type,
start_date=plan_effective_from,
end_date=plan_effective_to,
status="active",
remarks=remarks.strip() or None,
is_active=True,
created_by_user_id=user.id,
updated_by_user_id=user.id,
)
db.add(engagement)
db.flush()
attach_engagement_to_plan(
db,
engagement=engagement,
client=client,
catalogue=firm_selection.catalogue,
firm_selection=firm_selection,
actor_user_id=user.id,
)
apply_due_date_rule_to_subscription(db, engagement, force=True)
ensure_engagement_quality_workflow(
db,
subscription=engagement,
actor_user_id=user.id,
create_declarations=False,
)
enforce_quality_gate_on_subscription(engagement)
engagements_created += 1
db.commit()
return RedirectResponse(
"/services/subscriptions/bulk"
f"?subscriptions_created={subscriptions_created}"
f"&subscriptions_reused={subscriptions_reused}"
f"&engagements_created={engagements_created}"
f"&engagements_skipped={engagements_skipped}",
status_code=303,
)
except Exception:
db.rollback()
raise
finally:
db.close()
@router.get("/{plan_id}/edit")
def subscription_master_edit_page(
request: Request,