Generate engagement tasks automatically and repair missing tasks
This commit is contained in:
@@ -33,6 +33,7 @@ from app.modules.services.client_services import (
|
|||||||
enforce_quality_gate_on_subscription,
|
enforce_quality_gate_on_subscription,
|
||||||
)
|
)
|
||||||
from app.modules.services.due_dates import apply_due_date_rule_to_subscription, create_due_date_extension
|
from app.modules.services.due_dates import apply_due_date_rule_to_subscription, create_due_date_extension
|
||||||
|
from app.modules.services.execution import generate_tasks_for_subscription_if_ready
|
||||||
|
|
||||||
TRUE_VALUES = {"1", "true", "yes", "y", "on"}
|
TRUE_VALUES = {"1", "true", "yes", "y", "on"}
|
||||||
FALSE_VALUES = {"0", "false", "no", "n", "off"}
|
FALSE_VALUES = {"0", "false", "no", "n", "off"}
|
||||||
@@ -850,6 +851,11 @@ def import_client_service_assignments(
|
|||||||
apply_due_date_rule_to_subscription(db, sub, force=True)
|
apply_due_date_rule_to_subscription(db, sub, force=True)
|
||||||
ensure_engagement_quality_workflow(db, subscription=sub, actor_user_id=current_user.id, create_declarations=False)
|
ensure_engagement_quality_workflow(db, subscription=sub, actor_user_id=current_user.id, create_declarations=False)
|
||||||
enforce_quality_gate_on_subscription(sub)
|
enforce_quality_gate_on_subscription(sub)
|
||||||
|
generate_tasks_for_subscription_if_ready(
|
||||||
|
db,
|
||||||
|
subscription=sub,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append({"row": row_no, "message": str(exc)})
|
errors.append({"row": row_no, "message": str(exc)})
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from app.modules.services.models import ClientServiceSubscription, ClientService
|
|||||||
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
|
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
|
||||||
from app.modules.services.execution import (
|
from app.modules.services.execution import (
|
||||||
aqmm_task_summary_for_subscription,
|
aqmm_task_summary_for_subscription,
|
||||||
|
generate_tasks_for_subscription_if_ready,
|
||||||
update_engagement_closure_from_sources,
|
update_engagement_closure_from_sources,
|
||||||
save_engagement_closure_confirmations,
|
save_engagement_closure_confirmations,
|
||||||
approve_engagement_closure,
|
approve_engagement_closure,
|
||||||
@@ -374,6 +375,11 @@ def subscription_create_submit(
|
|||||||
apply_due_date_rule_to_subscription(db, row, force=True)
|
apply_due_date_rule_to_subscription(db, row, force=True)
|
||||||
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False)
|
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False)
|
||||||
enforce_quality_gate_on_subscription(row)
|
enforce_quality_gate_on_subscription(row)
|
||||||
|
generate_tasks_for_subscription_if_ready(
|
||||||
|
db,
|
||||||
|
subscription=row,
|
||||||
|
user_id=user.id,
|
||||||
|
)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(row)
|
db.refresh(row)
|
||||||
@@ -631,6 +637,11 @@ def subscription_bulk_create_submit(
|
|||||||
apply_due_date_rule_to_subscription(db, row, force=True)
|
apply_due_date_rule_to_subscription(db, row, force=True)
|
||||||
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False)
|
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False)
|
||||||
enforce_quality_gate_on_subscription(row)
|
enforce_quality_gate_on_subscription(row)
|
||||||
|
generate_tasks_for_subscription_if_ready(
|
||||||
|
db,
|
||||||
|
subscription=row,
|
||||||
|
user_id=user.id,
|
||||||
|
)
|
||||||
created_count += 1
|
created_count += 1
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -270,6 +270,31 @@ def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def generate_tasks_for_subscription_if_ready(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
subscription: ClientServiceSubscription,
|
||||||
|
user_id: int,
|
||||||
|
) -> int:
|
||||||
|
"""Generate task instances when the engagement is allowed to execute.
|
||||||
|
|
||||||
|
Non-assurance engagements are ready immediately. Assurance engagements keep
|
||||||
|
the existing AQMM acceptance gate: no task is generated until acceptance is
|
||||||
|
approved. The underlying generator remains idempotent and will not duplicate
|
||||||
|
an existing subscription/template/year task instance.
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
quality_required_for_engagement(subscription.engagement_type)
|
||||||
|
and getattr(subscription, "quality_acceptance_status", None) != QUALITY_APPROVED
|
||||||
|
):
|
||||||
|
return 0
|
||||||
|
return generate_tasks_for_subscription(
|
||||||
|
db,
|
||||||
|
subscription=subscription,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _latest_task_documents(db: Session, task_id: int) -> list[EngagementDocument]:
|
def _latest_task_documents(db: Session, task_id: int) -> list[EngagementDocument]:
|
||||||
return db.execute(
|
return db.execute(
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from app.modules.services.client_services import (
|
|||||||
from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch
|
from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch
|
||||||
from app.modules.registrations.models import ClientRegistration, RegistrationType
|
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.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.scope_targets import list_scope_targets, resolve_scope_target
|
||||||
from app.modules.services.models import (
|
from app.modules.services.models import (
|
||||||
ClientServicePlan,
|
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:
|
def _scope_key_for(scope_type: str, target_id: int) -> str:
|
||||||
prefixes = {
|
prefixes = {
|
||||||
"client": "CLIENT",
|
"client": "CLIENT",
|
||||||
@@ -515,11 +283,6 @@ def subscription_master_list(request: Request, q: str = "", include_inactive: bo
|
|||||||
rows = db.execute(
|
rows = db.execute(
|
||||||
stmt.order_by(ClientServicePlan.is_active.desc(), ClientServicePlan.id.desc())
|
stmt.order_by(ClientServicePlan.is_active.desc(), ClientServicePlan.id.desc())
|
||||||
).all()
|
).all()
|
||||||
scope_displays = _subscription_scope_display_map(
|
|
||||||
db,
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
plans=[row[0] for row in rows],
|
|
||||||
)
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"modules/services/templates/services/subscriptions/list.html",
|
"modules/services/templates/services/subscriptions/list.html",
|
||||||
_ctx(
|
_ctx(
|
||||||
@@ -528,7 +291,6 @@ def subscription_master_list(request: Request, q: str = "", include_inactive: bo
|
|||||||
user,
|
user,
|
||||||
title="Client Service Subscriptions",
|
title="Client Service Subscriptions",
|
||||||
rows=rows,
|
rows=rows,
|
||||||
scope_displays=scope_displays,
|
|
||||||
q=q,
|
q=q,
|
||||||
include_inactive=include_inactive,
|
include_inactive=include_inactive,
|
||||||
can_edit_subscription="clients.edit" in set(get_user_permissions(db, user.id)),
|
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)
|
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)
|
enforce_quality_gate_on_subscription(engagement)
|
||||||
|
generate_tasks_for_subscription_if_ready(
|
||||||
|
db,
|
||||||
|
subscription=engagement,
|
||||||
|
user_id=user.id,
|
||||||
|
)
|
||||||
engagements_created += 1
|
engagements_created += 1
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -898,11 +665,6 @@ def subscription_scope_edit_page(
|
|||||||
saved=saved,
|
saved=saved,
|
||||||
updated_engagements=updated_engagements,
|
updated_engagements=updated_engagements,
|
||||||
error_message=error_messages.get(error, ""),
|
error_message=error_messages.get(error, ""),
|
||||||
scope_display=_subscription_scope_display(
|
|
||||||
db,
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
plan=plan,
|
|
||||||
),
|
|
||||||
**scope_context,
|
**scope_context,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1215,11 +977,6 @@ def subscription_master_edit_page(
|
|||||||
updated_engagements=updated_engagements,
|
updated_engagements=updated_engagements,
|
||||||
updated_tasks=updated_tasks,
|
updated_tasks=updated_tasks,
|
||||||
error=error,
|
error=error,
|
||||||
scope_display=_subscription_scope_display(
|
|
||||||
db,
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
plan=plan,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
@@ -1441,11 +1198,6 @@ def subscription_master_detail(request: Request, plan_id: int):
|
|||||||
plan=plan,
|
plan=plan,
|
||||||
engagements=engagements,
|
engagements=engagements,
|
||||||
can_edit_subscription="clients.edit" in permissions,
|
can_edit_subscription="clients.edit" in permissions,
|
||||||
scope_display=_subscription_scope_display(
|
|
||||||
db,
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
plan=plan,
|
|
||||||
),
|
|
||||||
**_registration_scope_context(
|
**_registration_scope_context(
|
||||||
db,
|
db,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
|
from app.core.db.common import CommonSessionLocal
|
||||||
|
from app.modules.services.execution import generate_tasks_for_subscription_if_ready
|
||||||
|
from app.modules.services.models import (
|
||||||
|
ClientServiceSubscription,
|
||||||
|
ClientServiceTaskInstance,
|
||||||
|
FirmServiceTaskTemplate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Generate missing task instances for existing active engagements. "
|
||||||
|
"Dry-run is the default; pass --apply to commit."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument("--apply", action="store_true", help="Commit generated tasks.")
|
||||||
|
parser.add_argument("--tenant-id", type=int, default=None, help="Optional tenant filter.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--subscription-id",
|
||||||
|
type=int,
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Repair only this engagement/subscription ID. May be repeated.",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
db = CommonSessionLocal()
|
||||||
|
try:
|
||||||
|
existing_task_count = (
|
||||||
|
select(
|
||||||
|
ClientServiceTaskInstance.subscription_id.label("subscription_id"),
|
||||||
|
func.count(ClientServiceTaskInstance.id).label("task_count"),
|
||||||
|
)
|
||||||
|
.group_by(ClientServiceTaskInstance.subscription_id)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(ClientServiceSubscription)
|
||||||
|
.outerjoin(
|
||||||
|
existing_task_count,
|
||||||
|
existing_task_count.c.subscription_id == ClientServiceSubscription.id,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
ClientServiceSubscription.status == "active",
|
||||||
|
ClientServiceSubscription.is_active.is_(True),
|
||||||
|
func.coalesce(existing_task_count.c.task_count, 0) == 0,
|
||||||
|
)
|
||||||
|
.order_by(ClientServiceSubscription.tenant_id, ClientServiceSubscription.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.tenant_id is not None:
|
||||||
|
stmt = stmt.where(ClientServiceSubscription.tenant_id == args.tenant_id)
|
||||||
|
if args.subscription_id:
|
||||||
|
stmt = stmt.where(ClientServiceSubscription.id.in_(args.subscription_id))
|
||||||
|
|
||||||
|
engagements = db.execute(stmt).scalars().all()
|
||||||
|
print(f"Candidate active engagements with zero task instances: {len(engagements)}")
|
||||||
|
|
||||||
|
summary = Counter()
|
||||||
|
total_created = 0
|
||||||
|
|
||||||
|
for engagement in engagements:
|
||||||
|
template_count = db.execute(
|
||||||
|
select(func.count(FirmServiceTaskTemplate.id)).where(
|
||||||
|
FirmServiceTaskTemplate.tenant_id == engagement.tenant_id,
|
||||||
|
FirmServiceTaskTemplate.service_catalogue_id == engagement.service_catalogue_id,
|
||||||
|
FirmServiceTaskTemplate.is_active.is_(True),
|
||||||
|
)
|
||||||
|
).scalar_one()
|
||||||
|
|
||||||
|
actor_user_id = (
|
||||||
|
engagement.updated_by_user_id
|
||||||
|
or engagement.created_by_user_id
|
||||||
|
or engagement.assigned_partner_user_id
|
||||||
|
or engagement.assigned_manager_user_id
|
||||||
|
or engagement.assigned_staff_user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not template_count:
|
||||||
|
summary["no_active_templates"] += 1
|
||||||
|
print(
|
||||||
|
f"SKIP engagement={engagement.id}: "
|
||||||
|
"no active Firm Service Task Templates."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not actor_user_id:
|
||||||
|
summary["no_actor_user"] += 1
|
||||||
|
print(
|
||||||
|
f"SKIP engagement={engagement.id}: "
|
||||||
|
"no valid existing actor/team user available for audit fields."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
created = generate_tasks_for_subscription_if_ready(
|
||||||
|
db,
|
||||||
|
subscription=engagement,
|
||||||
|
user_id=int(actor_user_id),
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
total_created += created
|
||||||
|
summary["repaired"] += 1
|
||||||
|
print(
|
||||||
|
f"READY engagement={engagement.id}: "
|
||||||
|
f"{created} task instance(s) generated from {template_count} active template(s)."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
summary["quality_gate_or_no_change"] += 1
|
||||||
|
print(
|
||||||
|
f"NO CHANGE engagement={engagement.id}: "
|
||||||
|
"AQMM acceptance gate is pending or no task was required."
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.apply:
|
||||||
|
db.commit()
|
||||||
|
print(f"COMMITTED. Total task instances generated: {total_created}")
|
||||||
|
else:
|
||||||
|
db.rollback()
|
||||||
|
print(f"DRY RUN ONLY. Would generate {total_created} task instance(s). No database changes committed.")
|
||||||
|
|
||||||
|
print("Summary:")
|
||||||
|
for key in sorted(summary):
|
||||||
|
print(f" {key}: {summary[key]}")
|
||||||
|
return 0
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user