Integrate audit checklist responses into service tasks

This commit is contained in:
A R R R Associates
2026-07-19 21:59:24 +05:30
parent 05b736b6c9
commit df52845194
10 changed files with 383 additions and 10 deletions
+14
View File
@@ -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)
+25 -4
View File
@@ -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,
+109 -2
View File
@@ -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:
+32 -1
View File
@@ -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)
@@ -47,6 +47,30 @@
<textarea name="description" rows="4" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">{{ task.description or '' }}</textarea>
</div>
<div class="md:col-span-2 rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div class="text-sm font-semibold text-emerald-950">Structured Checklist Response</div>
<p class="mt-1 text-xs text-emerald-800">Configure this task as an auditable checklist item. Normal tasks can keep Response Type as None.</p>
<div class="mt-3 grid gap-4 md:grid-cols-2">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-emerald-800">Task Category</label>
<input type="text" name="task_category" value="{{ task.task_category or '' }}" class="w-full rounded-xl border border-emerald-200 px-3 py-2 text-sm" placeholder="e.g. Accounting Verification">
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-emerald-800">Response Type</label>
<select name="response_type" class="w-full rounded-xl border border-emerald-200 px-3 py-2 text-sm">
{% for value, label in [('NONE','None'),('YES_NO_NA','Yes / No / NA'),('YES_NO','Yes / No'),('TEXT','Text'),('NUMBER','Number'),('DATE','Date')] %}
<option value="{{ value }}" {% if (task.response_type or 'NONE') == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="mt-4 grid gap-3 text-sm text-slate-700 md:grid-cols-3">
<label class="inline-flex items-center gap-2"><input type="checkbox" name="response_required" {% if task.response_required %}checked{% endif %}> Response required</label>
<label class="inline-flex items-center gap-2"><input type="checkbox" name="evidence_required" {% if task.evidence_required %}checked{% endif %}> Evidence required</label>
<label class="inline-flex items-center gap-2"><input type="checkbox" name="remarks_required_if_no" {% if task.remarks_required_if_no %}checked{% endif %}> Remarks required if No</label>
</div>
</div>
<div class="md:col-span-2 rounded-2xl border border-indigo-200 bg-indigo-50 p-4">
<div class="text-sm font-semibold text-indigo-950">AQMM / Quality Control Tagging</div>
<p class="mt-1 text-xs text-indigo-800">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.</p>
@@ -47,6 +47,30 @@
<textarea name="description" rows="4" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">{{ task.description or '' }}</textarea>
</div>
<div class="md:col-span-2 rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div class="text-sm font-semibold text-emerald-950">Structured Checklist Response</div>
<p class="mt-1 text-xs text-emerald-800">Configure this task as an auditable checklist item. Normal tasks can keep Response Type as None.</p>
<div class="mt-3 grid gap-4 md:grid-cols-2">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-emerald-800">Task Category</label>
<input type="text" name="task_category" value="{{ task.task_category or '' }}" class="w-full rounded-xl border border-emerald-200 px-3 py-2 text-sm" placeholder="e.g. Accounting Verification">
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-emerald-800">Response Type</label>
<select name="response_type" class="w-full rounded-xl border border-emerald-200 px-3 py-2 text-sm">
{% for value, label in [('NONE','None'),('YES_NO_NA','Yes / No / NA'),('YES_NO','Yes / No'),('TEXT','Text'),('NUMBER','Number'),('DATE','Date')] %}
<option value="{{ value }}" {% if (task.response_type or 'NONE') == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="mt-4 grid gap-3 text-sm text-slate-700 md:grid-cols-3">
<label class="inline-flex items-center gap-2"><input type="checkbox" name="response_required" {% if task.response_required %}checked{% endif %}> Response required</label>
<label class="inline-flex items-center gap-2"><input type="checkbox" name="evidence_required" {% if task.evidence_required %}checked{% endif %}> Evidence required</label>
<label class="inline-flex items-center gap-2"><input type="checkbox" name="remarks_required_if_no" {% if task.remarks_required_if_no %}checked{% endif %}> Remarks required if No</label>
</div>
</div>
<div class="md:col-span-2 rounded-2xl border border-indigo-200 bg-indigo-50 p-4">
<div class="text-sm font-semibold text-indigo-950">AQMM / Quality Control Tagging</div>
<p class="mt-1 text-xs text-indigo-800">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.</p>
@@ -54,6 +54,10 @@
</div>
{% endif %}
{% if request.query_params.get('checklist_error') %}
<div class="mt-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">The task could not be completed. Enter the required checklist response, remarks and evidence.</div>
{% endif %}
<form method="post" class="mt-6 grid gap-5 md:grid-cols-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div>
@@ -82,6 +86,55 @@
{% if not can_reassign and task.assigned_to %}<p class="mt-1 text-xs text-slate-500">Assignee changes are restricted for this role.</p>{% endif %}
{% if can_edit and not can_manage_fields %}<p class="mt-1 text-xs text-slate-500">You can update status and work note only for your own assigned task.</p>{% endif %}
</div>
{% if (task.response_type or 'NONE') != 'NONE' or task.response_required or task.evidence_required %}
<div class="md:col-span-2 rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="text-sm font-semibold text-emerald-950">Checklist Response</div>
<p class="mt-1 text-xs text-emerald-800">{{ task.task_category or 'Verification' }}{% if task.response_required %} · Response required{% endif %}{% if task.evidence_required %} · Evidence required{% endif %}</p>
</div>
</div>
<div class="mt-4 grid gap-4 md:grid-cols-2">
{% if task.response_type == 'YES_NO_NA' %}
<div>
<label class="mb-2 block text-sm font-medium text-slate-700">Response</label>
<select name="checklist_response" class="w-full rounded-xl border border-emerald-200 px-4 py-2 text-sm" {% if not can_edit %}disabled{% endif %}>
<option value="">Select</option>
{% for value in ['YES','NO','NA'] %}<option value="{{ value }}" {% if task.checklist_response == value %}selected{% endif %}>{{ value.replace('_',' ').title() }}</option>{% endfor %}
</select>
</div>
{% elif task.response_type == 'YES_NO' %}
<div>
<label class="mb-2 block text-sm font-medium text-slate-700">Response</label>
<select name="checklist_response" class="w-full rounded-xl border border-emerald-200 px-4 py-2 text-sm" {% if not can_edit %}disabled{% endif %}>
<option value="">Select</option>
{% for value in ['YES','NO'] %}<option value="{{ value }}" {% if task.checklist_response == value %}selected{% endif %}>{{ value.title() }}</option>{% endfor %}
</select>
</div>
{% elif task.response_type == 'TEXT' %}
<div class="md:col-span-2">
<label class="mb-2 block text-sm font-medium text-slate-700">Response</label>
<textarea name="checklist_text_response" rows="3" class="w-full rounded-xl border border-emerald-200 px-4 py-2 text-sm" {% if not can_edit %}readonly{% endif %}>{{ task.checklist_text_response or '' }}</textarea>
</div>
{% elif task.response_type == 'NUMBER' %}
<div>
<label class="mb-2 block text-sm font-medium text-slate-700">Numeric Response</label>
<input type="number" step="0.01" name="checklist_number_response" value="{{ task.checklist_number_response if task.checklist_number_response is not none else '' }}" class="w-full rounded-xl border border-emerald-200 px-4 py-2 text-sm" {% if not can_edit %}readonly{% endif %}>
</div>
{% elif task.response_type == 'DATE' %}
<div>
<label class="mb-2 block text-sm font-medium text-slate-700">Date Response</label>
<input type="date" name="checklist_date_response" value="{{ task.checklist_date_response or '' }}" class="w-full rounded-xl border border-emerald-200 px-4 py-2 text-sm" {% if not can_edit %}readonly{% endif %}>
</div>
{% endif %}
<div class="{% if task.response_type == 'TEXT' %}md:col-span-2{% endif %}">
<label class="mb-2 block text-sm font-medium text-slate-700">Checklist Remarks{% if task.remarks_required_if_no %} (required when No){% endif %}</label>
<textarea name="checklist_remarks" rows="3" class="w-full rounded-xl border border-emerald-200 px-4 py-2 text-sm" {% if not can_edit %}readonly{% endif %}>{{ task.checklist_remarks or '' }}</textarea>
</div>
</div>
</div>
{% endif %}
<div class="md:col-span-2">
<label class="mb-2 block text-sm font-medium text-slate-700">Remarks / Work Note</label>
<textarea name="remarks" rows="4" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm" {% if not can_edit %}readonly{% endif %}>{{ task.remarks or '' }}</textarea>
+33 -3
View File
@@ -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
+22
View File
@@ -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,