From eb18a5695f235ec9f02f6219dfa9c7b2348474f0 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Tue, 11 Aug 2026 19:40:53 +0530 Subject: [PATCH] Auto lock completed non assurance engagements --- app/modules/services/engagements_ui.py | 72 ++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/app/modules/services/engagements_ui.py b/app/modules/services/engagements_ui.py index 66edfe1..623ffad 100644 --- a/app/modules/services/engagements_ui.py +++ b/app/modules/services/engagements_ui.py @@ -11,6 +11,7 @@ from app.core.templating import templates from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance from app.modules.services.due_dates import apply_due_date_rule_to_subscription from app.modules.services.execution import ( + CLOSED_TASK_STATUSES, aqmm_task_summary_for_subscription, generate_tasks_for_subscription_if_ready, update_engagement_closure_from_sources, @@ -151,6 +152,60 @@ def _lock_subscription_row(row: ClientServiceSubscription, user) -> bool: return True + +def _auto_lock_completed_non_assurance_subscription( + db, + row: ClientServiceSubscription, + user, +) -> bool: + """Lock a completed non-assurance engagement without touching its plan. + + Completion here means every active task is closed, every required normal + review is reviewed, every required AQMM task-review flag (if any) is + reviewed, and no task has open rework. Assurance engagements deliberately + remain on the existing AQMM/final-release closure workflow. + """ + if getattr(row, "is_locked", False): + return False + if (getattr(row, "engagement_type", None) or "non_assurance").strip().lower() == "assurance": + return False + + tasks = list( + db.execute( + select(ClientServiceTaskInstance).where( + ClientServiceTaskInstance.subscription_id == row.id, + ClientServiceTaskInstance.is_active.is_(True), + ) + ).scalars().all() + ) + if not tasks: + return False + + for task in tasks: + status = (getattr(task, "status", None) or "pending").strip().lower() + if status not in CLOSED_TASK_STATUSES: + return False + + if getattr(task, "normal_review_required", False): + if (getattr(task, "normal_review_status", None) or "not_required").strip().lower() != "reviewed": + return False + + if getattr(task, "aqmm_manager_review_required", False): + if (getattr(task, "manager_review_status", None) or "not_required").strip().lower() != "reviewed": + return False + if getattr(task, "aqmm_partner_review_required", False): + if (getattr(task, "partner_review_status", None) or "not_required").strip().lower() != "reviewed": + return False + if getattr(task, "aqmm_review_partner_required", False): + if (getattr(task, "review_partner_review_status", None) or "not_required").strip().lower() != "reviewed": + return False + + if (getattr(task, "rework_status", None) or "none").strip().lower() == "open": + return False + + return _lock_subscription_row(row, user) + + @router.get("") def subscription_list( request: Request, @@ -184,6 +239,13 @@ def subscription_list( q=q, include_inactive=include_inactive, ) + auto_locked = 0 + for row in rows: + if _auto_lock_completed_non_assurance_subscription(db, row, user): + auto_locked += 1 + if auto_locked: + db.commit() + return _render( request, "modules/services/templates/services/engagements/list.html", @@ -731,6 +793,10 @@ def subscription_detail(request: Request, subscription_id: int): if row.financial_year != active_fy: return RedirectResponse(url=f"/services/engagements?financial_year={active_fy}", status_code=303) + if _auto_lock_completed_non_assurance_subscription(db, row, user): + db.commit() + db.refresh(row) + ensure_engagement_quality_workflow(db, subscription=row, actor_user_id=user.id, create_declarations=False) db.flush() declarations = list_engagement_quality_declarations(db, subscription_id=row.id) @@ -1055,6 +1121,9 @@ def subscription_edit_page(request: Request, subscription_id: int): active_fy = _active_financial_year(request) if row.financial_year != active_fy: return RedirectResponse(url=f"/services/engagements?financial_year={active_fy}", status_code=303) + if _auto_lock_completed_non_assurance_subscription(db, row, user): + db.commit() + db.refresh(row) if getattr(row, "is_locked", False) or is_row_financial_year_locked(db, row): return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303) @@ -1125,6 +1194,9 @@ def subscription_edit_submit( active_fy = _active_financial_year(request) if row.financial_year != active_fy: return RedirectResponse(url=f"/services/engagements?financial_year={active_fy}", status_code=303) + if _auto_lock_completed_non_assurance_subscription(db, row, user): + db.commit() + db.refresh(row) if getattr(row, "is_locked", False) or is_row_financial_year_locked(db, row): return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)