Generate engagement tasks automatically and repair missing tasks
This commit is contained in:
@@ -37,6 +37,7 @@ from app.modules.services.client_services import (
|
||||
from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch
|
||||
from app.modules.registrations.models import ClientRegistration, RegistrationType
|
||||
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
|
||||
from app.modules.services.execution import generate_tasks_for_subscription_if_ready
|
||||
from app.modules.services.scope_targets import list_scope_targets, resolve_scope_target
|
||||
from app.modules.services.models import (
|
||||
ClientServicePlan,
|
||||
@@ -143,239 +144,6 @@ def _registration_scope_context(db, *, tenant_id: int, plan: ClientServicePlan):
|
||||
}
|
||||
|
||||
|
||||
|
||||
def _subscription_scope_display_map(
|
||||
db,
|
||||
*,
|
||||
tenant_id: int,
|
||||
plans: list[ClientServicePlan],
|
||||
) -> dict[int, dict[str, str]]:
|
||||
"""Resolve human-friendly subscription names without changing stored scope data."""
|
||||
if not plans:
|
||||
return {}
|
||||
|
||||
business_ids = {int(row.business_unit_id) for row in plans if row.business_unit_id}
|
||||
branch_ids = {int(row.client_branch_id) for row in plans if row.client_branch_id}
|
||||
registration_ids = {int(row.registration_id) for row in plans if row.registration_id}
|
||||
|
||||
businesses = {}
|
||||
if business_ids:
|
||||
businesses = {
|
||||
row.id: row
|
||||
for row in db.execute(
|
||||
select(ClientBusinessUnit).where(
|
||||
ClientBusinessUnit.tenant_id == tenant_id,
|
||||
ClientBusinessUnit.id.in_(business_ids),
|
||||
)
|
||||
).scalars().all()
|
||||
}
|
||||
|
||||
branches = {}
|
||||
if branch_ids:
|
||||
branches = {
|
||||
row.id: row
|
||||
for row in db.execute(
|
||||
select(ClientBranch).where(
|
||||
ClientBranch.tenant_id == tenant_id,
|
||||
ClientBranch.id.in_(branch_ids),
|
||||
)
|
||||
).scalars().all()
|
||||
}
|
||||
|
||||
registrations = {}
|
||||
registration_types = {}
|
||||
if registration_ids:
|
||||
registration_rows = db.execute(
|
||||
select(ClientRegistration, RegistrationType)
|
||||
.join(
|
||||
RegistrationType,
|
||||
RegistrationType.id == ClientRegistration.registration_type_id,
|
||||
)
|
||||
.where(
|
||||
ClientRegistration.tenant_id == tenant_id,
|
||||
ClientRegistration.id.in_(registration_ids),
|
||||
)
|
||||
).all()
|
||||
for registration, registration_type in registration_rows:
|
||||
registrations[registration.id] = registration
|
||||
registration_types[registration.id] = registration_type
|
||||
|
||||
# A mapped registration can carry its own Business Unit / Client Branch.
|
||||
# Load those as fallbacks even when the plan's denormalised IDs are absent.
|
||||
extra_business_ids = {
|
||||
int(row.business_unit_id)
|
||||
for row in registrations.values()
|
||||
if row.business_unit_id and row.business_unit_id not in businesses
|
||||
}
|
||||
if extra_business_ids:
|
||||
businesses.update({
|
||||
row.id: row
|
||||
for row in db.execute(
|
||||
select(ClientBusinessUnit).where(
|
||||
ClientBusinessUnit.tenant_id == tenant_id,
|
||||
ClientBusinessUnit.id.in_(extra_business_ids),
|
||||
)
|
||||
).scalars().all()
|
||||
})
|
||||
|
||||
extra_branch_ids = {
|
||||
int(row.client_branch_id)
|
||||
for row in registrations.values()
|
||||
if row.client_branch_id and row.client_branch_id not in branches
|
||||
}
|
||||
if extra_branch_ids:
|
||||
branches.update({
|
||||
row.id: row
|
||||
for row in db.execute(
|
||||
select(ClientBranch).where(
|
||||
ClientBranch.tenant_id == tenant_id,
|
||||
ClientBranch.id.in_(extra_branch_ids),
|
||||
)
|
||||
).scalars().all()
|
||||
})
|
||||
|
||||
result: dict[int, dict[str, str]] = {}
|
||||
for plan in plans:
|
||||
client = plan.client
|
||||
client_name = (getattr(client, "client_name", None) or "").strip()
|
||||
client_code = (getattr(client, "client_code", None) or "").strip()
|
||||
client_trade_name = (getattr(client, "trade_name", None) or "").strip()
|
||||
scope_type = (getattr(plan, "scope_type", None) or "client").strip().lower()
|
||||
|
||||
primary = client_name or client_trade_name or client_code or f"Client {plan.client_id}"
|
||||
secondary = client_code
|
||||
context = ""
|
||||
scope_label = "Client"
|
||||
|
||||
if scope_type == "business_unit":
|
||||
business = businesses.get(plan.business_unit_id)
|
||||
if business:
|
||||
primary = (
|
||||
(getattr(business, "trade_name", None) or "").strip()
|
||||
or (getattr(business, "business_name", None) or "").strip()
|
||||
or primary
|
||||
)
|
||||
secondary = (getattr(business, "business_code", None) or "").strip()
|
||||
context = client_name if client_name and client_name != primary else ""
|
||||
scope_label = "Business Unit"
|
||||
|
||||
elif scope_type == "client_branch":
|
||||
branch = branches.get(plan.client_branch_id)
|
||||
business = businesses.get(
|
||||
getattr(branch, "business_unit_id", None) if branch else plan.business_unit_id
|
||||
)
|
||||
if branch:
|
||||
primary = (getattr(branch, "branch_name", None) or "").strip() or primary
|
||||
secondary = (getattr(branch, "branch_code", None) or "").strip()
|
||||
business_name = ""
|
||||
if business:
|
||||
business_name = (
|
||||
(getattr(business, "trade_name", None) or "").strip()
|
||||
or (getattr(business, "business_name", None) or "").strip()
|
||||
)
|
||||
context_parts = []
|
||||
if business_name and business_name != primary:
|
||||
context_parts.append(business_name)
|
||||
if client_name and client_name not in {primary, business_name}:
|
||||
context_parts.append(f"Client: {client_name}")
|
||||
context = " · ".join(context_parts)
|
||||
scope_label = "Client Branch"
|
||||
|
||||
elif scope_type == "registration":
|
||||
registration = registrations.get(plan.registration_id)
|
||||
registration_type = registration_types.get(plan.registration_id)
|
||||
if registration:
|
||||
business = businesses.get(
|
||||
getattr(registration, "business_unit_id", None) or plan.business_unit_id
|
||||
)
|
||||
branch = branches.get(
|
||||
getattr(registration, "client_branch_id", None) or plan.client_branch_id
|
||||
)
|
||||
|
||||
business_trade_name = (
|
||||
(getattr(business, "trade_name", None) or "").strip()
|
||||
if business else ""
|
||||
)
|
||||
business_name = (
|
||||
(getattr(business, "business_name", None) or "").strip()
|
||||
if business else ""
|
||||
)
|
||||
branch_name = (
|
||||
(getattr(branch, "branch_name", None) or "").strip()
|
||||
if branch else ""
|
||||
)
|
||||
|
||||
# Registration-level subscriptions are identified by the
|
||||
# registration's own trade name first. This prevents one
|
||||
# individual/legal client with multiple registrations from
|
||||
# appearing as several indistinguishable rows.
|
||||
primary = (
|
||||
(getattr(registration, "trade_name", None) or "").strip()
|
||||
or business_trade_name
|
||||
or business_name
|
||||
or branch_name
|
||||
or (getattr(registration, "legal_name", None) or "").strip()
|
||||
or client_trade_name
|
||||
or client_name
|
||||
or primary
|
||||
)
|
||||
registration_number = (
|
||||
getattr(registration, "registration_number", None) or ""
|
||||
).strip()
|
||||
registration_code = (
|
||||
getattr(registration_type, "code", None) or ""
|
||||
).strip()
|
||||
secondary = (
|
||||
f"{registration_code} · {registration_number}"
|
||||
if registration_code and registration_number
|
||||
else registration_number or registration_code
|
||||
)
|
||||
context_parts = []
|
||||
if branch_name and branch_name != primary:
|
||||
context_parts.append(branch_name)
|
||||
if client_name and client_name != primary:
|
||||
context_parts.append(f"Client: {client_name}")
|
||||
context = " · ".join(context_parts)
|
||||
scope_label = "Registration"
|
||||
else:
|
||||
# Legacy/unmapped rows retain their existing client display.
|
||||
# No stored subscription or engagement data is altered.
|
||||
scope_label = "Registration"
|
||||
|
||||
result[int(plan.id)] = {
|
||||
"primary": primary,
|
||||
"secondary": secondary,
|
||||
"context": context,
|
||||
"scope_label": scope_label,
|
||||
"client_name": client_name,
|
||||
"client_code": client_code,
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _subscription_scope_display(
|
||||
db,
|
||||
*,
|
||||
tenant_id: int,
|
||||
plan: ClientServicePlan,
|
||||
) -> dict[str, str]:
|
||||
return _subscription_scope_display_map(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
plans=[plan],
|
||||
).get(
|
||||
int(plan.id),
|
||||
{
|
||||
"primary": plan.client.client_name,
|
||||
"secondary": plan.client.client_code or "",
|
||||
"context": "",
|
||||
"scope_label": "Client",
|
||||
"client_name": plan.client.client_name,
|
||||
"client_code": plan.client.client_code or "",
|
||||
},
|
||||
)
|
||||
|
||||
def _scope_key_for(scope_type: str, target_id: int) -> str:
|
||||
prefixes = {
|
||||
"client": "CLIENT",
|
||||
@@ -515,11 +283,6 @@ def subscription_master_list(request: Request, q: str = "", include_inactive: bo
|
||||
rows = db.execute(
|
||||
stmt.order_by(ClientServicePlan.is_active.desc(), ClientServicePlan.id.desc())
|
||||
).all()
|
||||
scope_displays = _subscription_scope_display_map(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
plans=[row[0] for row in rows],
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
"modules/services/templates/services/subscriptions/list.html",
|
||||
_ctx(
|
||||
@@ -528,7 +291,6 @@ def subscription_master_list(request: Request, q: str = "", include_inactive: bo
|
||||
user,
|
||||
title="Client Service Subscriptions",
|
||||
rows=rows,
|
||||
scope_displays=scope_displays,
|
||||
q=q,
|
||||
include_inactive=include_inactive,
|
||||
can_edit_subscription="clients.edit" in set(get_user_permissions(db, user.id)),
|
||||
@@ -826,6 +588,11 @@ def subscription_bulk_submit(
|
||||
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)
|
||||
generate_tasks_for_subscription_if_ready(
|
||||
db,
|
||||
subscription=engagement,
|
||||
user_id=user.id,
|
||||
)
|
||||
engagements_created += 1
|
||||
|
||||
db.commit()
|
||||
@@ -898,11 +665,6 @@ def subscription_scope_edit_page(
|
||||
saved=saved,
|
||||
updated_engagements=updated_engagements,
|
||||
error_message=error_messages.get(error, ""),
|
||||
scope_display=_subscription_scope_display(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
plan=plan,
|
||||
),
|
||||
**scope_context,
|
||||
),
|
||||
)
|
||||
@@ -1215,11 +977,6 @@ def subscription_master_edit_page(
|
||||
updated_engagements=updated_engagements,
|
||||
updated_tasks=updated_tasks,
|
||||
error=error,
|
||||
scope_display=_subscription_scope_display(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
plan=plan,
|
||||
),
|
||||
),
|
||||
)
|
||||
finally:
|
||||
@@ -1441,11 +1198,6 @@ def subscription_master_detail(request: Request, plan_id: int):
|
||||
plan=plan,
|
||||
engagements=engagements,
|
||||
can_edit_subscription="clients.edit" in permissions,
|
||||
scope_display=_subscription_scope_display(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
plan=plan,
|
||||
),
|
||||
**_registration_scope_context(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
|
||||
Reference in New Issue
Block a user