From c3684b7afad0cc0f38bbf6d51b48c98eb3f4b949 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Tue, 11 Aug 2026 09:05:09 +0530 Subject: [PATCH] Separate normal task review from AQMM review --- .../20260811_normal_review_separation.py | 112 ++++++++++++++++++ app/modules/employees/service.py | 64 +++++----- app/modules/manager_dashboard/service.py | 56 +++++++-- .../manager_review_workspace.html | 6 +- app/modules/manager_dashboard/ui.py | 27 +++-- app/modules/partner_dashboard/service.py | 41 +++++-- .../partner_review_workspace.html | 4 +- app/modules/partner_dashboard/ui.py | 29 +++-- .../templates/partners/review_board.html | 4 +- app/modules/partners/ui.py | 47 +++++--- app/modules/services/execution.py | 87 ++++++++++++++ app/modules/services/models.py | 10 ++ .../templates/services/default_task_form.html | 8 ++ .../services/default_templates_detail.html | 4 +- .../templates/services/firm_task_form.html | 8 ++ .../services/task_template_detail.html | 4 +- app/modules/services/ui.py | 22 +++- 17 files changed, 435 insertions(+), 98 deletions(-) create mode 100644 alembic/versions/20260811_normal_review_separation.py diff --git a/alembic/versions/20260811_normal_review_separation.py b/alembic/versions/20260811_normal_review_separation.py new file mode 100644 index 0000000..aea0880 --- /dev/null +++ b/alembic/versions/20260811_normal_review_separation.py @@ -0,0 +1,112 @@ +"""separate normal operational review from AQMM review + +Revision ID: 20260811_normal_review +Revises: 20260808_task_role_eligibility +Create Date: 2026-08-11 +""" +from alembic import op +import sqlalchemy as sa + +revision = "20260811_normal_review" +down_revision = "20260808_task_role_eligibility" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("service_default_task_templates", sa.Column("normal_review_role", sa.String(length=30), nullable=True)) + op.add_column("firm_service_task_templates", sa.Column("normal_review_role", sa.String(length=30), nullable=True)) + + op.add_column("client_service_task_instances", sa.Column("normal_review_required", sa.Boolean(), nullable=False, server_default=sa.false())) + op.add_column("client_service_task_instances", sa.Column("normal_review_role", sa.String(length=30), nullable=True)) + op.add_column("client_service_task_instances", sa.Column("normal_review_status", sa.String(length=30), nullable=False, server_default="not_required")) + op.add_column("client_service_task_instances", sa.Column("normal_review_note", sa.Text(), nullable=True)) + op.add_column("client_service_task_instances", sa.Column("normal_reviewed_by_user_id", sa.Integer(), nullable=True)) + op.add_column("client_service_task_instances", sa.Column("normal_reviewed_at_utc", sa.DateTime(timezone=True), nullable=True)) + op.create_foreign_key( + "fk_task_instance_normal_reviewed_by", + "client_service_task_instances", + "users", + ["normal_reviewed_by_user_id"], + ["id"], + ondelete="SET NULL", + ) + op.create_index("ix_task_instance_normal_review_required", "client_service_task_instances", ["normal_review_required"]) + op.create_index("ix_task_instance_normal_review_role", "client_service_task_instances", ["normal_review_role"]) + op.create_index("ix_task_instance_normal_review_status", "client_service_task_instances", ["normal_review_status"]) + op.create_index("ix_task_instance_normal_reviewed_by", "client_service_task_instances", ["normal_reviewed_by_user_id"]) + + # Preserve the old intent of requires_review while making the reviewer explicit. + # Reviewer inference follows task hierarchy: Staff -> Manager, Manager -> Partner. + role_case = """ + CASE + WHEN NOT requires_review THEN NULL + WHEN lower(coalesce(default_role_name,'')) IN ('manager','branch manager') + AND lower(coalesce(eligible_role_names,'')) LIKE '%partner%' THEN 'partner' + WHEN lower(coalesce(default_role_name,'')) NOT IN ('manager','branch manager','partner') + AND (lower(coalesce(eligible_role_names,'')) LIKE '%manager%' OR lower(coalesce(eligible_role_names,'')) LIKE '%branch manager%') THEN 'manager' + WHEN lower(coalesce(eligible_role_names,'')) LIKE '%partner%' + AND lower(coalesce(eligible_role_names,'')) NOT LIKE '%manager%' THEN 'partner' + ELSE 'manager' + END + """ + op.execute(sa.text(f"UPDATE service_default_task_templates SET normal_review_role = {role_case} WHERE requires_review = TRUE")) + op.execute(sa.text(f"UPDATE firm_service_task_templates SET normal_review_role = {role_case} WHERE requires_review = TRUE")) + + # Snapshot normal-review configuration into every existing engagement task. + op.execute(sa.text(""" + UPDATE client_service_task_instances AS t + SET normal_review_required = f.requires_review, + normal_review_role = f.normal_review_role + FROM firm_service_task_templates AS f + WHERE t.firm_task_template_id = f.id + """)) + + # Preserve prior generic approvals where they were already recorded in the legacy + # manager/partner fields. Otherwise completed/submitted work becomes Pending Review; + # unstarted work remains Not Ready. AQMM fields themselves are not changed. + op.execute(sa.text(""" + UPDATE client_service_task_instances + SET normal_review_status = CASE + WHEN NOT normal_review_required THEN 'not_required' + WHEN normal_review_role IN ('manager','manager_or_partner') AND manager_review_status = 'reviewed' THEN 'reviewed' + WHEN normal_review_role = 'partner' AND partner_review_status = 'reviewed' THEN 'reviewed' + WHEN rework_status = 'open' THEN 'rework_required' + WHEN submitted_for_review_at_utc IS NOT NULL OR lower(coalesce(status,'')) = 'completed' THEN 'pending' + ELSE 'not_ready' + END, + normal_review_note = CASE + WHEN normal_review_role IN ('manager','manager_or_partner') AND manager_review_status IN ('reviewed','rework_required') THEN manager_review_note + WHEN normal_review_role = 'partner' AND partner_review_status IN ('reviewed','rework_required') THEN partner_review_note + ELSE NULL + END, + normal_reviewed_by_user_id = CASE + WHEN normal_review_role IN ('manager','manager_or_partner') AND manager_review_status = 'reviewed' THEN manager_reviewed_by_user_id + WHEN normal_review_role = 'partner' AND partner_review_status = 'reviewed' THEN partner_reviewed_by_user_id + ELSE NULL + END, + normal_reviewed_at_utc = CASE + WHEN normal_review_role IN ('manager','manager_or_partner') AND manager_review_status = 'reviewed' THEN manager_reviewed_at_utc + WHEN normal_review_role = 'partner' AND partner_review_status = 'reviewed' THEN partner_reviewed_at_utc + ELSE NULL + END + """)) + + op.alter_column("client_service_task_instances", "normal_review_required", server_default=None) + op.alter_column("client_service_task_instances", "normal_review_status", server_default=None) + + +def downgrade() -> None: + op.drop_index("ix_task_instance_normal_reviewed_by", table_name="client_service_task_instances") + op.drop_index("ix_task_instance_normal_review_status", table_name="client_service_task_instances") + op.drop_index("ix_task_instance_normal_review_role", table_name="client_service_task_instances") + op.drop_index("ix_task_instance_normal_review_required", table_name="client_service_task_instances") + op.drop_constraint("fk_task_instance_normal_reviewed_by", "client_service_task_instances", type_="foreignkey") + op.drop_column("client_service_task_instances", "normal_reviewed_at_utc") + op.drop_column("client_service_task_instances", "normal_reviewed_by_user_id") + op.drop_column("client_service_task_instances", "normal_review_note") + op.drop_column("client_service_task_instances", "normal_review_status") + op.drop_column("client_service_task_instances", "normal_review_role") + op.drop_column("client_service_task_instances", "normal_review_required") + op.drop_column("firm_service_task_templates", "normal_review_role") + op.drop_column("service_default_task_templates", "normal_review_role") diff --git a/app/modules/employees/service.py b/app/modules/employees/service.py index eb873c3..e661966 100644 --- a/app/modules/employees/service.py +++ b/app/modules/employees/service.py @@ -3489,34 +3489,34 @@ def _employee_task_category(task: ClientServiceTaskInstance) -> str: def _i1_required_review_levels(task: ClientServiceTaskInstance) -> list[str]: - """Return configured review levels for I1 using existing task-template controls.""" + """Return normal and AQMM review gates without mixing their state.""" levels: list[str] = [] - if getattr(task, "aqmm_manager_review_required", False): - levels.append("manager") - if getattr(task, "aqmm_partner_review_required", False): - levels.append("partner") - if getattr(task, "aqmm_review_partner_required", False): - levels.append("review_partner") + normal_role = (getattr(task, "normal_review_role", None) or "").strip().lower() + if getattr(task, "normal_review_required", False): + if normal_role == "manager": + levels.append("normal_manager") + elif normal_role == "partner": + levels.append("normal_partner") + elif normal_role == "manager_or_partner": + levels.append("normal_manager_or_partner") - # Preserve the pre-existing generic requires_review flag. Where no explicit - # reviewer flag was configured, route to Manager when available, otherwise - # Engagement Partner. No new review table/state is introduced. - template = getattr(task, "template", None) - if not levels and getattr(template, "requires_review", False): - subscription = getattr(task, "subscription", None) - if getattr(subscription, "assigned_manager_user_id", None): - levels.append("manager") - elif getattr(subscription, "assigned_partner_user_id", None): - levels.append("partner") + if getattr(task, "aqmm_manager_review_required", False): + levels.append("aqmm_manager") + if getattr(task, "aqmm_partner_review_required", False): + levels.append("aqmm_partner") + if getattr(task, "aqmm_review_partner_required", False): + levels.append("aqmm_review_partner") return levels def _i1_review_level_pending(task: ClientServiceTaskInstance, level: str) -> bool: - if level == "manager": + if level.startswith("normal_"): + return (getattr(task, "normal_review_status", None) or "not_required") != "reviewed" + if level == "aqmm_manager": return (getattr(task, "manager_review_status", None) or "not_required") != "reviewed" - if level == "partner": + if level == "aqmm_partner": return (getattr(task, "partner_review_status", None) or "not_required") != "reviewed" - if level == "review_partner": + if level == "aqmm_review_partner": return (getattr(task, "review_partner_review_status", None) or "not_required") != "reviewed" return False @@ -3531,9 +3531,12 @@ def _i1_review_label(task: ClientServiceTaskInstance) -> str | None: if not pending: return "Approved" labels = { - "manager": "Manager", - "partner": "Partner", - "review_partner": "Review Partner", + "normal_manager": "Manager", + "normal_partner": "Partner", + "normal_manager_or_partner": "Manager / Partner", + "aqmm_manager": "AQMM Manager", + "aqmm_partner": "AQMM Partner", + "aqmm_review_partner": "AQMM Review Partner", } return "Awaiting " + " + ".join(labels[level] for level in pending) + " approval" @@ -3554,9 +3557,12 @@ def _i1_approval_blocker( if not pending: continue labels = { - "manager": "Manager", - "partner": "Partner", - "review_partner": "Review Partner", + "normal_manager": "Manager", + "normal_partner": "Partner", + "normal_manager_or_partner": "Manager / Partner", + "aqmm_manager": "AQMM Manager", + "aqmm_partner": "AQMM Partner", + "aqmm_review_partner": "AQMM Review Partner", } return { "task_id": int(row.id), @@ -3620,6 +3626,8 @@ def _workflow_task_weight(task: ClientServiceTaskInstance) -> int: weight += 2 if bool(getattr(task, "aqmm_blocks_final_release", False)): weight += 2 + if bool(getattr(task, "normal_review_required", False)): + weight += 1 if any(bool(getattr(task, name, False)) for name in ( "aqmm_manager_review_required", "aqmm_partner_review_required", @@ -3971,7 +3979,7 @@ def save_employee_workflow_task( engagement_tasks_for_gate = db.execute( _employee_work_task_query( - db, scope, assigned_only=True, financial_year=financial_year + db, scope, assigned_only=False, financial_year=financial_year ) .where(ClientServiceTaskInstance.subscription_id == task.subscription_id) .order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc()) @@ -4025,7 +4033,7 @@ def save_employee_workflow_task( engagement_tasks = db.execute( _employee_work_task_query( - db, scope, assigned_only=True, financial_year=financial_year + db, scope, assigned_only=False, financial_year=financial_year ) .where(ClientServiceTaskInstance.subscription_id == task.subscription_id) .order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc()) diff --git a/app/modules/manager_dashboard/service.py b/app/modules/manager_dashboard/service.py index 9c34993..1519cf5 100644 --- a/app/modules/manager_dashboard/service.py +++ b/app/modules/manager_dashboard/service.py @@ -309,7 +309,19 @@ def _advanced_engagement_rows(tasks: list[ClientServiceTaskInstance]) -> list[di team = _engagement_team(subscription) status_values = {(getattr(t, "status", None) or "pending").strip().lower() for t in group} completed = sum(1 for t in group if (getattr(t, "status", None) or "pending").strip().lower() in CLOSED_TASK_STATUSES) - review_pending = sum(1 for t in group if (getattr(t, "manager_review_status", None) or "") == "pending" or (getattr(t, "partner_review_status", None) or "") == "pending" or (getattr(t, "review_partner_review_status", None) or "") == "pending") + review_pending = sum( + 1 + for t in group + if ( + getattr(t, "normal_review_required", False) + and (getattr(t, "normal_review_role", None) or "").strip().lower() in {"manager", "manager_or_partner"} + and (getattr(t, "normal_review_status", None) or "not_required") in {"pending", "rework_required"} + ) + or ( + getattr(t, "aqmm_manager_review_required", False) + and (getattr(t, "manager_review_status", None) or "not_required") in {"pending", "rework_required"} + ) + ) if completed == len(group) and group: status = "completed" elif "blocked" in status_values or getattr(subscription, "workflow_pause_reason", None): @@ -482,19 +494,33 @@ _MANAGER_REVIEW_DONE = {"reviewed"} _MANAGER_REVIEW_REWORK = {"rework_required"} -def _manager_review_task_state(task: ClientServiceTaskInstance) -> str: +def _manager_review_task_state(task: ClientServiceTaskInstance) -> tuple[str, str | None]: + normal_role = (getattr(task, "normal_review_role", None) or "").strip().lower() + normal_status = (getattr(task, "normal_review_status", None) or "not_required").strip().lower() manager_status = (getattr(task, "manager_review_status", None) or "not_required").strip().lower() - task_status = (getattr(task, "status", None) or "pending").strip().lower() rework_status = (getattr(task, "rework_status", None) or "none").strip().lower() - if manager_status in _MANAGER_REVIEW_REWORK or rework_status == "open": - return "rework" - if manager_status in _MANAGER_REVIEW_DONE: - return "reviewed" - if manager_status in _MANAGER_REVIEW_PENDING or task_status in REVIEW_STATUSES: - return "pending_review" - if getattr(task, "aqmm_manager_review_required", False) and task_status in CLOSED_TASK_STATUSES: - return "pending_review" - return "not_ready" + + if getattr(task, "normal_review_required", False) and normal_role in {"manager", "manager_or_partner"}: + if normal_status == "rework_required" or rework_status == "open": + return "rework", "normal" + if normal_status == "reviewed": + if not getattr(task, "aqmm_manager_review_required", False): + return "reviewed", "normal" + elif normal_status == "pending": + return "pending_review", "normal" + + if getattr(task, "aqmm_manager_review_required", False): + if manager_status in _MANAGER_REVIEW_REWORK or rework_status == "open": + return "rework", "aqmm_manager" + if manager_status in _MANAGER_REVIEW_DONE: + return "reviewed", "aqmm_manager" + if manager_status in _MANAGER_REVIEW_PENDING: + return "pending_review", "aqmm_manager" + task_status = (getattr(task, "status", None) or "pending").strip().lower() + if task_status in CLOSED_TASK_STATUSES: + return "pending_review", "aqmm_manager" + + return "not_ready", None def _manager_review_category(task: ClientServiceTaskInstance) -> str: @@ -502,7 +528,7 @@ def _manager_review_category(task: ClientServiceTaskInstance) -> str: def _manager_review_task_row(task: ClientServiceTaskInstance) -> dict[str, Any]: - state = _manager_review_task_state(task) + state, review_kind = _manager_review_task_state(task) comments = [c for c in (getattr(task, "comments", None) or []) if not getattr(c, "is_deleted", False)] documents = list(getattr(task, "documents", None) or []) assignee = getattr(task, "assigned_to", None) @@ -516,6 +542,10 @@ def _manager_review_task_row(task: ClientServiceTaskInstance) -> dict[str, Any]: "task_status_label": _status_label(getattr(task, "status", None)), "review_state": state, "review_state_label": state.replace("_", " ").title(), + "review_kind": review_kind, + "review_kind_label": "Normal Review" if review_kind == "normal" else ("AQMM Manager Review" if review_kind == "aqmm_manager" else ""), + "normal_review_status": (getattr(task, "normal_review_status", None) or "not_required").strip().lower(), + "normal_review_note": getattr(task, "normal_review_note", None) or "", "manager_review_status": (getattr(task, "manager_review_status", None) or "not_required").strip().lower(), "manager_review_note": getattr(task, "manager_review_note", None) or "", "rework_status": (getattr(task, "rework_status", None) or "none").strip().lower(), diff --git a/app/modules/manager_dashboard/templates/manager_dashboard/manager_review_workspace.html b/app/modules/manager_dashboard/templates/manager_dashboard/manager_review_workspace.html index 513d9a0..50b3e0f 100644 --- a/app/modules/manager_dashboard/templates/manager_dashboard/manager_review_workspace.html +++ b/app/modules/manager_dashboard/templates/manager_dashboard/manager_review_workspace.html @@ -57,7 +57,7 @@
{{ task.category }}

