Auto generate engagement tasks after AQMM activation
This commit is contained in:
@@ -905,6 +905,14 @@ def subscription_detail(request: Request, subscription_id: int):
|
||||
db.refresh(row)
|
||||
|
||||
ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False)
|
||||
# Self-heal existing active engagements that became executable after AQMM approval
|
||||
# but were created before automatic post-approval task generation was wired in.
|
||||
# The generator is idempotent and only inserts missing template-backed tasks.
|
||||
if row.is_active and row.status == "active" and not row.is_locked:
|
||||
created_tasks = generate_tasks_for_subscription_if_ready(db, subscription=row, user_id=user.id)
|
||||
if created_tasks:
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
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"]
|
||||
@@ -1088,6 +1096,10 @@ def subscription_aqmm_approve(request: Request, subscription_id: int, csrf_token
|
||||
if row:
|
||||
try:
|
||||
approve_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id)
|
||||
# Approval removes the assurance quality gate. Create any missing execution
|
||||
# tasks immediately so the assigned staff member sees the engagement in My Work.
|
||||
# Existing task instances are preserved because the generator is idempotent.
|
||||
generate_tasks_for_subscription_if_ready(db, subscription=row, user_id=user.id)
|
||||
db.commit()
|
||||
except ValueError:
|
||||
db.rollback()
|
||||
|
||||
@@ -843,10 +843,36 @@ def get_or_create_engagement_closure(
|
||||
return row
|
||||
|
||||
|
||||
def _normal_tasks_completed(db: Session, *, subscription_id: int) -> bool:
|
||||
def _task_generation_counts(db: Session, *, subscription: ClientServiceSubscription) -> tuple[int, int]:
|
||||
"""Return (expected_active_templates, generated_active_tasks) for an engagement.
|
||||
|
||||
This is intentionally read-only and uses the same active firm task template scope as
|
||||
generate_tasks_for_subscription(). It lets closure readiness distinguish a genuinely
|
||||
completed engagement from an engagement where execution tasks were never generated.
|
||||
"""
|
||||
expected = db.execute(
|
||||
select(func.count(FirmServiceTaskTemplate.id)).where(
|
||||
FirmServiceTaskTemplate.tenant_id == subscription.tenant_id,
|
||||
FirmServiceTaskTemplate.service_catalogue_id == subscription.service_catalogue_id,
|
||||
FirmServiceTaskTemplate.is_active.is_(True),
|
||||
)
|
||||
).scalar_one()
|
||||
generated = db.execute(
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
ClientServiceTaskInstance.subscription_id == subscription.id,
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
)
|
||||
).scalar_one()
|
||||
return int(expected or 0), int(generated or 0)
|
||||
|
||||
|
||||
def _normal_tasks_completed(db: Session, *, subscription: ClientServiceSubscription) -> bool:
|
||||
expected_count, generated_count = _task_generation_counts(db, subscription=subscription)
|
||||
if expected_count > 0 and generated_count == 0:
|
||||
return False
|
||||
open_task = db.execute(
|
||||
select(ClientServiceTaskInstance.id).where(
|
||||
ClientServiceTaskInstance.subscription_id == subscription_id,
|
||||
ClientServiceTaskInstance.subscription_id == subscription.id,
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
ClientServiceTaskInstance.status.notin_(list(CLOSED_TASK_STATUSES)),
|
||||
)
|
||||
@@ -891,11 +917,14 @@ def _udin_completed(db: Session, *, subscription_id: int) -> bool:
|
||||
def closure_readiness_for_subscription(db: Session, *, subscription: ClientServiceSubscription) -> dict:
|
||||
assurance = quality_required_for_engagement(subscription.engagement_type)
|
||||
aqmm_summary = aqmm_task_summary_for_subscription(db, subscription_id=subscription.id)
|
||||
normal_tasks_done = _normal_tasks_completed(db, subscription_id=subscription.id)
|
||||
expected_task_count, generated_task_count = _task_generation_counts(db, subscription=subscription)
|
||||
tasks_generated = expected_task_count == 0 or generated_task_count > 0
|
||||
normal_tasks_done = _normal_tasks_completed(db, subscription=subscription)
|
||||
|
||||
aqmm_acceptance_completed = (not assurance) or getattr(subscription, "quality_acceptance_status", None) == QUALITY_APPROVED
|
||||
aqmm_tasks_completed = (not assurance) or (
|
||||
aqmm_summary["mandatory_done"] == aqmm_summary["mandatory"]
|
||||
tasks_generated
|
||||
and aqmm_summary["mandatory_done"] == aqmm_summary["mandatory"]
|
||||
and aqmm_summary["evidence_missing"] == 0
|
||||
and aqmm_summary["manager_review_pending"] == 0
|
||||
and aqmm_summary["partner_review_pending"] == 0
|
||||
@@ -903,7 +932,8 @@ def closure_readiness_for_subscription(db: Session, *, subscription: ClientServi
|
||||
and aqmm_summary["rework_open"] == 0
|
||||
)
|
||||
evidence_review_completed = (not assurance) or (
|
||||
aqmm_summary["evidence_missing"] == 0
|
||||
tasks_generated
|
||||
and aqmm_summary["evidence_missing"] == 0
|
||||
and aqmm_summary["manager_review_pending"] == 0
|
||||
and aqmm_summary["partner_review_pending"] == 0
|
||||
and aqmm_summary["review_partner_review_pending"] == 0
|
||||
@@ -917,6 +947,9 @@ def closure_readiness_for_subscription(db: Session, *, subscription: ClientServi
|
||||
return {
|
||||
"assurance": assurance,
|
||||
"aqmm_task_summary": aqmm_summary,
|
||||
"expected_task_count": expected_task_count,
|
||||
"generated_task_count": generated_task_count,
|
||||
"tasks_generated": tasks_generated,
|
||||
"normal_tasks_completed": normal_tasks_done,
|
||||
"aqmm_acceptance_completed": aqmm_acceptance_completed,
|
||||
"aqmm_tasks_completed": aqmm_tasks_completed,
|
||||
@@ -943,7 +976,9 @@ def update_engagement_closure_from_sources(
|
||||
row.normal_tasks_completed = bool(summary["normal_tasks_completed"])
|
||||
|
||||
blockers: list[str] = []
|
||||
if not row.normal_tasks_completed:
|
||||
if not summary.get("tasks_generated", True):
|
||||
blockers.append("execution tasks not generated")
|
||||
elif not row.normal_tasks_completed:
|
||||
blockers.append("open work tracker tasks pending")
|
||||
if summary["assurance"]:
|
||||
if not row.aqmm_acceptance_completed:
|
||||
|
||||
@@ -149,10 +149,10 @@
|
||||
{% if closure_checklist.closure_block_reason %}<div class="mt-3 rounded-xl border border-amber-200 bg-white px-4 py-3 text-sm text-amber-800">Closure pending: {{ closure_checklist.closure_block_reason }}</div>{% endif %}
|
||||
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
<div class="rounded-xl bg-white px-4 py-3 border border-emerald-100"><div class="text-xs font-semibold uppercase text-slate-500">Tasks</div><div class="mt-1 text-sm font-medium {{ 'text-emerald-700' if closure_checklist.normal_tasks_completed else 'text-amber-700' }}">{{ 'Completed' if closure_checklist.normal_tasks_completed else 'Pending' }}</div></div>
|
||||
<div class="rounded-xl bg-white px-4 py-3 border border-emerald-100"><div class="text-xs font-semibold uppercase text-slate-500">Tasks</div><div class="mt-1 text-sm font-medium {{ 'text-emerald-700' if closure_checklist.normal_tasks_completed else 'text-amber-700' }}">{{ 'Not Generated' if not closure_summary.tasks_generated else ('Completed' if closure_checklist.normal_tasks_completed else 'Pending') }}</div></div>
|
||||
<div class="rounded-xl bg-white px-4 py-3 border border-emerald-100"><div class="text-xs font-semibold uppercase text-slate-500">AQMM Acceptance</div><div class="mt-1 text-sm font-medium {{ 'text-emerald-700' if closure_checklist.aqmm_acceptance_completed else 'text-amber-700' }}">{{ 'Completed' if closure_checklist.aqmm_acceptance_completed else 'Pending' }}</div></div>
|
||||
<div class="rounded-xl bg-white px-4 py-3 border border-emerald-100"><div class="text-xs font-semibold uppercase text-slate-500">AQMM Tasks</div><div class="mt-1 text-sm font-medium {{ 'text-emerald-700' if closure_checklist.aqmm_tasks_completed else 'text-amber-700' }}">{{ 'Completed' if closure_checklist.aqmm_tasks_completed else 'Pending' }}</div></div>
|
||||
<div class="rounded-xl bg-white px-4 py-3 border border-emerald-100"><div class="text-xs font-semibold uppercase text-slate-500">Evidence / Reviews</div><div class="mt-1 text-sm font-medium {{ 'text-emerald-700' if closure_checklist.evidence_review_completed else 'text-amber-700' }}">{{ 'Completed' if closure_checklist.evidence_review_completed else 'Pending' }}</div></div>
|
||||
<div class="rounded-xl bg-white px-4 py-3 border border-emerald-100"><div class="text-xs font-semibold uppercase text-slate-500">AQMM Tasks</div><div class="mt-1 text-sm font-medium {{ 'text-emerald-700' if closure_checklist.aqmm_tasks_completed else 'text-amber-700' }}">{{ 'Not Generated' if not closure_summary.tasks_generated else ('Completed' if closure_checklist.aqmm_tasks_completed else 'Pending') }}</div></div>
|
||||
<div class="rounded-xl bg-white px-4 py-3 border border-emerald-100"><div class="text-xs font-semibold uppercase text-slate-500">Evidence / Reviews</div><div class="mt-1 text-sm font-medium {{ 'text-emerald-700' if closure_checklist.evidence_review_completed else 'text-amber-700' }}">{{ 'Not Generated' if not closure_summary.tasks_generated else ('Completed' if closure_checklist.evidence_review_completed else 'Pending') }}</div></div>
|
||||
<div class="rounded-xl bg-white px-4 py-3 border border-emerald-100"><div class="text-xs font-semibold uppercase text-slate-500">Final Release</div><div class="mt-1 text-sm font-medium {{ 'text-emerald-700' if closure_checklist.final_documents_released else 'text-amber-700' }}">{{ 'Completed' if closure_checklist.final_documents_released else 'Pending' }}</div></div>
|
||||
<div class="rounded-xl bg-white px-4 py-3 border border-emerald-100"><div class="text-xs font-semibold uppercase text-slate-500">UDIN</div><div class="mt-1 text-sm font-medium {{ 'text-emerald-700' if closure_checklist.udin_completed else 'text-amber-700' }}">{{ 'Completed' if closure_checklist.udin_completed else 'Pending' }}</div></div>
|
||||
</div>
|
||||
@@ -201,7 +201,7 @@
|
||||
<td class="px-4 py-3 text-right"><a href="/documents/tasks/{{ task.id }}" class="text-sm font-medium text-brand-700 hover:underline">Open Task Documents</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">No execution tasks generated yet. Generate work tracker tasks first.</td></tr>
|
||||
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">No execution tasks are available yet. For active engagements the system automatically creates any missing tasks from the current service task templates; reload this page after activation or AQMM approval.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user