Auto generate engagement tasks after AQMM activation

This commit is contained in:
A R R R Associates
2026-09-22 16:22:08 +05:30
parent cb3da81f6f
commit de47ceb72d
5 changed files with 579 additions and 375 deletions
+12
View File
@@ -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()
+41 -6
View File
@@ -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>