{{ task.task_name }}

{{ task.description }}

{{ task.task_status_label }}{% if task.is_aqmm_task %}AQMM{% endif %}{% if task.blocks_final_release %}Final Gate{% endif %}
-
Prepared By
{{ task.assigned_to }}
Evidence
{{ task.evidence_count }} document(s)
Comments
{{ task.comment_count }}
Manager Review
{{ task.review_state_label }}
+
Prepared By
{{ task.assigned_to }}
Evidence
{{ task.evidence_count }} document(s)
Comments
{{ task.comment_count }}
Manager Review
{{ task.review_state_label }}
{{ task.review_kind_label }}
@@ -66,12 +66,12 @@
-

Manager Review Decision

+

{{ task.review_kind_label or "Manager Review" }} Decision

{% if task.is_locked or workspace.is_locked %}
This task or engagement is locked. Review is read-only.
{% endif %}
- +
Close Workspace
diff --git a/app/modules/manager_dashboard/ui.py b/app/modules/manager_dashboard/ui.py index 81310f2..fb42707 100644 --- a/app/modules/manager_dashboard/ui.py +++ b/app/modules/manager_dashboard/ui.py @@ -15,7 +15,7 @@ from app.modules.manager_dashboard.service import ( get_manager_review_workspace, get_next_manager_review_task_id, ) -from app.modules.services.execution import apply_task_review +from app.modules.services.execution import apply_normal_task_review, apply_task_review from app.modules.alerts.workflow_escalations import update_workflow_escalation router = APIRouter(prefix="/manager", tags=["manager-dashboard-v2-ui"]) @@ -160,14 +160,23 @@ def manager_review_task_submit( if not task: return RedirectResponse(url="/manager/dashboard?tab=review-queue", status_code=303) try: - apply_task_review( - db, - task=task, - review_level="manager", - decision=decision, - note=review_note, - user_id=current_user.id, - ) + if active.get("review_kind") == "normal": + apply_normal_task_review( + db, + task=task, + decision=decision, + note=review_note, + user_id=current_user.id, + ) + else: + apply_task_review( + db, + task=task, + review_level="manager", + decision=decision, + note=review_note, + user_id=current_user.id, + ) db.commit() except ValueError: db.rollback() diff --git a/app/modules/partner_dashboard/service.py b/app/modules/partner_dashboard/service.py index c6d96a6..afaac6c 100644 --- a/app/modules/partner_dashboard/service.py +++ b/app/modules/partner_dashboard/service.py @@ -446,16 +446,21 @@ def _partner_task_category(task: ClientServiceTaskInstance) -> str: def _partner_review_level_for_task(task: ClientServiceTaskInstance, subscription: ClientServiceSubscription, current_user) -> str | None: - """Return the review level the current partner is authorised to perform. - - Review Partner work is kept separate from Engagement Partner work. System and - Firm Admin users may inspect the workspace, but review decisions are selected - from the engagement assignments rather than from a new permission model. - """ + """Return the review level the current partner is authorised to perform.""" uid = int(getattr(current_user, "id", 0) or 0) is_review_partner = uid and uid == int(getattr(subscription, "review_partner_user_id", 0) or 0) is_engagement_partner = uid and uid == int(getattr(subscription, "assigned_partner_user_id", 0) or 0) + normal_role = (getattr(task, "normal_review_role", None) or "").strip().lower() + normal_status = (getattr(task, "normal_review_status", None) or "not_required").strip().lower() + if ( + is_engagement_partner + and getattr(task, "normal_review_required", False) + and normal_role in {"partner", "manager_or_partner"} + and normal_status in {"pending", "reviewed", "rework_required"} + ): + return "normal_partner" + if is_review_partner and getattr(task, "aqmm_review_partner_required", False): return "review_partner" if is_engagement_partner and getattr(task, "aqmm_partner_review_required", False): @@ -464,6 +469,14 @@ def _partner_review_level_for_task(task: ClientServiceTaskInstance, subscription def _partner_review_state(task: ClientServiceTaskInstance, review_level: str | None) -> str: + if review_level == "normal_partner": + status = (getattr(task, "normal_review_status", "not_required") or "not_required").strip().lower() + if status == "reviewed": + return "reviewed" + if status == "rework_required" or getattr(task, "rework_status", "none") == "open": + return "rework" + return "pending_review" if status == "pending" else "not_ready" + if getattr(task, "rework_status", "none") == "open": return "rework" if review_level == "review_partner": @@ -476,10 +489,8 @@ def _partner_review_state(task: ClientServiceTaskInstance, review_level: str | N return "reviewed" if status == "rework_required": return "rework" - # Partner review must follow any required Manager review. if getattr(task, "aqmm_manager_review_required", False) and getattr(task, "manager_review_status", "not_required") != "reviewed": return "not_ready" - # Review Partner review must follow required Engagement Partner review. if review_level == "review_partner" and getattr(task, "aqmm_partner_review_required", False) and getattr(task, "partner_review_status", "not_required") != "reviewed": return "not_ready" if (getattr(task, "status", "pending") or "pending") not in CLOSED_TASK_STATUSES and getattr(task, "submitted_for_review_at_utc", None) is None: @@ -523,7 +534,10 @@ def _partner_task_payload(task: ClientServiceTaskInstance, subscription: ClientS "blocks_final_release": bool(getattr(task, "aqmm_blocks_final_release", False)), "partner_review_required": bool(getattr(task, "aqmm_partner_review_required", False)), "review_partner_required": bool(getattr(task, "aqmm_review_partner_required", False)), + "normal_review_status": getattr(task, "normal_review_status", "not_required"), + "normal_review_note": getattr(task, "normal_review_note", None), "review_level": review_level, + "review_kind_label": "Normal Review" if review_level == "normal_partner" else ("AQMM Review Partner" if review_level == "review_partner" else ("AQMM Partner Review" if review_level == "partner" else "")), "review_state": review_state, "can_review": review_level is not None and review_state in {"pending_review", "reviewed", "rework"}, "is_exception": getattr(task, "checklist_response", None) == "NO" or getattr(task, "rework_status", "none") == "open", @@ -562,12 +576,18 @@ def _partner_review_queue_rows(db: Session, request, current_user, *, limit: int ClientServiceTaskInstance.is_active.is_(True), ) tasks = list(db.execute(task_stmt.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())).scalars().all()) + pending_normal_partner = sum( + 1 for t in tasks + if getattr(t, "normal_review_required", False) + and (getattr(t, "normal_review_role", None) or "").strip().lower() in {"partner", "manager_or_partner"} + and (getattr(t, "normal_review_status", None) or "not_required") in {"pending", "rework_required"} + ) pending_partner = sum(1 for t in tasks if getattr(t, "aqmm_partner_review_required", False) and getattr(t, "partner_review_status", "not_required") != "reviewed") pending_review_partner = sum(1 for t in tasks if getattr(t, "aqmm_review_partner_required", False) and getattr(t, "review_partner_review_status", "not_required") != "reviewed") rework = sum(1 for t in tasks if getattr(t, "rework_status", "none") == "open") blockers = sum(1 for t in tasks if getattr(t, "aqmm_blocks_final_release", False) and getattr(t, "aqmm_status", "not_required") != "completed") exceptions = sum(1 for t in tasks if getattr(t, "checklist_response", None) == "NO") - if not any((pending_partner, pending_review_partner, rework, blockers, exceptions)): + if not any((pending_normal_partner, pending_partner, pending_review_partner, rework, blockers, exceptions)): continue weighted = _weighted_progress(tasks) sla = _engagement_sla(sub, tasks) @@ -576,6 +596,7 @@ def _partner_review_queue_rows(db: Session, request, current_user, *, limit: int "client_name": getattr(getattr(sub, "client", None), "client_name", None) or "Unlinked Client", "service_name": getattr(getattr(sub, "catalogue", None), "service_name", None) or "Service", "financial_year": sub.financial_year or "-", + "pending_normal_partner": pending_normal_partner, "pending_partner": pending_partner, "pending_review_partner": pending_review_partner, "rework_count": rework, @@ -585,7 +606,7 @@ def _partner_review_queue_rows(db: Session, request, current_user, *, limit: int "sla": sla, "href": f"/partner/reviews/engagements/{sub.id}", }) - rows.sort(key=lambda r: (r["release_blockers"], r["rework_count"], r["pending_partner"] + r["pending_review_partner"]), reverse=True) + rows.sort(key=lambda r: (r["release_blockers"], r["rework_count"], r["pending_normal_partner"] + r["pending_partner"] + r["pending_review_partner"]), reverse=True) return rows diff --git a/app/modules/partner_dashboard/templates/partner_dashboard/partner_review_workspace.html b/app/modules/partner_dashboard/templates/partner_dashboard/partner_review_workspace.html index b9b5e33..6b664ee 100644 --- a/app/modules/partner_dashboard/templates/partner_dashboard/partner_review_workspace.html +++ b/app/modules/partner_dashboard/templates/partner_dashboard/partner_review_workspace.html @@ -63,7 +63,7 @@
{% if active %} -
{{ active.category }}

{{ active.task_name }}

{{ active.description or 'No additional procedure description.' }}

Assigned: {{ active.assigned_to }}{% if active.review_level %}{{ active.review_level.replace('_',' ').title() }}{% endif %}
+
{{ active.category }}

{{ active.task_name }}

{{ active.description or 'No additional procedure description.' }}

Assigned: {{ active.assigned_to }}{% if active.review_level %}{{ active.review_kind_label or active.review_level.replace('_',' ').title() }}{% endif %}

Staff Response & Conclusion

Response
{{ active.checklist_response or active.checklist_text_response or active.checklist_number_response or active.checklist_date_response or '-' }}
Checklist remarks
{{ active.checklist_remarks or '-' }}
Work remarks
{{ active.work_remarks or '-' }}
@@ -80,7 +80,7 @@
- +
Close Workspace
{% else %} diff --git a/app/modules/partner_dashboard/ui.py b/app/modules/partner_dashboard/ui.py index 2653043..caf25ad 100644 --- a/app/modules/partner_dashboard/ui.py +++ b/app/modules/partner_dashboard/ui.py @@ -15,7 +15,7 @@ from app.modules.partner_dashboard.service import ( get_next_partner_review_task_id, get_partner_review_workspace, ) -from app.modules.services.execution import apply_task_review +from app.modules.services.execution import apply_normal_task_review, apply_task_review from app.modules.alerts.workflow_escalations import update_workflow_escalation router = APIRouter(prefix="/partner", tags=["partner-dashboard-v2-ui"]) @@ -137,7 +137,7 @@ def partner_review_task_submit( return RedirectResponse(url=f"/partner/reviews/engagements/{subscription_id}?review_error=task", status_code=303) if active.get("is_locked") or workspace.get("is_locked"): return RedirectResponse(url=f"/partner/reviews/engagements/{subscription_id}?task_id={task_id}&review_error=locked", status_code=303) - if not active.get("can_review") or active.get("review_level") not in {"partner", "review_partner"}: + if not active.get("can_review") or active.get("review_level") not in {"normal_partner", "partner", "review_partner"}: return RedirectResponse(url=f"/partner/reviews/engagements/{subscription_id}?task_id={task_id}&review_error=not_ready", status_code=303) from sqlalchemy import select @@ -149,14 +149,23 @@ def partner_review_task_submit( if not task: return RedirectResponse(url="/partner/dashboard?tab=review", status_code=303) try: - apply_task_review( - db, - task=task, - review_level=active["review_level"], - decision=decision, - note=review_note, - user_id=current_user.id, - ) + if active.get("review_level") == "normal_partner": + apply_normal_task_review( + db, + task=task, + decision=decision, + note=review_note, + user_id=current_user.id, + ) + else: + apply_task_review( + db, + task=task, + review_level=active["review_level"], + decision=decision, + note=review_note, + user_id=current_user.id, + ) db.commit() except ValueError: db.rollback() diff --git a/app/modules/partners/templates/partners/review_board.html b/app/modules/partners/templates/partners/review_board.html index a73d832..6e60669 100644 --- a/app/modules/partners/templates/partners/review_board.html +++ b/app/modules/partners/templates/partners/review_board.html @@ -111,7 +111,7 @@ - {{ task.client_display }} + {{ task.client_display }}
{{ task.engagement_label }}
Engagement #{{ task.subscription_id }}
@@ -157,7 +157,7 @@
{% if task.internal_target_date %}Target {{ task.internal_target_date }}{% else %}No task target{% endif %}
- Open Review + Open Review {% endif %} diff --git a/app/modules/partners/ui.py b/app/modules/partners/ui.py index 63fc973..1404e34 100644 --- a/app/modules/partners/ui.py +++ b/app/modules/partners/ui.py @@ -152,23 +152,40 @@ def _decorate_task(task: ClientServiceTaskInstance, today: date) -> ClientServic return task -def _task_bucket(task: ClientServiceTaskInstance) -> str: +def _task_bucket(task: ClientServiceTaskInstance, current_user) -> str: status = (task.status or "pending").strip().lower() - if status == "blocked": + subscription = getattr(task, "subscription", None) + uid = int(getattr(current_user, "id", 0) or 0) + is_engagement_partner = uid and uid == int(getattr(subscription, "assigned_partner_user_id", 0) or 0) + is_review_partner = uid and uid == int(getattr(subscription, "review_partner_user_id", 0) or 0) + + normal_role = (getattr(task, "normal_review_role", None) or "").strip().lower() + normal_status = (getattr(task, "normal_review_status", None) or "not_required").strip().lower() + normal_partner_review = ( + is_engagement_partner + and getattr(task, "normal_review_required", False) + and normal_role in {"partner", "manager_or_partner"} + ) + aqmm_partner_review = is_engagement_partner and getattr(task, "aqmm_partner_review_required", False) + aqmm_review_partner_review = is_review_partner and getattr(task, "aqmm_review_partner_required", False) + + if status == "blocked" and getattr(task, "rework_status", "none") != "open": return "clarification_required" - if status == "completed": + if ( + (normal_partner_review and normal_status == "rework_required") + or (aqmm_partner_review and getattr(task, "partner_review_status", "not_required") == "rework_required") + or (aqmm_review_partner_review and getattr(task, "review_partner_review_status", "not_required") == "rework_required") + or getattr(task, "rework_status", "none") == "open" + ): + return "rework_sent" + if ( + (normal_partner_review and normal_status == "pending") + or (aqmm_partner_review and getattr(task, "partner_review_status", "not_required") == "pending") + or (aqmm_review_partner_review and getattr(task, "review_partner_review_status", "not_required") == "pending") + ): return "pending_review" if status in CLOSED_TASK_STATUSES: return "completed" - if status in {"rework", "rework_required"}: - return "rework_sent" - if status == "pending": - has_partner_rework = any( - (getattr(comment, "comment_type", "") or "").strip().lower() == "partner_review_note" - and not getattr(comment, "is_deleted", False) - for comment in getattr(task, "comments", []) - ) - return "rework_sent" if has_partner_rework else "approved" return "approved" @@ -186,6 +203,7 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue), selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_manager), selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_staff), + selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.review_partner), selectinload(ClientServiceTaskInstance.assigned_to), selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by), ) @@ -227,7 +245,8 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str summary["clients"].add(task.client_id) if getattr(task, "subscription_id", None): summary["engagements"].add(task.subscription_id) - if status == "completed": + bucket = _task_bucket(task, current_user) + if bucket == "pending_review": summary["pending_review"] += 1 if status == "blocked": summary["blocked"] += 1 @@ -237,7 +256,7 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str summary["due_today"] += 1 if status in CLOSED_TASK_STATUSES: summary["completed"] += 1 - lookup[_task_bucket(task)]["tasks"].append(task) + lookup[bucket]["tasks"].append(task) summary["clients"] = len(summary["clients"]) summary["engagements"] = len(summary["engagements"]) diff --git a/app/modules/services/execution.py b/app/modules/services/execution.py index 08df63f..ddaf843 100644 --- a/app/modules/services/execution.py +++ b/app/modules/services/execution.py @@ -31,6 +31,29 @@ TASK_STATUSES = [ OPEN_TASK_STATUSES = {"pending", "in_progress", "blocked"} CLOSED_TASK_STATUSES = {"completed", "not_applicable", "cancelled"} +NORMAL_REVIEW_ROLES = {"manager", "partner", "manager_or_partner"} + + +def normal_review_role_for_template(template) -> str | None: + if not getattr(template, "requires_review", False): + return None + explicit = (getattr(template, "normal_review_role", None) or "").strip().lower() + if explicit in NORMAL_REVIEW_ROLES: + return explicit + default_role = (getattr(template, "default_role_name", None) or "").strip().lower() + eligible = { + part.strip().lower() + for part in (getattr(template, "eligible_role_names", None) or "").split(",") + if part.strip() + } + if default_role in {"manager", "branch manager"} and "partner" in eligible: + return "partner" + if default_role not in {"manager", "branch manager", "partner"} and ({"manager", "branch manager"} & eligible): + return "manager" + if "partner" in eligible and not ({"manager", "branch manager"} & eligible): + return "partner" + return "manager" + TASK_COMMENT_TYPES = [ ("internal_note", "Internal Note"), @@ -307,6 +330,9 @@ def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceS response_type=(getattr(template, "response_type", "NONE") or "NONE").upper(), evidence_required=getattr(template, "evidence_required", False), remarks_required_if_no=getattr(template, "remarks_required_if_no", False), + normal_review_required=bool(getattr(template, "requires_review", False)), + normal_review_role=normal_review_role_for_template(template), + normal_review_status="not_ready" if getattr(template, "requires_review", False) else "not_required", is_aqmm_task=getattr(template, "is_aqmm_task", False), aqmm_mandatory=getattr(template, "aqmm_mandatory", False), aqmm_evidence_required=getattr(template, "aqmm_evidence_required", False), @@ -372,6 +398,12 @@ def _copy_template_snapshot_to_task( task.response_type = (getattr(template, "response_type", "NONE") or "NONE").upper() task.evidence_required = getattr(template, "evidence_required", False) task.remarks_required_if_no = getattr(template, "remarks_required_if_no", False) + task.normal_review_required = bool(getattr(template, "requires_review", False)) + task.normal_review_role = normal_review_role_for_template(template) + if not task.normal_review_required: + task.normal_review_status = "not_required" + elif (getattr(task, "normal_review_status", None) or "not_required") == "not_required": + task.normal_review_status = "not_ready" task.is_aqmm_task = getattr(template, "is_aqmm_task", False) task.aqmm_mandatory = getattr(template, "aqmm_mandatory", False) task.aqmm_evidence_required = getattr(template, "aqmm_evidence_required", False) @@ -465,6 +497,9 @@ def sync_open_engagement_tasks_for_service( response_type=(getattr(template, "response_type", "NONE") or "NONE").upper(), evidence_required=getattr(template, "evidence_required", False), remarks_required_if_no=getattr(template, "remarks_required_if_no", False), + normal_review_required=bool(getattr(template, "requires_review", False)), + normal_review_role=normal_review_role_for_template(template), + normal_review_status="not_ready" if getattr(template, "requires_review", False) else "not_required", is_aqmm_task=getattr(template, "is_aqmm_task", False), aqmm_mandatory=getattr(template, "aqmm_mandatory", False), aqmm_evidence_required=getattr(template, "aqmm_evidence_required", False), @@ -1323,6 +1358,11 @@ def submit_task_for_review(db: Session, *, task: ClientServiceTaskInstance, note now = datetime.now(timezone.utc) task.submitted_for_review_by_user_id = user_id task.submitted_for_review_at_utc = now + if getattr(task, "normal_review_required", False): + task.normal_review_status = "pending" + task.normal_review_note = None + task.normal_reviewed_by_user_id = None + task.normal_reviewed_at_utc = None if getattr(task, "aqmm_manager_review_required", False) and task.manager_review_status != "reviewed": task.manager_review_status = "pending" if getattr(task, "aqmm_partner_review_required", False) and task.partner_review_status != "reviewed": @@ -1339,6 +1379,53 @@ def submit_task_for_review(db: Session, *, task: ClientServiceTaskInstance, note recalculate_task_aqmm_status(db, task) + +def apply_normal_task_review( + db: Session, + *, + task: ClientServiceTaskInstance, + decision: str, + note: str, + user_id: int, +) -> None: + if getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False): + return + if not getattr(task, "normal_review_required", False): + raise ValueError("Normal review is not required for this task.") + clean_decision = (decision or "reviewed").strip().lower() + if clean_decision not in {"reviewed", "rework_required"}: + clean_decision = "reviewed" + clean_note = (note or "").strip() + if clean_decision == "rework_required" and not clean_note: + raise ValueError("Rework reason is required.") + now = datetime.now(timezone.utc) + task.normal_review_status = clean_decision + task.normal_review_note = clean_note or task.normal_review_note + task.normal_reviewed_by_user_id = user_id + task.normal_reviewed_at_utc = now + if clean_decision == "rework_required": + task.rework_status = "open" + task.rework_reason = clean_note + task.rework_requested_by_user_id = user_id + task.rework_requested_at_utc = now + task.rework_resolved_at_utc = None + task.status = "blocked" + comment_type = "rework_note" + else: + if getattr(task, "rework_status", "none") == "open": + task.rework_status = "resolved" + task.rework_resolved_at_utc = now + comment_type = "manager_review_note" if (getattr(task, "normal_review_role", "") or "").startswith("manager") else "partner_review_note" + task.updated_by_user_id = user_id + _append_system_task_comment( + db, + task=task, + comment_type=comment_type, + message=clean_note or ("Normal review approved." if clean_decision == "reviewed" else clean_note), + user_id=user_id, + ) + + def apply_task_review( db: Session, *, diff --git a/app/modules/services/models.py b/app/modules/services/models.py index 212a7a6..b93b0cc 100644 --- a/app/modules/services/models.py +++ b/app/modules/services/models.py @@ -114,6 +114,7 @@ class ServiceDefaultTaskTemplate(CommonBase): eligible_role_names: Mapped[str | None] = mapped_column(String(200), nullable=True) is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + normal_review_role: Mapped[str | None] = mapped_column(String(30), nullable=True) # AQMM task tags. These flags allow the existing service checklist to become # the engagement quality checklist for assurance engagements, without creating @@ -183,6 +184,7 @@ class FirmServiceTaskTemplate(CommonBase): eligible_role_names: Mapped[str | None] = mapped_column(String(200), nullable=True) is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + normal_review_role: Mapped[str | None] = mapped_column(String(30), nullable=True) # AQMM task tags copied into generated ClientServiceTaskInstance rows. is_aqmm_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True) @@ -783,6 +785,14 @@ class ClientServiceTaskInstance(CommonBase): submitted_for_review_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True) submitted_for_review_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + # Normal operational review is intentionally separate from AQMM review. + normal_review_required: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True) + normal_review_role: Mapped[str | None] = mapped_column(String(30), nullable=True, index=True) + normal_review_status: Mapped[str] = mapped_column(String(30), nullable=False, default="not_required", index=True) + normal_review_note: Mapped[str | None] = mapped_column(Text, nullable=True) + normal_reviewed_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True) + normal_reviewed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + manager_review_status: Mapped[str] = mapped_column(String(30), nullable=False, default="not_required", index=True) manager_review_note: Mapped[str | None] = mapped_column(Text, nullable=True) manager_reviewed_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True) diff --git a/app/modules/services/templates/services/default_task_form.html b/app/modules/services/templates/services/default_task_form.html index 25b80e6..e4848a3 100644 --- a/app/modules/services/templates/services/default_task_form.html +++ b/app/modules/services/templates/services/default_task_form.html @@ -56,6 +56,14 @@ Review +
@@ -44,7 +44,7 @@
-
+
AQMM / Quality Control Tagging
diff --git a/app/modules/services/templates/services/firm_task_form.html b/app/modules/services/templates/services/firm_task_form.html index d347da5..46956e4 100644 --- a/app/modules/services/templates/services/firm_task_form.html +++ b/app/modules/services/templates/services/firm_task_form.html @@ -56,6 +56,14 @@ Review +
@@ -165,7 +165,7 @@
{% if task.is_mandatory %}Mandatory{% endif %} - {% if task.requires_review %}Review{% endif %} + {% if task.requires_review %}Review: {{ (task.normal_review_role or "manager").replace("_"," ").title() }}{% endif %} {% if task.is_aqmm_task %}AQMM{% endif %} {% if task.aqmm_mandatory %}AQMM Mandatory{% endif %} {% if task.aqmm_evidence_required %}Evidence{% endif %} diff --git a/app/modules/services/ui.py b/app/modules/services/ui.py index 48a9eab..390e235 100644 --- a/app/modules/services/ui.py +++ b/app/modules/services/ui.py @@ -138,6 +138,15 @@ def _validated_registration_type_code(db, value: str, *, existing_value: str | N raise ValueError("registration_type") +def _normal_review_role_value(value: str, *, requires_review: bool) -> str | None: + if not requires_review: + return None + clean = (value or "").strip().lower() + if clean in {"manager", "partner", "manager_or_partner"}: + return clean + return "manager" + + def _redirect_denied(): from app.core.http_responses import ui_access_denied return ui_access_denied() @@ -948,6 +957,7 @@ def _copy_system_defaults_if_firm_tasks_empty( eligible_role_names=getattr(default, "eligible_role_names", None) or default.default_role_name, is_mandatory=default.is_mandatory, requires_review=default.requires_review, + normal_review_role=getattr(default, "normal_review_role", None), is_aqmm_task=getattr(default, "is_aqmm_task", False), aqmm_mandatory=getattr(default, "aqmm_mandatory", False), aqmm_evidence_required=getattr(default, "aqmm_evidence_required", False), @@ -1241,7 +1251,7 @@ def task_templates_detail(request: Request, catalogue_id: int): @router.post('/templates/{catalogue_id}/tasks/new') -def task_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), eligible_roles: list[str] = Form([]), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), evidence_required: str | None = Form(None), remarks_required_if_no: str | None = Form(None), is_aqmm_task: str | None = Form(None), aqmm_mandatory: str | None = Form(None), aqmm_evidence_required: str | None = Form(None), aqmm_manager_review_required: str | None = Form(None), aqmm_partner_review_required: str | None = Form(None), aqmm_review_partner_required: str | None = Form(None), aqmm_blocks_final_release: str | None = Form(None), aqmm_reference: str = Form(''), is_active: str | None = Form(None), csrf_token: str = Form(...)): +def task_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), eligible_roles: list[str] = Form([]), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), normal_review_role: str = Form('manager'), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), evidence_required: str | None = Form(None), remarks_required_if_no: str | None = Form(None), is_aqmm_task: str | None = Form(None), aqmm_mandatory: str | None = Form(None), aqmm_evidence_required: str | None = Form(None), aqmm_manager_review_required: str | None = Form(None), aqmm_partner_review_required: str | None = Form(None), aqmm_review_partner_required: str | None = Form(None), aqmm_blocks_final_release: str | None = Form(None), aqmm_reference: str = Form(''), is_active: str | None = Form(None), csrf_token: str = Form(...)): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: @@ -1267,6 +1277,7 @@ def task_template_create_submit(request: Request, catalogue_id: int, task_name: sequence_no=sequence_no or next_task_sequence(db, tenant_id=tenant_id, catalogue_id=catalogue_id), is_mandatory=is_mandatory is not None, requires_review=requires_review is not None, + normal_review_role=_normal_review_role_value(normal_review_role, requires_review=requires_review is not None), task_category=task_category.strip() or None, response_required=response_required is not None, response_type=(response_type.strip().upper() or "NONE"), @@ -1351,7 +1362,7 @@ def default_templates_detail(request: Request, catalogue_id: int): @router.post('/catalogue/{catalogue_id}/defaults/new') -def default_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), evidence_required: str | None = Form(None), remarks_required_if_no: str | None = Form(None), is_aqmm_task: str | None = Form(None), aqmm_mandatory: str | None = Form(None), aqmm_evidence_required: str | None = Form(None), aqmm_manager_review_required: str | None = Form(None), aqmm_partner_review_required: str | None = Form(None), aqmm_review_partner_required: str | None = Form(None), aqmm_blocks_final_release: str | None = Form(None), aqmm_reference: str = Form(''), is_active: str | None = Form(None), csrf_token: str = Form(...)): +def default_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), normal_review_role: str = Form('manager'), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), evidence_required: str | None = Form(None), remarks_required_if_no: str | None = Form(None), is_aqmm_task: str | None = Form(None), aqmm_mandatory: str | None = Form(None), aqmm_evidence_required: str | None = Form(None), aqmm_manager_review_required: str | None = Form(None), aqmm_partner_review_required: str | None = Form(None), aqmm_review_partner_required: str | None = Form(None), aqmm_blocks_final_release: str | None = Form(None), aqmm_reference: str = Form(''), is_active: str | None = Form(None), csrf_token: str = Form(...)): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: @@ -1364,7 +1375,7 @@ def default_template_create_submit(request: Request, catalogue_id: int, task_nam catalogue = get_catalogue(db, catalogue_id) if not catalogue: return RedirectResponse(url='/services/defaults', status_code=303) - row = ServiceDefaultTaskTemplate(service_catalogue_id=catalogue_id, task_name=task_name.strip(), description=description.strip() or None, default_role_name=default_role_name.strip() or None, sequence_no=sequence_no or next_default_task_sequence(db, catalogue_id=catalogue_id), is_mandatory=is_mandatory is not None, requires_review=requires_review is not None, task_category=task_category.strip() or None, response_required=response_required is not None, response_type=(response_type.strip().upper() or "NONE"), evidence_required=evidence_required is not None, remarks_required_if_no=remarks_required_if_no is not None, is_aqmm_task=is_aqmm_task is not None, aqmm_mandatory=aqmm_mandatory is not None, aqmm_evidence_required=aqmm_evidence_required is not None, aqmm_manager_review_required=aqmm_manager_review_required is not None, aqmm_partner_review_required=aqmm_partner_review_required is not None, aqmm_review_partner_required=aqmm_review_partner_required is not None, aqmm_blocks_final_release=aqmm_blocks_final_release is not None, aqmm_reference=aqmm_reference.strip() or None, is_active=is_active is not None) + row = ServiceDefaultTaskTemplate(service_catalogue_id=catalogue_id, task_name=task_name.strip(), description=description.strip() or None, default_role_name=default_role_name.strip() or None, sequence_no=sequence_no or next_default_task_sequence(db, catalogue_id=catalogue_id), is_mandatory=is_mandatory is not None, requires_review=requires_review is not None, normal_review_role=_normal_review_role_value(normal_review_role, requires_review=requires_review is not None), task_category=task_category.strip() or None, response_required=response_required is not None, response_type=(response_type.strip().upper() or "NONE"), evidence_required=evidence_required is not None, remarks_required_if_no=remarks_required_if_no is not None, is_aqmm_task=is_aqmm_task is not None, aqmm_mandatory=aqmm_mandatory is not None, aqmm_evidence_required=aqmm_evidence_required is not None, aqmm_manager_review_required=aqmm_manager_review_required is not None, aqmm_partner_review_required=aqmm_partner_review_required is not None, aqmm_review_partner_required=aqmm_review_partner_required is not None, aqmm_blocks_final_release=aqmm_blocks_final_release is not None, aqmm_reference=aqmm_reference.strip() or None, is_active=is_active is not None) db.add(row); db.commit() return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults', status_code=303) finally: @@ -1403,6 +1414,7 @@ def copy_defaults_to_firm(request: Request, catalogue_id: int, csrf_token: str = default_role_name=d.default_role_name, is_mandatory=d.is_mandatory, requires_review=d.requires_review, + normal_review_role=getattr(d, "normal_review_role", None), task_category=getattr(d, "task_category", None), response_required=getattr(d, "response_required", False), response_type=getattr(d, "response_type", "NONE") or "NONE", @@ -1707,6 +1719,7 @@ def firm_task_template_edit_submit( sequence_no: int = Form(1), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), + normal_review_role: str = Form('manager'), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), @@ -1753,6 +1766,7 @@ def firm_task_template_edit_submit( task.sequence_no = sequence_no task.is_mandatory = is_mandatory is not None task.requires_review = requires_review is not None + task.normal_review_role = _normal_review_role_value(normal_review_role, requires_review=task.requires_review) task.task_category = task_category.strip() or None task.response_required = response_required is not None task.response_type = response_type.strip().upper() or "NONE" @@ -1849,6 +1863,7 @@ def default_task_template_edit_submit( sequence_no: int = Form(1), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), + normal_review_role: str = Form('manager'), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), @@ -1895,6 +1910,7 @@ def default_task_template_edit_submit( task.sequence_no = sequence_no task.is_mandatory = is_mandatory is not None task.requires_review = requires_review is not None + task.normal_review_role = _normal_review_role_value(normal_review_role, requires_review=task.requires_review) task.task_category = task_category.strip() or None task.response_required = response_required is not None task.response_type = response_type.strip().upper() or "NONE"