From df52845194aca81e2b8e2e23a2d83f58ea2d8fb0 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Sun, 19 Jul 2026 21:59:24 +0530 Subject: [PATCH] Integrate audit checklist responses into service tasks --- .../20260719_task_checklist_fields.py | 47 ++++++++ app/modules/services/bulk_imports.py | 14 +++ app/modules/services/default_tasks_export.py | 29 ++++- app/modules/services/execution.py | 111 +++++++++++++++++- app/modules/services/models.py | 33 +++++- .../templates/services/default_task_form.html | 24 ++++ .../templates/services/firm_task_form.html | 24 ++++ .../services/work_tracker/task_form.html | 53 +++++++++ app/modules/services/ui.py | 36 +++++- app/modules/services/work_tracker_ui.py | 22 ++++ 10 files changed, 383 insertions(+), 10 deletions(-) create mode 100644 alembic/versions/20260719_task_checklist_fields.py diff --git a/alembic/versions/20260719_task_checklist_fields.py b/alembic/versions/20260719_task_checklist_fields.py new file mode 100644 index 0000000..5154d49 --- /dev/null +++ b/alembic/versions/20260719_task_checklist_fields.py @@ -0,0 +1,47 @@ +"""add structured checklist fields to service tasks + +Revision ID: 20260719_task_checklist_fields +Revises: 20260713_bank_stmt_queue +""" +from alembic import op +import sqlalchemy as sa + +revision = "20260719_task_checklist_fields" +down_revision = "20260713_bank_stmt_queue" +branch_labels = None +depends_on = None + +_TEMPLATE_TABLES = ("service_default_task_templates", "firm_service_task_templates") + +def upgrade(): + for table in _TEMPLATE_TABLES: + op.add_column(table, sa.Column("task_category", sa.String(length=100), nullable=True)) + op.add_column(table, sa.Column("response_required", sa.Boolean(), nullable=False, server_default=sa.false())) + op.add_column(table, sa.Column("response_type", sa.String(length=20), nullable=False, server_default="NONE")) + op.add_column(table, sa.Column("evidence_required", sa.Boolean(), nullable=False, server_default=sa.false())) + op.add_column(table, sa.Column("remarks_required_if_no", sa.Boolean(), nullable=False, server_default=sa.false())) + op.create_index(f"ix_{table}_task_category", table, ["task_category"], unique=False) + + table = "client_service_task_instances" + op.add_column(table, sa.Column("task_category", sa.String(length=100), nullable=True)) + op.add_column(table, sa.Column("response_required", sa.Boolean(), nullable=False, server_default=sa.false())) + op.add_column(table, sa.Column("response_type", sa.String(length=20), nullable=False, server_default="NONE")) + op.add_column(table, sa.Column("evidence_required", sa.Boolean(), nullable=False, server_default=sa.false())) + op.add_column(table, sa.Column("remarks_required_if_no", sa.Boolean(), nullable=False, server_default=sa.false())) + op.add_column(table, sa.Column("checklist_response", sa.String(length=10), nullable=True)) + op.add_column(table, sa.Column("checklist_text_response", sa.Text(), nullable=True)) + op.add_column(table, sa.Column("checklist_number_response", sa.Numeric(18, 2), nullable=True)) + op.add_column(table, sa.Column("checklist_date_response", sa.Date(), nullable=True)) + op.add_column(table, sa.Column("checklist_remarks", sa.Text(), nullable=True)) + op.create_index("ix_client_service_task_instances_task_category", table, ["task_category"], unique=False) + +def downgrade(): + table = "client_service_task_instances" + op.drop_index("ix_client_service_task_instances_task_category", table_name=table) + for column in ("checklist_remarks", "checklist_date_response", "checklist_number_response", "checklist_text_response", "checklist_response", "remarks_required_if_no", "evidence_required", "response_type", "response_required", "task_category"): + op.drop_column(table, column) + + for table in reversed(_TEMPLATE_TABLES): + op.drop_index(f"ix_{table}_task_category", table_name=table) + for column in ("remarks_required_if_no", "evidence_required", "response_type", "response_required", "task_category"): + op.drop_column(table, column) diff --git a/app/modules/services/bulk_imports.py b/app/modules/services/bulk_imports.py index 8d4faa8..b4e84f4 100644 --- a/app/modules/services/bulk_imports.py +++ b/app/modules/services/bulk_imports.py @@ -1026,6 +1026,13 @@ 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.task_category = _clean(_cell(row, headers, "task_category")) or None + task.response_required = _bool(_cell(row, headers, "response_required"), False) + task.response_type = (_clean(_cell(row, headers, "response_type")) or "NONE").upper() + if task.response_type not in {"NONE", "YES_NO_NA", "YES_NO", "TEXT", "NUMBER", "DATE"}: + raise ValueError("response_type must be NONE, YES_NO_NA, YES_NO, TEXT, NUMBER or DATE.") + task.evidence_required = _bool(_cell(row, headers, "evidence_required"), False) + task.remarks_required_if_no = _bool(_cell(row, headers, "remarks_required_if_no"), 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) @@ -1091,6 +1098,13 @@ 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.task_category = _clean(_cell(row, headers, "task_category")) or None + task.response_required = _bool(_cell(row, headers, "response_required"), False) + task.response_type = (_clean(_cell(row, headers, "response_type")) or "NONE").upper() + if task.response_type not in {"NONE", "YES_NO_NA", "YES_NO", "TEXT", "NUMBER", "DATE"}: + raise ValueError("response_type must be NONE, YES_NO_NA, YES_NO, TEXT, NUMBER or DATE.") + task.evidence_required = _bool(_cell(row, headers, "evidence_required"), False) + task.remarks_required_if_no = _bool(_cell(row, headers, "remarks_required_if_no"), 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) diff --git a/app/modules/services/default_tasks_export.py b/app/modules/services/default_tasks_export.py index 0fa3b59..dff97c3 100644 --- a/app/modules/services/default_tasks_export.py +++ b/app/modules/services/default_tasks_export.py @@ -21,6 +21,11 @@ _HEADERS = [ "default_role_name", "is_mandatory", "requires_review", + "task_category", + "response_required", + "response_type", + "evidence_required", + "remarks_required_if_no", "is_aqmm_task", "aqmm_mandatory", "aqmm_evidence_required", @@ -40,6 +45,11 @@ _IMPORT_HEADERS = [ "default_role_name", "is_mandatory", "requires_review", + "task_category", + "response_required", + "response_type", + "evidence_required", + "remarks_required_if_no", "is_aqmm_task", "aqmm_mandatory", "aqmm_evidence_required", @@ -154,6 +164,11 @@ def build_default_tasks_export(db: Session) -> bytes: task.default_role_name or "", _yes_no(task.is_mandatory), _yes_no(task.requires_review), + task.task_category or "", + _yes_no(task.response_required), + task.response_type or "NONE", + _yes_no(task.evidence_required), + _yes_no(task.remarks_required_if_no), _yes_no(task.is_aqmm_task), _yes_no(task.aqmm_mandatory), _yes_no(task.aqmm_evidence_required), @@ -172,6 +187,11 @@ def build_default_tasks_export(db: Session) -> bytes: task.default_role_name or "", _yes_no(task.is_mandatory), _yes_no(task.requires_review), + task.task_category or "", + _yes_no(task.response_required), + task.response_type or "NONE", + _yes_no(task.evidence_required), + _yes_no(task.remarks_required_if_no), _yes_no(task.is_aqmm_task), _yes_no(task.aqmm_mandatory), _yes_no(task.aqmm_evidence_required), @@ -225,13 +245,14 @@ def build_default_tasks_export(db: Session) -> bytes: _style_sheet( all_tasks_ws, - {1: 32, 2: 42, 3: 18, 4: 26, 5: 12, 6: 44, 7: 20, 8: 14, 9: 14, 10: 14, - 11: 16, 12: 20, 13: 22, 14: 22, 15: 23, 16: 22, 17: 30, 18: 12, 19: 72}, + {1: 32, 2: 42, 3: 18, 4: 26, 5: 12, 6: 44, 7: 20, 8: 14, 9: 14, 10: 24, + 11: 18, 12: 18, 13: 18, 14: 20, 15: 14, 16: 16, 17: 20, 18: 22, 19: 22, 20: 23, + 21: 22, 22: 30, 23: 12, 24: 72}, ) _style_sheet( import_ws, - {1: 32, 2: 12, 3: 44, 4: 20, 5: 14, 6: 14, 7: 14, 8: 16, 9: 20, 10: 22, - 11: 22, 12: 23, 13: 22, 14: 30, 15: 12, 16: 72}, + {1: 32, 2: 12, 3: 44, 4: 20, 5: 14, 6: 14, 7: 24, 8: 18, 9: 18, 10: 18, + 11: 20, 12: 14, 13: 16, 14: 20, 15: 22, 16: 22, 17: 23, 18: 22, 19: 30, 20: 12, 21: 72}, ) _style_sheet( summary_ws, diff --git a/app/modules/services/execution.py b/app/modules/services/execution.py index 3714cfa..5172243 100644 --- a/app/modules/services/execution.py +++ b/app/modules/services/execution.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import date, datetime, timezone +from decimal import Decimal, InvalidOperation from sqlalchemy import func, or_, select from sqlalchemy.orm import Session, selectinload @@ -243,6 +244,11 @@ def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceS internal_target_date=_default_internal_target_date(subscription, template), status="pending", priority="normal", + task_category=getattr(template, "task_category", None), + response_required=getattr(template, "response_required", False), + 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), 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), @@ -834,6 +840,84 @@ def list_assignees_for_execution(db: Session, *, tenant_id: int, branch_id: int return db.execute(query.order_by(User.full_name.asc(), User.email.asc())).scalars().all() + +_CHECKLIST_RESPONSE_TYPES = {"NONE", "YES_NO_NA", "YES_NO", "TEXT", "NUMBER", "DATE"} +_CHECKLIST_YES_NO_NA_VALUES = {"YES", "NO", "NA"} +_CHECKLIST_YES_NO_VALUES = {"YES", "NO"} + + +def _normalise_checklist_response_type(value: str | None) -> str: + response_type = (value or "NONE").strip().upper() + return response_type if response_type in _CHECKLIST_RESPONSE_TYPES else "NONE" + + +def apply_task_checklist_response( + db: Session, + task: ClientServiceTaskInstance, + *, + checklist_response: str, + checklist_text_response: str, + checklist_number_response: str, + checklist_date_response: str, + checklist_remarks: str, + requested_status: str, +) -> None: + """Validate and persist the structured response for an execution task. + + Validation is enforced when a task is marked completed. Draft/in-progress + tasks may save partial responses so existing work-tracker behaviour remains + unchanged. + """ + response_type = _normalise_checklist_response_type(getattr(task, "response_type", "NONE")) + response_value = (checklist_response or "").strip().upper() + text_value = (checklist_text_response or "").strip() + number_text = (checklist_number_response or "").strip().replace(",", "") + date_text = (checklist_date_response or "").strip() + remarks_value = (checklist_remarks or "").strip() + + if response_type == "YES_NO_NA" and response_value and response_value not in _CHECKLIST_YES_NO_NA_VALUES: + raise ValueError("Checklist response must be Yes, No or NA.") + if response_type == "YES_NO" and response_value and response_value not in _CHECKLIST_YES_NO_VALUES: + raise ValueError("Checklist response must be Yes or No.") + + number_value = None + if number_text: + try: + number_value = Decimal(number_text) + except (InvalidOperation, ValueError): + raise ValueError("Checklist number response is invalid.") + + date_value = None + if date_text: + try: + date_value = date.fromisoformat(date_text) + except ValueError: + raise ValueError("Checklist date response is invalid.") + + completing = _normalise_status(requested_status) == "completed" + if completing and getattr(task, "response_required", False): + missing = ( + (response_type in {"YES_NO_NA", "YES_NO"} and not response_value) + or (response_type == "TEXT" and not text_value) + or (response_type == "NUMBER" and number_value is None) + or (response_type == "DATE" and date_value is None) + or (response_type == "NONE") + ) + if missing: + raise ValueError("Complete the required checklist response before marking the task completed.") + + if completing and getattr(task, "remarks_required_if_no", False) and response_value == "NO" and not remarks_value: + raise ValueError("Checklist remarks are required when the response is No.") + + if completing and getattr(task, "evidence_required", False) and not _task_has_evidence(db, task.id): + raise ValueError("Upload task evidence before marking the task completed.") + + task.checklist_response = response_value or None + task.checklist_text_response = text_value or None + task.checklist_number_response = number_value + task.checklist_date_response = date_value + task.checklist_remarks = remarks_value or None + def apply_task_update( task: ClientServiceTaskInstance, *, @@ -866,6 +950,7 @@ def apply_task_update( def apply_bulk_task_update( + db: Session, tasks: list[ClientServiceTaskInstance], *, status: str | None, @@ -882,8 +967,30 @@ def apply_bulk_task_update( skipped += 1 continue previous_status = task.status - if status: - task.status = _normalise_status(status) + requested_status = _normalise_status(status) if status else None + if requested_status == "completed": + response_type = _normalise_checklist_response_type(getattr(task, "response_type", "NONE")) + missing_response = ( + getattr(task, "response_required", False) + and ( + (response_type in {"YES_NO_NA", "YES_NO"} and not getattr(task, "checklist_response", None)) + or (response_type == "TEXT" and not getattr(task, "checklist_text_response", None)) + or (response_type == "NUMBER" and getattr(task, "checklist_number_response", None) is None) + or (response_type == "DATE" and getattr(task, "checklist_date_response", None) is None) + or response_type == "NONE" + ) + ) + missing_remarks = ( + getattr(task, "remarks_required_if_no", False) + and getattr(task, "checklist_response", None) == "NO" + and not getattr(task, "checklist_remarks", None) + ) + missing_evidence = getattr(task, "evidence_required", False) and not _task_has_evidence(db, task.id) + if missing_response or missing_remarks or missing_evidence: + skipped += 1 + continue + if requested_status: + task.status = requested_status if update_assignee: task.assigned_to_user_id = assigned_to_user_id if update_internal_target_date: diff --git a/app/modules/services/models.py b/app/modules/services/models.py index eeb542f..8d64400 100644 --- a/app/modules/services/models.py +++ b/app/modules/services/models.py @@ -1,8 +1,9 @@ from __future__ import annotations from datetime import date, datetime, timezone +from decimal import Decimal -from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint +from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, Numeric, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.db.common import CommonBase @@ -112,6 +113,15 @@ class ServiceDefaultTaskTemplate(CommonBase): is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + # Structured checklist controls. These fields allow normal service tasks to + # collect auditable Yes/No/NA, text, number or date responses without a + # separate checklist module. + task_category: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True) + response_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + response_type: Mapped[str] = mapped_column(String(20), default="NONE", nullable=False) + evidence_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + remarks_required_if_no: 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. @@ -180,6 +190,13 @@ class FirmServiceTaskTemplate(CommonBase): is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + # Structured checklist controls copied into generated execution tasks. + task_category: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True) + response_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + response_type: Mapped[str] = mapped_column(String(20), default="NONE", nullable=False) + evidence_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + remarks_required_if_no: 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) @@ -652,6 +669,20 @@ class ClientServiceTaskInstance(CommonBase): status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending", index=True) priority: Mapped[str] = mapped_column(String(20), nullable=False, default="normal") + # Structured checklist definition copied from the firm task template. + task_category: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True) + response_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + response_type: Mapped[str] = mapped_column(String(20), default="NONE", nullable=False) + evidence_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + remarks_required_if_no: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + + # Structured response values entered during task execution. + checklist_response: Mapped[str | None] = mapped_column(String(10), nullable=True) + checklist_text_response: Mapped[str | None] = mapped_column(Text, nullable=True) + checklist_number_response: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), nullable=True) + checklist_date_response: Mapped[date | None] = mapped_column(Date, nullable=True) + checklist_remarks: Mapped[str | None] = mapped_column(Text, nullable=True) + # 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) diff --git a/app/modules/services/templates/services/default_task_form.html b/app/modules/services/templates/services/default_task_form.html index ab8783c..53bf130 100644 --- a/app/modules/services/templates/services/default_task_form.html +++ b/app/modules/services/templates/services/default_task_form.html @@ -47,6 +47,30 @@ +
+
Structured Checklist Response
+

Configure this task as an auditable checklist item. Normal tasks can keep Response Type as None.

+
+
+ + +
+
+ + +
+
+
+ + + +
+
+
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.

diff --git a/app/modules/services/templates/services/firm_task_form.html b/app/modules/services/templates/services/firm_task_form.html index a2b522e..ba0a18c 100644 --- a/app/modules/services/templates/services/firm_task_form.html +++ b/app/modules/services/templates/services/firm_task_form.html @@ -47,6 +47,30 @@
+
+
Structured Checklist Response
+

Configure this task as an auditable checklist item. Normal tasks can keep Response Type as None.

+
+
+ + +
+
+ + +
+
+
+ + + +
+
+
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.

diff --git a/app/modules/services/templates/services/work_tracker/task_form.html b/app/modules/services/templates/services/work_tracker/task_form.html index 4af9809..b45d5ec 100644 --- a/app/modules/services/templates/services/work_tracker/task_form.html +++ b/app/modules/services/templates/services/work_tracker/task_form.html @@ -54,6 +54,10 @@
{% endif %} + {% if request.query_params.get('checklist_error') %} +
The task could not be completed. Enter the required checklist response, remarks and evidence.
+ {% endif %} +
@@ -82,6 +86,55 @@ {% if not can_reassign and task.assigned_to %}

Assignee changes are restricted for this role.

{% endif %} {% if can_edit and not can_manage_fields %}

You can update status and work note only for your own assigned task.

{% endif %}
+ {% if (task.response_type or 'NONE') != 'NONE' or task.response_required or task.evidence_required %} +
+
+
+
Checklist Response
+

{{ task.task_category or 'Verification' }}{% if task.response_required %} · Response required{% endif %}{% if task.evidence_required %} · Evidence required{% endif %}

+
+
+
+ {% if task.response_type == 'YES_NO_NA' %} +
+ + +
+ {% elif task.response_type == 'YES_NO' %} +
+ + +
+ {% elif task.response_type == 'TEXT' %} +
+ + +
+ {% elif task.response_type == 'NUMBER' %} +
+ + +
+ {% elif task.response_type == 'DATE' %} +
+ + +
+ {% endif %} +
+ + +
+
+
+ {% endif %} +
diff --git a/app/modules/services/ui.py b/app/modules/services/ui.py index ab402ca..311330a 100644 --- a/app/modules/services/ui.py +++ b/app/modules/services/ui.py @@ -948,7 +948,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_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(''), 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(...)): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: @@ -973,6 +973,11 @@ 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, + 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, @@ -1052,7 +1057,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_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), 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: @@ -1065,7 +1070,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_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, 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: @@ -1104,6 +1109,11 @@ 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, + task_category=getattr(d, "task_category", None), + response_required=getattr(d, "response_required", False), + response_type=getattr(d, "response_type", "NONE") or "NONE", + evidence_required=getattr(d, "evidence_required", False), + remarks_required_if_no=getattr(d, "remarks_required_if_no", False), 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), @@ -1308,6 +1318,11 @@ def firm_task_template_edit_submit( sequence_no: int = Form(1), 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), @@ -1346,6 +1361,11 @@ 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.task_category = task_category.strip() or None + task.response_required = response_required is not None + task.response_type = response_type.strip().upper() or "NONE" + task.evidence_required = evidence_required is not None + task.remarks_required_if_no = remarks_required_if_no 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 @@ -1415,6 +1435,11 @@ def default_task_template_edit_submit( sequence_no: int = Form(1), 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), @@ -1455,6 +1480,11 @@ 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.task_category = task_category.strip() or None + task.response_required = response_required is not None + task.response_type = response_type.strip().upper() or "NONE" + task.evidence_required = evidence_required is not None + task.remarks_required_if_no = remarks_required_if_no 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 diff --git a/app/modules/services/work_tracker_ui.py b/app/modules/services/work_tracker_ui.py index e0d4f29..4c652a7 100644 --- a/app/modules/services/work_tracker_ui.py +++ b/app/modules/services/work_tracker_ui.py @@ -19,6 +19,7 @@ from app.modules.services.execution import ( add_task_comment, apply_bulk_task_update, apply_task_review, + apply_task_checklist_response, apply_task_update, dashboard_stats, generate_tasks_for_subscription, @@ -322,6 +323,7 @@ def task_bulk_update_submit( return _redirect_denied() target_date = parse_date_value(bulk_internal_target_date) if update_internal_target_date is not None else None apply_bulk_task_update( + db, tasks, status=bulk_status.strip() or None, assigned_to_user_id=assigned_to_user_id, @@ -455,6 +457,11 @@ def task_edit_submit( assigned_to_user_id: str = Form(""), internal_target_date: str = Form(""), remarks: str = Form(""), + checklist_response: str = Form(""), + checklist_text_response: str = Form(""), + checklist_number_response: str = Form(""), + checklist_date_response: str = Form(""), + checklist_remarks: str = Form(""), is_active: str | None = Form(None), csrf_token: str = Form(...), ): @@ -504,6 +511,21 @@ def task_edit_submit( resolved_internal_target_date = task.internal_target_date resolved_is_active = task.is_active + try: + apply_task_checklist_response( + db, + task, + checklist_response=checklist_response, + checklist_text_response=checklist_text_response, + checklist_number_response=checklist_number_response, + checklist_date_response=checklist_date_response, + checklist_remarks=checklist_remarks, + requested_status=status, + ) + except ValueError: + db.rollback() + return RedirectResponse(url=f"/services/work-tracker/tasks/{task_id}/edit?checklist_error=1", status_code=303) + apply_task_update( task, status=status,