From 581f5faf80834105cc4357c95e13dd749c9eea88 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Tue, 22 Sep 2026 17:33:06 +0530 Subject: [PATCH] Synchronise approved AQMM across staff and review boards --- app/modules/employees/service.py | 26 ++++++++++++++++++++ app/modules/manager_dashboard/service.py | 15 +++++++++++- app/modules/partner_dashboard/service.py | 20 ++++++++++++++++ app/modules/partners/ui.py | 30 +++++++++++++++++++++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/app/modules/employees/service.py b/app/modules/employees/service.py index cda614b..d587463 100644 --- a/app/modules/employees/service.py +++ b/app/modules/employees/service.py @@ -34,6 +34,7 @@ from app.modules.services.execution import ( apply_task_checklist_response, recalculate_task_aqmm_status, submit_task_for_review, + synchronise_approved_engagement_acceptance_tasks, ) EMPLOYEE_STATUS = ["active", "inactive", "relieved"] @@ -4077,6 +4078,31 @@ def get_employee_engagement_work_board( document, checklist, AQMM and lock controls remain the source of truth. """ today = date.today() + + # Cross-view AQMM self-heal: an engagement that has already passed the + # engagement-level AQMM acceptance must never continue to show the four + # duplicated setup rows as pending in the Staff work board. Reconcile the + # persisted execution rows before the board queries them. The synchroniser + # is deliberately limited to untouched acceptance/team/independence/KYC + # setup rows and does not modify normal execution history, evidence or rework. + subscription_stmt = select(ClientServiceSubscription).where( + ClientServiceSubscription.id == int(engagement_id), + ClientServiceSubscription.tenant_id == int(scope.tenant_id), + ) + if scope.branch_id is not None: + subscription_stmt = subscription_stmt.where( + ClientServiceSubscription.branch_id == int(scope.branch_id) + ) + subscription_for_sync = db.execute(subscription_stmt).scalar_one_or_none() + if subscription_for_sync is not None: + repaired = synchronise_approved_engagement_acceptance_tasks( + db, + subscription=subscription_for_sync, + actor_user_id=int(scope.own_user_id), + ) + if repaired: + db.commit() + assigned_stmt = _employee_work_task_query( db, scope, assigned_only=True, financial_year=financial_year ).where(ClientServiceTaskInstance.subscription_id == engagement_id) diff --git a/app/modules/manager_dashboard/service.py b/app/modules/manager_dashboard/service.py index 1519cf5..05a5957 100644 --- a/app/modules/manager_dashboard/service.py +++ b/app/modules/manager_dashboard/service.py @@ -17,7 +17,7 @@ from app.modules.employees.service import ( _engagement_team, _weighted_progress, ) -from app.modules.services.execution import CLOSED_TASK_STATUSES +from app.modules.services.execution import CLOSED_TASK_STATUSES, synchronise_approved_engagement_acceptance_tasks from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance, ServiceCatalogue, ServiceTaskComment MANAGER_ROLES = {"Manager", "Branch Manager", "System Admin", "Firm Admin", "Partner"} @@ -599,6 +599,19 @@ def get_manager_review_workspace( if not tasks: return None + subscription_for_sync = getattr(tasks[0], "subscription", None) + if subscription_for_sync is not None: + repaired_setup_rows = synchronise_approved_engagement_acceptance_tasks( + db, + subscription=subscription_for_sync, + actor_user_id=int(current_user.id), + ) + if repaired_setup_rows: + db.commit() + tasks = list(db.execute( + stmt.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc()) + ).scalars().all()) + task_rows = [_manager_review_task_row(task) for task in tasks] reviewable = [row for row in task_rows if row["review_state"] in {"pending_review", "reviewed", "rework"}] pending = [row for row in reviewable if row["review_state"] == "pending_review"] diff --git a/app/modules/partner_dashboard/service.py b/app/modules/partner_dashboard/service.py index bd18feb..25153ba 100644 --- a/app/modules/partner_dashboard/service.py +++ b/app/modules/partner_dashboard/service.py @@ -18,6 +18,7 @@ from app.modules.services.execution import ( CLOSED_TASK_STATUSES, aqmm_task_summary_for_subscription, closure_readiness_for_subscription, + synchronise_approved_engagement_acceptance_tasks, ) from app.modules.services.models import ( ClientServiceSubscription, @@ -571,6 +572,17 @@ def _partner_review_queue_rows(db: Session, request, current_user, *, limit: int ClientServiceSubscription.review_partner_user_id == current_user.id, )) subscriptions = list(db.execute(stmt.order_by(ClientServiceSubscription.current_due_date.is_(None), ClientServiceSubscription.current_due_date.asc(), ClientServiceSubscription.id.desc()).limit(limit)).scalars().all()) + + repaired_setup_rows = 0 + for sub in subscriptions: + repaired_setup_rows += synchronise_approved_engagement_acceptance_tasks( + db, + subscription=sub, + actor_user_id=int(current_user.id), + ) + if repaired_setup_rows: + db.commit() + rows: list[dict[str, Any]] = [] for sub in subscriptions: task_stmt = select(ClientServiceTaskInstance).where( @@ -638,6 +650,14 @@ def get_partner_review_workspace(db: Session, request, current_user, *, subscrip }: return None + repaired_setup_rows = synchronise_approved_engagement_acceptance_tasks( + db, + subscription=subscription, + actor_user_id=int(current_user.id), + ) + if repaired_setup_rows: + db.commit() + task_stmt = ( select(ClientServiceTaskInstance) .options( diff --git a/app/modules/partners/ui.py b/app/modules/partners/ui.py index 78f5472..34b0775 100644 --- a/app/modules/partners/ui.py +++ b/app/modules/partners/ui.py @@ -18,7 +18,12 @@ from app.modules.core.rbac.deps import get_user_permissions, get_user_roles from app.modules.documents.models import EngagementDocument from app.modules.clients.models import Client, ClientBranch, ClientBusinessUnit from app.modules.registrations.models import ClientRegistration, RegistrationType -from app.modules.services.execution import CLOSED_TASK_STATUSES, TASK_PRIORITIES, TASK_STATUSES +from app.modules.services.execution import ( + CLOSED_TASK_STATUSES, + TASK_PRIORITIES, + TASK_STATUSES, + synchronise_approved_engagement_acceptance_tasks, +) from app.modules.services.engagement_resources import build_engagement_resource_context from app.modules.services.models import ( ClientServiceSubscription, @@ -408,6 +413,21 @@ def build_partner_payload( ) option_stmt = _subscription_scope_filter(option_stmt, tenant_id, branch_id, current_user, roles, financial_year=None) option_engagements = db.execute(option_stmt).scalars().all() + + # Keep the Review Board in step with the engagement-level AQMM workflow. + # Older generated task rows can retain their original AQMM gate flags even + # after acceptance is approved; reconcile only those duplicated setup rows + # before task buckets/counts are built. + repaired_setup_rows = 0 + for subscription in option_engagements: + repaired_setup_rows += synchronise_approved_engagement_acceptance_tasks( + db, + subscription=subscription, + actor_user_id=int(current_user.id), + ) + if repaired_setup_rows: + db.commit() + financial_year_options = sorted({(row.financial_year or "").strip() for row in option_engagements if (row.financial_year or "").strip()}, reverse=True) period_options = sorted({(row.period_label or "").strip() for row in option_engagements if (row.period_label or "").strip()}) service_map = {} @@ -697,6 +717,14 @@ def partner_engagement_detail(request: Request, engagement_id: int): if not engagement: return _redirect_denied() + repaired_setup_rows = synchronise_approved_engagement_acceptance_tasks( + db, + subscription=engagement, + actor_user_id=int(current_user.id), + ) + if repaired_setup_rows: + db.commit() + task_stmt = select(ClientServiceTaskInstance).options( selectinload(ClientServiceTaskInstance.client), selectinload(ClientServiceTaskInstance.catalogue),