Fix duplicate AQMM gating on approved engagements
This commit is contained in:
@@ -279,6 +279,128 @@ def _default_internal_target_date(subscription: ClientServiceSubscription, templ
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_task_name_for_setup_sync(value: str | None) -> str:
|
||||||
|
return " ".join((value or "").strip().lower().replace("&", "and").split())
|
||||||
|
|
||||||
|
|
||||||
|
def _is_engagement_acceptance_setup_task_name(value: str | None) -> bool:
|
||||||
|
"""Return True only for execution rows that duplicate the engagement-level AQMM setup.
|
||||||
|
|
||||||
|
These tasks are retained in the work tracker as historical/scope rows, but once the
|
||||||
|
engagement-level AQMM workflow is approved they must not ask staff to repeat the
|
||||||
|
same acceptance, independence, KYC or team-setup approvals.
|
||||||
|
"""
|
||||||
|
name = _normalise_task_name_for_setup_sync(value)
|
||||||
|
if not name:
|
||||||
|
return False
|
||||||
|
if "acceptance and continuance" in name:
|
||||||
|
return True
|
||||||
|
if "independence and conflict" in name:
|
||||||
|
return True
|
||||||
|
if "kyc" in name and "engagement letter" in name:
|
||||||
|
return True
|
||||||
|
if name.startswith("create ") and "engagement" in name and "assign team" in name:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_approved_engagement_acceptance_to_task(
|
||||||
|
task: ClientServiceTaskInstance,
|
||||||
|
*,
|
||||||
|
actor_user_id: int,
|
||||||
|
) -> bool:
|
||||||
|
"""Synchronise one duplicate setup task from an already-approved engagement AQMM gate.
|
||||||
|
|
||||||
|
No task is deleted and no executed/reviewed normal work is rewritten. Only the four
|
||||||
|
pre-execution setup controls that duplicate the engagement-level acceptance workflow
|
||||||
|
are satisfied automatically. This removes the circular gate where an active, AQMM-
|
||||||
|
approved engagement asks staff to obtain AQMM approval again before starting work.
|
||||||
|
"""
|
||||||
|
if not _is_engagement_acceptance_setup_task_name(getattr(task, "task_name", None)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Never rewrite genuine task execution/rework history. This repair is intentionally
|
||||||
|
# limited to untouched setup rows, plus rows already auto-completed by this sync.
|
||||||
|
status = (getattr(task, "status", None) or "pending").strip().lower()
|
||||||
|
if status not in {"pending", "completed"}:
|
||||||
|
return False
|
||||||
|
if getattr(task, "started_at_utc", None) is not None and status != "completed":
|
||||||
|
return False
|
||||||
|
if (getattr(task, "rework_status", None) or "none").strip().lower() == "open":
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
def set_if_different(attr: str, value) -> None:
|
||||||
|
nonlocal changed
|
||||||
|
if getattr(task, attr, None) != value:
|
||||||
|
setattr(task, attr, value)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
set_if_different("status", "completed")
|
||||||
|
if getattr(task, "completed_at_utc", None) is None:
|
||||||
|
task.completed_at_utc = now
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# The engagement-level AQMM approval has already performed these reviews. Keep the
|
||||||
|
# task row, but do not make staff repeat Manager/Partner/Review-Partner approvals.
|
||||||
|
set_if_different("normal_review_required", False)
|
||||||
|
set_if_different("normal_review_role", None)
|
||||||
|
set_if_different("normal_review_status", "not_required")
|
||||||
|
set_if_different("aqmm_manager_review_required", False)
|
||||||
|
set_if_different("aqmm_partner_review_required", False)
|
||||||
|
set_if_different("aqmm_review_partner_required", False)
|
||||||
|
set_if_different("manager_review_status", "not_required")
|
||||||
|
set_if_different("partner_review_status", "not_required")
|
||||||
|
set_if_different("review_partner_review_status", "not_required")
|
||||||
|
|
||||||
|
# Evidence for these four setup controls lives in the engagement-level AQMM records
|
||||||
|
# (declarations/KYC/engagement letter). Do not require duplicate task-document evidence.
|
||||||
|
set_if_different("aqmm_evidence_required", False)
|
||||||
|
set_if_different("evidence_status", "not_required")
|
||||||
|
set_if_different("aqmm_review_status", "not_required")
|
||||||
|
set_if_different("aqmm_status", "completed")
|
||||||
|
if getattr(task, "aqmm_completed_at_utc", None) is None:
|
||||||
|
task.aqmm_completed_at_utc = now
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
task.updated_by_user_id = actor_user_id
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def synchronise_approved_engagement_acceptance_tasks(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
subscription: ClientServiceSubscription,
|
||||||
|
actor_user_id: int,
|
||||||
|
) -> int:
|
||||||
|
"""Self-heal duplicate pre-acceptance execution rows after AQMM approval.
|
||||||
|
|
||||||
|
Returns the number of task rows changed. The caller can use this to decide whether
|
||||||
|
a commit is required. Existing non-setup tasks, task responses, evidence, rework and
|
||||||
|
reviews are left untouched.
|
||||||
|
"""
|
||||||
|
if not quality_required_for_engagement(subscription.engagement_type):
|
||||||
|
return 0
|
||||||
|
if getattr(subscription, "quality_acceptance_status", None) != QUALITY_APPROVED:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
tasks = db.execute(
|
||||||
|
select(ClientServiceTaskInstance).where(
|
||||||
|
ClientServiceTaskInstance.subscription_id == subscription.id,
|
||||||
|
ClientServiceTaskInstance.is_active.is_(True),
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
|
||||||
|
changed = 0
|
||||||
|
for task in tasks:
|
||||||
|
if _apply_approved_engagement_acceptance_to_task(task, actor_user_id=actor_user_id):
|
||||||
|
changed += 1
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceSubscription, user_id: int) -> int:
|
def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceSubscription, user_id: int) -> int:
|
||||||
if quality_required_for_engagement(subscription.engagement_type) and getattr(subscription, "quality_acceptance_status", None) != QUALITY_APPROVED:
|
if quality_required_for_engagement(subscription.engagement_type) and getattr(subscription, "quality_acceptance_status", None) != QUALITY_APPROVED:
|
||||||
raise ValueError(getattr(subscription, "quality_block_reason", None) or "AQMM acceptance is pending for this assurance engagement. Complete AQMM before generating tasks.")
|
raise ValueError(getattr(subscription, "quality_block_reason", None) or "AQMM acceptance is pending for this assurance engagement. Complete AQMM before generating tasks.")
|
||||||
@@ -372,11 +494,19 @@ def generate_tasks_for_subscription_if_ready(
|
|||||||
and getattr(subscription, "quality_acceptance_status", None) != QUALITY_APPROVED
|
and getattr(subscription, "quality_acceptance_status", None) != QUALITY_APPROVED
|
||||||
):
|
):
|
||||||
return 0
|
return 0
|
||||||
return generate_tasks_for_subscription(
|
created = generate_tasks_for_subscription(
|
||||||
db,
|
db,
|
||||||
subscription=subscription,
|
subscription=subscription,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
|
repaired = synchronise_approved_engagement_acceptance_tasks(
|
||||||
|
db,
|
||||||
|
subscription=subscription,
|
||||||
|
actor_user_id=user_id,
|
||||||
|
)
|
||||||
|
# Preserve the historical meaning (positive = database changed) while allowing
|
||||||
|
# existing already-generated engagements to self-heal and be committed by callers.
|
||||||
|
return int(created) + int(repaired)
|
||||||
|
|
||||||
|
|
||||||
def _sync_normal_review_snapshot_preserving_history(
|
def _sync_normal_review_snapshot_preserving_history(
|
||||||
|
|||||||
Reference in New Issue
Block a user