Add scope-aware subscriptions using existing registrations
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -36,6 +36,7 @@ from app.modules.services.client_services import (
|
||||
)
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
|
||||
from app.modules.services.scope_targets import list_scope_targets, resolve_scope_target
|
||||
from app.modules.services.models import (
|
||||
ClientServicePlan,
|
||||
ClientServiceSubscription,
|
||||
@@ -240,27 +241,13 @@ def subscription_bulk_page(
|
||||
|
||||
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
|
||||
)
|
||||
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.",
|
||||
"targets": "Select at least one permitted Client, Business Unit, Client Branch or Registration.",
|
||||
"service": "Select a valid enabled firm service.",
|
||||
"partner": "Select a valid Engagement Partner.",
|
||||
"performing_partner": "Select a valid Performing Partner.",
|
||||
@@ -276,20 +263,9 @@ def subscription_bulk_page(
|
||||
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
|
||||
},
|
||||
request, db, user, title="Bulk Client Subscriptions",
|
||||
enabled_services=enabled_services, partners=partners, managers=managers,
|
||||
staff_users=staff_users, review_partners=review_partners,
|
||||
financial_year=_active_financial_year(request),
|
||||
error_message=error_messages.get(error, ""),
|
||||
subscriptions_created=subscriptions_created,
|
||||
@@ -302,10 +278,59 @@ def subscription_bulk_page(
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/bulk/targets")
|
||||
def subscription_bulk_targets(request: Request, service_catalogue_id: int):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return JSONResponse({"error": "Authentication required."}, status_code=401)
|
||||
try:
|
||||
require_permission(db, user, "clients.edit")
|
||||
except Exception:
|
||||
return JSONResponse({"error": "Access denied."}, status_code=403)
|
||||
|
||||
tenant_id = _tenant_id(request, user)
|
||||
branch_id = _branch_id(request, user)
|
||||
firm_selection = get_enabled_firm_service(db, tenant_id=tenant_id, service_catalogue_id=service_catalogue_id)
|
||||
if not firm_selection:
|
||||
return JSONResponse({"error": "Enabled firm service not found."}, status_code=404)
|
||||
|
||||
clients = list_clients_for_assignment(
|
||||
db, tenant_id=tenant_id, branch_id=branch_id, partner_id=_partner_scope_id(db, user)
|
||||
)
|
||||
targets = list_scope_targets(db, tenant_id=tenant_id, clients=clients, catalogue=firm_selection.catalogue)
|
||||
scope_type = getattr(firm_selection.catalogue, "service_scope_type", "client") or "client"
|
||||
registration_type = getattr(firm_selection.catalogue, "required_registration_type", None)
|
||||
return {
|
||||
"scope_type": scope_type,
|
||||
"registration_type": registration_type,
|
||||
"targets": [
|
||||
{
|
||||
"token": row.token,
|
||||
"client_code": row.client_code,
|
||||
"client_name": row.client_name,
|
||||
"pan": row.pan,
|
||||
"business_unit": row.business_unit,
|
||||
"client_branch": row.client_branch,
|
||||
"registration_type": row.registration_type,
|
||||
"registration_number": row.registration_number,
|
||||
"trade_name": row.trade_name,
|
||||
"state": row.state,
|
||||
"entity_type": row.entity_type,
|
||||
"subscription_status": row.existing_plan_status or "not_subscribed",
|
||||
}
|
||||
for row in targets
|
||||
],
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/bulk")
|
||||
def subscription_bulk_submit(
|
||||
request: Request,
|
||||
client_ids: list[int] = Form([]),
|
||||
scope_targets: list[str] = Form([]),
|
||||
service_catalogue_id: int = Form(...),
|
||||
default_partner_user_id: int = Form(...),
|
||||
default_performing_partner_user_id: str = Form(""),
|
||||
@@ -323,10 +348,7 @@ def subscription_bulk_submit(
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
subscriptions_created = 0
|
||||
subscriptions_reused = 0
|
||||
engagements_created = 0
|
||||
engagements_skipped = 0
|
||||
subscriptions_created = subscriptions_reused = engagements_created = engagements_skipped = 0
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
@@ -338,70 +360,41 @@ def subscription_bulk_submit(
|
||||
|
||||
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,
|
||||
)
|
||||
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
|
||||
permitted_clients = list_clients_for_assignment(
|
||||
db, tenant_id=tenant_id, branch_id=branch_id, partner_id=_partner_scope_id(db, user)
|
||||
)
|
||||
clients_by_id = {int(row.id): row for row in permitted_clients}
|
||||
selected_tokens = list(dict.fromkeys(value for value in scope_targets if value))
|
||||
if not selected_tokens:
|
||||
return RedirectResponse("/services/subscriptions/bulk?error=targets", 303)
|
||||
|
||||
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}
|
||||
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)
|
||||
|
||||
if default_partner_user_id not in partner_ids:
|
||||
if default_partner_user_id not in {row.id for row in partners}:
|
||||
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:
|
||||
if performing_partner_id not in {row.id for row in partners}:
|
||||
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:
|
||||
if manager_id is not None and manager_id not in {row.id for row in managers}:
|
||||
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:
|
||||
if staff_id is not None and staff_id not in {row.id for row in staff_users}:
|
||||
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:
|
||||
if review_partner_id is not None and review_partner_id not in {row.id for row in review_partners}:
|
||||
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
|
||||
)
|
||||
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}:
|
||||
@@ -409,39 +402,26 @@ def subscription_bulk_submit(
|
||||
|
||||
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)
|
||||
)
|
||||
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,
|
||||
locked = 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
|
||||
if locked:
|
||||
return locked
|
||||
|
||||
recurrence_type = getattr(firm_selection.catalogue, "recurrence_type", None)
|
||||
if generation_mode == "subscription_only":
|
||||
requested_periods: list[str] = []
|
||||
requested_periods = []
|
||||
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
|
||||
)
|
||||
]
|
||||
requested_periods = [code for code, _ 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,
|
||||
)
|
||||
]
|
||||
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:
|
||||
@@ -452,54 +432,51 @@ def subscription_bulk_submit(
|
||||
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,
|
||||
for token in selected_tokens:
|
||||
try:
|
||||
client, scope_type, scope_key, business_unit_id, client_branch_id, registration_id = resolve_scope_target(
|
||||
db, tenant_id=tenant_id, clients_by_id=clients_by_id, token=token, actor_user_id=user.id
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
except ValueError:
|
||||
db.rollback()
|
||||
return RedirectResponse("/services/subscriptions/bulk?error=targets", 303)
|
||||
|
||||
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()
|
||||
plan = db.execute(select(ClientServicePlan).where(
|
||||
ClientServicePlan.tenant_id == tenant_id,
|
||||
ClientServicePlan.service_catalogue_id == service_catalogue_id,
|
||||
ClientServicePlan.scope_key == scope_key,
|
||||
)).scalar_one_or_none()
|
||||
|
||||
if existing_plan is None:
|
||||
subscriptions_created += 1
|
||||
else:
|
||||
if plan:
|
||||
subscriptions_reused += 1
|
||||
else:
|
||||
plan = ClientServicePlan(
|
||||
tenant_id=tenant_id, branch_id=plan_branch_id or client.branch_id,
|
||||
client_id=client.id, scope_type=scope_type, scope_key=scope_key,
|
||||
business_unit_id=business_unit_id, client_branch_id=client_branch_id,
|
||||
registration_id=registration_id, service_catalogue_id=service_catalogue_id,
|
||||
firm_service_selection_id=firm_selection.id,
|
||||
default_partner_user_id=default_partner_user_id,
|
||||
default_performing_partner_user_id=performing_partner_id,
|
||||
default_manager_user_id=manager_id, default_staff_user_id=staff_id,
|
||||
default_review_partner_user_id=review_partner_id,
|
||||
recurrence_type=(recurrence_type or "one_time"),
|
||||
effective_from=plan_effective_from, effective_to=plan_effective_to,
|
||||
auto_generate_periods=auto_generate_periods is not None,
|
||||
status="active", is_active=True, remarks=remarks.strip() or None,
|
||||
created_by_user_id=user.id, updated_by_user_id=user.id,
|
||||
)
|
||||
db.add(plan); db.flush()
|
||||
subscriptions_created += 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,
|
||||
)
|
||||
existing = db.execute(select(ClientServiceSubscription).where(
|
||||
ClientServiceSubscription.tenant_id == tenant_id,
|
||||
ClientServiceSubscription.service_catalogue_id == service_catalogue_id,
|
||||
ClientServiceSubscription.scope_key == scope_key,
|
||||
ClientServiceSubscription.financial_year == selected_financial_year,
|
||||
ClientServiceSubscription.period_label == requested_period,
|
||||
)).scalar_one_or_none()
|
||||
if existing:
|
||||
if existing.service_plan_id is None:
|
||||
existing.service_plan_id = plan.id
|
||||
@@ -507,57 +484,37 @@ def subscription_bulk_submit(
|
||||
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,
|
||||
tenant_id=tenant_id, branch_id=plan.branch_id, service_plan_id=plan.id,
|
||||
client_id=client.id, scope_type=scope_type, scope_key=scope_key,
|
||||
business_unit_id=business_unit_id, client_branch_id=client_branch_id,
|
||||
registration_id=registration_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,
|
||||
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,
|
||||
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,
|
||||
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()
|
||||
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,
|
||||
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,
|
||||
)
|
||||
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,
|
||||
f"?subscriptions_created={subscriptions_created}&subscriptions_reused={subscriptions_reused}"
|
||||
f"&engagements_created={engagements_created}&engagements_skipped={engagements_skipped}",
|
||||
303,
|
||||
)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
|
||||
Reference in New Issue
Block a user