Integrate audit checklist responses into service tasks
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user