From 05690a220c834b43da5631024061e5509b5e9866 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Tue, 7 Jul 2026 11:30:59 +0530 Subject: [PATCH] Add AQMM quality tags to existing service task checklist --- ...260625_phase_3_1_aqmm_task_quality_tags.py | 125 ++++++++++++++++++ app/modules/services/bulk_imports.py | 32 +++++ app/modules/services/engagements_ui.py | 3 + app/modules/services/execution.py | 94 +++++++++++++ app/modules/services/models.py | 39 ++++++ .../templates/services/default_task_form.html | 18 +++ .../services/default_templates_detail.html | 15 ++- .../services/engagements/detail.html | 37 +++++- .../templates/services/firm_task_form.html | 18 +++ .../services/task_template_detail.html | 18 +++ app/modules/services/ui.py | 54 +++++++- 11 files changed, 448 insertions(+), 5 deletions(-) create mode 100644 alembic/versions/20260625_phase_3_1_aqmm_task_quality_tags.py diff --git a/alembic/versions/20260625_phase_3_1_aqmm_task_quality_tags.py b/alembic/versions/20260625_phase_3_1_aqmm_task_quality_tags.py new file mode 100644 index 0000000..d93615b --- /dev/null +++ b/alembic/versions/20260625_phase_3_1_aqmm_task_quality_tags.py @@ -0,0 +1,125 @@ +"""phase 3.1 aqmm quality tags on existing service tasks + +Revision ID: 20260625_phase_3_1_aqmm_task_quality_tags +Revises: 20260624_phase_3_aqmm_engagement_level_rework +Create Date: 2026-06-25 +""" +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +revision = "20260625_phase_3_1_aqmm_task_quality_tags" +down_revision = "20260624_phase_3_aqmm_engagement_level_rework" +branch_labels = None +depends_on = None + + +AQMM_TEMPLATE_COLUMNS = [ + sa.Column("is_aqmm_task", sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column("aqmm_mandatory", sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column("aqmm_evidence_required", sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column("aqmm_manager_review_required", sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column("aqmm_partner_review_required", sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column("aqmm_review_partner_required", sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column("aqmm_blocks_final_release", sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column("aqmm_reference", sa.String(length=120), nullable=True), +] + +AQMM_INSTANCE_COLUMNS = AQMM_TEMPLATE_COLUMNS + [ + sa.Column("aqmm_status", sa.String(length=40), nullable=False, server_default="not_required"), + sa.Column("aqmm_review_status", sa.String(length=40), nullable=False, server_default="not_required"), + sa.Column("aqmm_completed_at_utc", sa.DateTime(timezone=True), nullable=True), +] + + +def _has_table(bind, table_name: str) -> bool: + return sa.inspect(bind).has_table(table_name) + + +def _has_column(bind, table_name: str, column_name: str) -> bool: + if not _has_table(bind, table_name): + return False + return column_name in {c["name"] for c in sa.inspect(bind).get_columns(table_name)} + + +def _add_column_once(bind, table: str, column: sa.Column) -> None: + if not _has_column(bind, table, column.name): + with op.batch_alter_table(table) as batch: + batch.add_column(column) + + +def _drop_column_once(bind, table: str, column_name: str) -> None: + if _has_column(bind, table, column_name): + with op.batch_alter_table(table) as batch: + batch.drop_column(column_name) + + +def upgrade() -> None: + bind = op.get_bind() + + for table in ["service_default_task_templates", "firm_service_task_templates"]: + if _has_table(bind, table): + for column in AQMM_TEMPLATE_COLUMNS: + _add_column_once(bind, table, column.copy()) + + if _has_table(bind, "client_service_task_instances"): + for column in AQMM_INSTANCE_COLUMNS: + _add_column_once(bind, "client_service_task_instances", column.copy()) + + # Backfill existing generated tasks from their firm task templates. + op.execute(""" + UPDATE client_service_task_instances csti + SET is_aqmm_task = COALESCE(fst.is_aqmm_task, FALSE), + aqmm_mandatory = COALESCE(fst.aqmm_mandatory, FALSE), + aqmm_evidence_required = COALESCE(fst.aqmm_evidence_required, FALSE), + aqmm_manager_review_required = COALESCE(fst.aqmm_manager_review_required, FALSE), + aqmm_partner_review_required = COALESCE(fst.aqmm_partner_review_required, FALSE), + aqmm_review_partner_required = COALESCE(fst.aqmm_review_partner_required, FALSE), + aqmm_blocks_final_release = COALESCE(fst.aqmm_blocks_final_release, FALSE), + aqmm_reference = fst.aqmm_reference, + aqmm_status = CASE WHEN COALESCE(fst.is_aqmm_task, FALSE) THEN 'pending' ELSE 'not_required' END, + aqmm_review_status = CASE + WHEN COALESCE(fst.aqmm_manager_review_required, FALSE) + OR COALESCE(fst.aqmm_partner_review_required, FALSE) + OR COALESCE(fst.aqmm_review_partner_required, FALSE) + THEN 'pending_review' + ELSE 'not_required' + END + FROM firm_service_task_templates fst + WHERE csti.firm_task_template_id = fst.id + """) + + +def downgrade() -> None: + bind = op.get_bind() + for table in ["client_service_task_instances"]: + if _has_table(bind, table): + for col in [ + "aqmm_completed_at_utc", + "aqmm_review_status", + "aqmm_status", + "aqmm_reference", + "aqmm_blocks_final_release", + "aqmm_review_partner_required", + "aqmm_partner_review_required", + "aqmm_manager_review_required", + "aqmm_evidence_required", + "aqmm_mandatory", + "is_aqmm_task", + ]: + _drop_column_once(bind, table, col) + + for table in ["firm_service_task_templates", "service_default_task_templates"]: + if _has_table(bind, table): + for col in [ + "aqmm_reference", + "aqmm_blocks_final_release", + "aqmm_review_partner_required", + "aqmm_partner_review_required", + "aqmm_manager_review_required", + "aqmm_evidence_required", + "aqmm_mandatory", + "is_aqmm_task", + ]: + _drop_column_once(bind, table, col) diff --git a/app/modules/services/bulk_imports.py b/app/modules/services/bulk_imports.py index 6ebfe38..8d4faa8 100644 --- a/app/modules/services/bulk_imports.py +++ b/app/modules/services/bulk_imports.py @@ -73,6 +73,14 @@ DEFAULT_TASK_COLUMNS = [ "default_role_name", "is_mandatory", "requires_review", + "is_aqmm_task", + "aqmm_mandatory", + "aqmm_evidence_required", + "aqmm_manager_review_required", + "aqmm_partner_review_required", + "aqmm_review_partner_required", + "aqmm_blocks_final_release", + "aqmm_reference", "is_active", "description", ] @@ -84,6 +92,14 @@ FIRM_TASK_COLUMNS = [ "default_role_name", "is_mandatory", "requires_review", + "is_aqmm_task", + "aqmm_mandatory", + "aqmm_evidence_required", + "aqmm_manager_review_required", + "aqmm_partner_review_required", + "aqmm_review_partner_required", + "aqmm_blocks_final_release", + "aqmm_reference", "is_active", "description", ] @@ -1010,6 +1026,14 @@ def import_system_default_tasks(db: Session, *, current_user, file_bytes: bytes, task.default_role_name = _clean(_cell(row, headers, "default_role_name")) or None task.is_mandatory = _bool(_cell(row, headers, "is_mandatory"), True) task.requires_review = _bool(_cell(row, headers, "requires_review"), False) + task.is_aqmm_task = _bool(_cell(row, headers, "is_aqmm_task"), False) + task.aqmm_mandatory = _bool(_cell(row, headers, "aqmm_mandatory"), False) + task.aqmm_evidence_required = _bool(_cell(row, headers, "aqmm_evidence_required"), False) + task.aqmm_manager_review_required = _bool(_cell(row, headers, "aqmm_manager_review_required"), False) + task.aqmm_partner_review_required = _bool(_cell(row, headers, "aqmm_partner_review_required"), False) + task.aqmm_review_partner_required = _bool(_cell(row, headers, "aqmm_review_partner_required"), False) + task.aqmm_blocks_final_release = _bool(_cell(row, headers, "aqmm_blocks_final_release"), False) + task.aqmm_reference = _clean(_cell(row, headers, "aqmm_reference")) or None task.is_active = _bool(_cell(row, headers, "is_active"), True) except Exception as exc: errors.append({"row": row_no, "message": str(exc)}) @@ -1067,6 +1091,14 @@ def import_firm_task_templates(db: Session, *, current_user, tenant_id: int, fil task.default_role_name = _clean(_cell(row, headers, "default_role_name")) or None task.is_mandatory = _bool(_cell(row, headers, "is_mandatory"), True) task.requires_review = _bool(_cell(row, headers, "requires_review"), False) + task.is_aqmm_task = _bool(_cell(row, headers, "is_aqmm_task"), False) + task.aqmm_mandatory = _bool(_cell(row, headers, "aqmm_mandatory"), False) + task.aqmm_evidence_required = _bool(_cell(row, headers, "aqmm_evidence_required"), False) + task.aqmm_manager_review_required = _bool(_cell(row, headers, "aqmm_manager_review_required"), False) + task.aqmm_partner_review_required = _bool(_cell(row, headers, "aqmm_partner_review_required"), False) + task.aqmm_review_partner_required = _bool(_cell(row, headers, "aqmm_review_partner_required"), False) + task.aqmm_blocks_final_release = _bool(_cell(row, headers, "aqmm_blocks_final_release"), False) + task.aqmm_reference = _clean(_cell(row, headers, "aqmm_reference")) or None task.is_active = _bool(_cell(row, headers, "is_active"), True) task.updated_by_user_id = current_user.id except Exception as exc: diff --git a/app/modules/services/engagements_ui.py b/app/modules/services/engagements_ui.py index 0897c9d..a3841aa 100644 --- a/app/modules/services/engagements_ui.py +++ b/app/modules/services/engagements_ui.py @@ -10,6 +10,7 @@ from app.core.security.session_auth import get_current_user 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 aqmm_task_summary_for_subscription from app.modules.clients.models import Client from app.modules.services.client_services import ( SUBSCRIPTION_STATUSES, @@ -398,6 +399,7 @@ def subscription_detail(request: Request, subscription_id: int): my_pending_declarations = [d for d in declarations if d.requested_user_id == user.id and d.status == "pending"] kyc_verification = get_latest_engagement_kyc_verification(db, subscription_id=row.id) engagement_letter = get_current_engagement_letter(db, subscription_id=row.id) + aqmm_task_summary = aqmm_task_summary_for_subscription(db, subscription_id=row.id) tasks = db.execute( select(ClientServiceTaskInstance) @@ -417,6 +419,7 @@ def subscription_detail(request: Request, subscription_id: int): my_pending_declarations=my_pending_declarations, kyc_verification=kyc_verification, engagement_letter=engagement_letter, + aqmm_task_summary=aqmm_task_summary, can_manage=_can_manage_client_services(db, user), ) finally: diff --git a/app/modules/services/execution.py b/app/modules/services/execution.py index 1f42b9c..ff751cf 100644 --- a/app/modules/services/execution.py +++ b/app/modules/services/execution.py @@ -6,6 +6,7 @@ from sqlalchemy import func, or_, select from sqlalchemy.orm import Session, selectinload from app.modules.clients.models import Client +from app.modules.documents.models import EngagementDocument from app.modules.core.iam.models import User from app.modules.services.client_services import quality_required_for_engagement, QUALITY_APPROVED from app.modules.services.models import ( @@ -222,6 +223,16 @@ def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceS internal_target_date=_default_internal_target_date(subscription, template), status="pending", priority="normal", + 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), + aqmm_manager_review_required=getattr(template, "aqmm_manager_review_required", False), + aqmm_partner_review_required=getattr(template, "aqmm_partner_review_required", False), + aqmm_review_partner_required=getattr(template, "aqmm_review_partner_required", False), + aqmm_blocks_final_release=getattr(template, "aqmm_blocks_final_release", False), + aqmm_reference=getattr(template, "aqmm_reference", None), + aqmm_status="pending" if getattr(template, "is_aqmm_task", False) else "not_required", + aqmm_review_status="pending_review" if (getattr(template, "aqmm_manager_review_required", False) or getattr(template, "aqmm_partner_review_required", False) or getattr(template, "aqmm_review_partner_required", False)) else "not_required", is_active=True, created_by_user_id=user_id, updated_by_user_id=user_id, @@ -231,6 +242,89 @@ def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceS return created + +def _task_has_evidence(db: Session, task_id: int) -> bool: + return bool( + db.execute( + select(EngagementDocument.id).where( + EngagementDocument.task_instance_id == int(task_id), + EngagementDocument.is_deleted.is_(False), + ) + ).first() + ) + + +def _aqmm_task_issue(db: Session, task: ClientServiceTaskInstance) -> str | None: + """Return blocking reason for one AQMM-tagged task, or None if it passes. + + This deliberately reuses the existing task status and task document upload + workflow. Review flags are captured for reporting and future review-specific + workflow; until dedicated manager/partner review statuses are added, the + task must at least be completed and evidence uploaded wherever marked. + """ + if not getattr(task, "is_aqmm_task", False): + return None + if getattr(task, "aqmm_mandatory", False) and task.status != "completed": + return "Task not completed" + if getattr(task, "aqmm_evidence_required", False) and not _task_has_evidence(db, task.id): + return "Evidence not uploaded" + return None + + +def list_aqmm_quality_tasks(db: Session, *, subscription_id: int) -> list[dict]: + tasks = db.execute( + select(ClientServiceTaskInstance) + .where( + ClientServiceTaskInstance.subscription_id == int(subscription_id), + ClientServiceTaskInstance.is_active.is_(True), + ClientServiceTaskInstance.is_aqmm_task.is_(True), + ) + .order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc()) + ).scalars().all() + rows: list[dict] = [] + for task in tasks: + has_evidence = _task_has_evidence(db, task.id) + issue = _aqmm_task_issue(db, task) + rows.append({"task": task, "has_evidence": has_evidence, "issue": issue, "passes": issue is None}) + return rows + + +def aqmm_task_summary_for_subscription(db: Session, *, subscription_id: int) -> dict: + rows = list_aqmm_quality_tasks(db, subscription_id=subscription_id) + total = len(rows) + completed = sum(1 for r in rows if r["task"].status == "completed") + mandatory = sum(1 for r in rows if getattr(r["task"], "aqmm_mandatory", False)) + mandatory_done = sum(1 for r in rows if getattr(r["task"], "aqmm_mandatory", False) and r["passes"]) + evidence_required = sum(1 for r in rows if getattr(r["task"], "aqmm_evidence_required", False)) + evidence_missing = sum(1 for r in rows if getattr(r["task"], "aqmm_evidence_required", False) and not r["has_evidence"]) + blockers = [r for r in rows if getattr(r["task"], "aqmm_blocks_final_release", False) and not r["passes"]] + status = "not_required" + if total: + status = "completed" if mandatory_done == mandatory and evidence_missing == 0 else "in_progress" + return { + "rows": rows, + "total": total, + "completed": completed, + "mandatory": mandatory, + "mandatory_done": mandatory_done, + "evidence_required": evidence_required, + "evidence_missing": evidence_missing, + "blockers": blockers, + "status": status, + } + + +def assert_aqmm_quality_tasks_complete(db: Session, *, subscription_id: int) -> None: + summary = aqmm_task_summary_for_subscription(db, subscription_id=subscription_id) + if summary["blockers"]: + first = summary["blockers"][0] + task = first["task"] + raise ValueError(f"AQMM final release blocked: {task.task_name} - {first['issue']}") + if summary["mandatory"] and summary["mandatory_done"] < summary["mandatory"]: + raise ValueError("AQMM mandatory quality checklist tasks are pending.") + if summary["evidence_missing"]: + raise ValueError("AQMM evidence upload is pending for one or more quality checklist tasks.") + def _decorate_task_for_tracker(task: ClientServiceTaskInstance, *, today: date) -> ClientServiceTaskInstance: target_date = getattr(task, "internal_target_date", None) subscription = getattr(task, "subscription", None) diff --git a/app/modules/services/models.py b/app/modules/services/models.py index 2c5c888..547682e 100644 --- a/app/modules/services/models.py +++ b/app/modules/services/models.py @@ -111,6 +111,19 @@ class ServiceDefaultTaskTemplate(CommonBase): default_role_name: Mapped[str | None] = mapped_column(String(100), nullable=True) is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + + # AQMM task tags. These flags allow the existing service checklist to become + # the engagement quality checklist for assurance engagements, without creating + # a separate duplicate AQMM checklist module. + is_aqmm_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True) + aqmm_mandatory: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_evidence_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_manager_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_partner_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_review_partner_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_blocks_final_release: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_reference: Mapped[str | None] = mapped_column(String(120), nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) catalogue = relationship("ServiceCatalogue", back_populates="default_task_templates") @@ -166,6 +179,17 @@ class FirmServiceTaskTemplate(CommonBase): default_role_name: Mapped[str | None] = mapped_column(String(100), nullable=True) is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + + # AQMM task tags copied into generated ClientServiceTaskInstance rows. + is_aqmm_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True) + aqmm_mandatory: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_evidence_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_manager_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_partner_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_review_partner_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_blocks_final_release: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_reference: Mapped[str | None] = mapped_column(String(120), nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) @@ -572,6 +596,21 @@ class ClientServiceTaskInstance(CommonBase): internal_target_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True) status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending", index=True) priority: Mapped[str] = mapped_column(String(20), nullable=False, default="normal") + + # Copied from FirmServiceTaskTemplate at generation time. Existing task + # completion/evidence upload flow is reused to calculate AQMM quality status. + is_aqmm_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True) + aqmm_mandatory: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_evidence_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_manager_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_partner_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_review_partner_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_blocks_final_release: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + aqmm_reference: Mapped[str | None] = mapped_column(String(120), nullable=True) + aqmm_status: Mapped[str] = mapped_column(String(40), default="not_required", nullable=False, index=True) + aqmm_review_status: Mapped[str] = mapped_column(String(40), default="not_required", nullable=False, index=True) + aqmm_completed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + remarks: Mapped[str | None] = mapped_column(Text, nullable=True) started_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) diff --git a/app/modules/services/templates/services/default_task_form.html b/app/modules/services/templates/services/default_task_form.html index efc0f6c..ab8783c 100644 --- a/app/modules/services/templates/services/default_task_form.html +++ b/app/modules/services/templates/services/default_task_form.html @@ -47,6 +47,24 @@ +
+
AQMM / Quality Control Tagging
+

Use these only for tasks that should become part of the assurance engagement quality checklist. Non-assurance engagements will continue using the normal task flow.

+
+ + + + + + + +
+ + +
+
+
+
To deactivate this system default task, untick Active and save. Existing firm-copied tasks will not be automatically changed.
diff --git a/app/modules/services/templates/services/default_templates_detail.html b/app/modules/services/templates/services/default_templates_detail.html index 757796f..ce8e53f 100644 --- a/app/modules/services/templates/services/default_templates_detail.html +++ b/app/modules/services/templates/services/default_templates_detail.html @@ -19,7 +19,7 @@
Role: {{ task.default_role_name or '-' }} · Mandatory: {{ 'Yes' if task.is_mandatory else 'No' }} · - Review: {{ 'Yes' if task.requires_review else 'No' }} + Review: {{ 'Yes' if task.requires_review else 'No' }} · AQMM: {{ 'Yes' if task.is_aqmm_task else 'No' }}
@@ -46,6 +46,19 @@
+
+
AQMM / Quality Control Tagging
+
+ + + + + + + + +
+
diff --git a/app/modules/services/templates/services/engagements/detail.html b/app/modules/services/templates/services/engagements/detail.html index e493b05..a58b4a4 100644 --- a/app/modules/services/templates/services/engagements/detail.html +++ b/app/modules/services/templates/services/engagements/detail.html @@ -45,6 +45,41 @@
{% endif %} + +
+
+
+

AQMM Quality Checklist Tasks

+

These are your existing service tasks tagged as part of AQMM. No separate checklist is created.

+
+
+ Total: {{ aqmm_task_summary.total if aqmm_task_summary else 0 }} + Completed: {{ aqmm_task_summary.completed if aqmm_task_summary else 0 }} + Evidence Missing: {{ aqmm_task_summary.evidence_missing if aqmm_task_summary else 0 }} + Final Blockers: {{ aqmm_task_summary.blockers|length if aqmm_task_summary else 0 }} +
+
+
+ + + + {% for item in aqmm_task_summary.rows if aqmm_task_summary %} + {% set task = item.task %} + + + + + + + + {% else %} + + {% endfor %} + +
TaskAQMM FlagsEvidenceStatusAction
{{ task.sequence_no }}. {{ task.task_name }}
{% if task.aqmm_reference %}
{{ task.aqmm_reference }}
{% endif %}
{% if task.aqmm_mandatory %}Mandatory{% endif %}{% if task.aqmm_evidence_required %}Evidence{% endif %}{% if task.aqmm_manager_review_required %}Manager{% endif %}{% if task.aqmm_partner_review_required %}Partner{% endif %}{% if task.aqmm_review_partner_required %}Review Partner{% endif %}{% if task.aqmm_blocks_final_release %}Final Gate{% endif %}
{{ 'Uploaded' if item.has_evidence else ('Required' if task.aqmm_evidence_required else '-') }}{{ 'OK' if item.passes else item.issue }}
Task: {{ task.status|replace('_',' ')|title }}
Open Evidence
No AQMM-tagged task generated yet. Mark selected service tasks as AQMM and generate work tracker tasks.
+
+
+ {% if my_pending_declarations %}

My Pending Declarations

@@ -83,7 +118,7 @@ {% for task in tasks or [] %} {{ task.sequence_no }} -
{{ task.task_name }}
{% if task.description %}
{{ task.description }}
{% endif %} +
{{ task.task_name }}
{% if task.description %}
{{ task.description }}
{% endif %}{% if task.is_aqmm_task %}
AQMM{% if task.aqmm_mandatory %}Mandatory{% endif %}{% if task.aqmm_evidence_required %}Evidence{% endif %}
{% endif %} {{ task.status.replace('_',' ').title() }} Open Task Documents diff --git a/app/modules/services/templates/services/firm_task_form.html b/app/modules/services/templates/services/firm_task_form.html index ecbe1af..a2b522e 100644 --- a/app/modules/services/templates/services/firm_task_form.html +++ b/app/modules/services/templates/services/firm_task_form.html @@ -47,6 +47,24 @@
+
+
AQMM / Quality Control Tagging
+

Use these only for tasks that should become part of the assurance engagement quality checklist. Non-assurance engagements will continue using the normal task flow.

+
+ + + + + + + +
+ + +
+
+
+
To deactivate this task, untick Active and save. No hard delete is used.
diff --git a/app/modules/services/templates/services/task_template_detail.html b/app/modules/services/templates/services/task_template_detail.html index 3701016..cade5b2 100644 --- a/app/modules/services/templates/services/task_template_detail.html +++ b/app/modules/services/templates/services/task_template_detail.html @@ -74,6 +74,20 @@ +
+
AQMM / Quality Control Tagging
+

Tick only the service checklist tasks that should form part of assurance engagement quality evidence.

+
+ + + + + + + + +
+
@@ -115,6 +129,10 @@
{% if task.is_mandatory %}Mandatory{% endif %} {% if task.requires_review %}Review{% endif %} + {% if task.is_aqmm_task %}AQMM{% endif %} + {% if task.aqmm_mandatory %}AQMM Mandatory{% endif %} + {% if task.aqmm_evidence_required %}Evidence{% endif %} + {% if task.aqmm_blocks_final_release %}Final Gate{% endif %} {{ 'Active' if task.is_active else 'Inactive' }}
diff --git a/app/modules/services/ui.py b/app/modules/services/ui.py index a5b4d7d..9cf7c9a 100644 --- a/app/modules/services/ui.py +++ b/app/modules/services/ui.py @@ -814,7 +814,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(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), 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(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: 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: @@ -839,6 +839,14 @@ 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, + 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, created_by_user_id=user.id, updated_by_user_id=user.id, @@ -888,7 +896,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), 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), 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: @@ -901,7 +909,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, 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, 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: @@ -940,6 +948,14 @@ 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, + is_aqmm_task=getattr(d, "is_aqmm_task", False), + aqmm_mandatory=getattr(d, "aqmm_mandatory", False), + aqmm_evidence_required=getattr(d, "aqmm_evidence_required", False), + aqmm_manager_review_required=getattr(d, "aqmm_manager_review_required", False), + aqmm_partner_review_required=getattr(d, "aqmm_partner_review_required", False), + aqmm_review_partner_required=getattr(d, "aqmm_review_partner_required", False), + aqmm_blocks_final_release=getattr(d, "aqmm_blocks_final_release", False), + aqmm_reference=getattr(d, "aqmm_reference", None), is_active=d.is_active, created_by_user_id=user.id, updated_by_user_id=user.id, @@ -1136,6 +1152,14 @@ def firm_task_template_edit_submit( sequence_no: int = Form(1), is_mandatory: str | None = Form(None), requires_review: 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(...), ): @@ -1166,6 +1190,14 @@ 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.is_aqmm_task = is_aqmm_task is not None + task.aqmm_mandatory = aqmm_mandatory is not None + task.aqmm_evidence_required = aqmm_evidence_required is not None + task.aqmm_manager_review_required = aqmm_manager_review_required is not None + task.aqmm_partner_review_required = aqmm_partner_review_required is not None + task.aqmm_review_partner_required = aqmm_review_partner_required is not None + task.aqmm_blocks_final_release = aqmm_blocks_final_release is not None + task.aqmm_reference = aqmm_reference.strip() or None task.is_active = is_active is not None task.updated_by_user_id = user.id @@ -1227,6 +1259,14 @@ def default_task_template_edit_submit( sequence_no: int = Form(1), is_mandatory: str | None = Form(None), requires_review: 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(...), ): @@ -1259,6 +1299,14 @@ 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.is_aqmm_task = is_aqmm_task is not None + task.aqmm_mandatory = aqmm_mandatory is not None + task.aqmm_evidence_required = aqmm_evidence_required is not None + task.aqmm_manager_review_required = aqmm_manager_review_required is not None + task.aqmm_partner_review_required = aqmm_partner_review_required is not None + task.aqmm_review_partner_required = aqmm_review_partner_required is not None + task.aqmm_blocks_final_release = aqmm_blocks_final_release is not None + task.aqmm_reference = aqmm_reference.strip() or None task.is_active = is_active is not None db.commit()