Add AQMM quality tags to existing service task checklist
This commit is contained in:
@@ -0,0 +1,125 @@
|
|||||||
|
"""phase 3.1 aqmm quality tags on existing service tasks
|
||||||
|
|
||||||
|
Revision ID: 20260625_phase_3_1_aqmm_task_quality_tags
|
||||||
|
Revises: 20260624_phase_3_aqmm_engagement_level_rework
|
||||||
|
Create Date: 2026-06-25
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "20260625_phase_3_1_aqmm_task_quality_tags"
|
||||||
|
down_revision = "20260624_phase_3_aqmm_engagement_level_rework"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
AQMM_TEMPLATE_COLUMNS = [
|
||||||
|
sa.Column("is_aqmm_task", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column("aqmm_mandatory", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column("aqmm_evidence_required", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column("aqmm_manager_review_required", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column("aqmm_partner_review_required", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column("aqmm_review_partner_required", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column("aqmm_blocks_final_release", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column("aqmm_reference", sa.String(length=120), nullable=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
AQMM_INSTANCE_COLUMNS = AQMM_TEMPLATE_COLUMNS + [
|
||||||
|
sa.Column("aqmm_status", sa.String(length=40), nullable=False, server_default="not_required"),
|
||||||
|
sa.Column("aqmm_review_status", sa.String(length=40), nullable=False, server_default="not_required"),
|
||||||
|
sa.Column("aqmm_completed_at_utc", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _has_table(bind, table_name: str) -> bool:
|
||||||
|
return sa.inspect(bind).has_table(table_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_column(bind, table_name: str, column_name: str) -> bool:
|
||||||
|
if not _has_table(bind, table_name):
|
||||||
|
return False
|
||||||
|
return column_name in {c["name"] for c in sa.inspect(bind).get_columns(table_name)}
|
||||||
|
|
||||||
|
|
||||||
|
def _add_column_once(bind, table: str, column: sa.Column) -> None:
|
||||||
|
if not _has_column(bind, table, column.name):
|
||||||
|
with op.batch_alter_table(table) as batch:
|
||||||
|
batch.add_column(column)
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_column_once(bind, table: str, column_name: str) -> None:
|
||||||
|
if _has_column(bind, table, column_name):
|
||||||
|
with op.batch_alter_table(table) as batch:
|
||||||
|
batch.drop_column(column_name)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
|
||||||
|
for table in ["service_default_task_templates", "firm_service_task_templates"]:
|
||||||
|
if _has_table(bind, table):
|
||||||
|
for column in AQMM_TEMPLATE_COLUMNS:
|
||||||
|
_add_column_once(bind, table, column.copy())
|
||||||
|
|
||||||
|
if _has_table(bind, "client_service_task_instances"):
|
||||||
|
for column in AQMM_INSTANCE_COLUMNS:
|
||||||
|
_add_column_once(bind, "client_service_task_instances", column.copy())
|
||||||
|
|
||||||
|
# Backfill existing generated tasks from their firm task templates.
|
||||||
|
op.execute("""
|
||||||
|
UPDATE client_service_task_instances csti
|
||||||
|
SET is_aqmm_task = COALESCE(fst.is_aqmm_task, FALSE),
|
||||||
|
aqmm_mandatory = COALESCE(fst.aqmm_mandatory, FALSE),
|
||||||
|
aqmm_evidence_required = COALESCE(fst.aqmm_evidence_required, FALSE),
|
||||||
|
aqmm_manager_review_required = COALESCE(fst.aqmm_manager_review_required, FALSE),
|
||||||
|
aqmm_partner_review_required = COALESCE(fst.aqmm_partner_review_required, FALSE),
|
||||||
|
aqmm_review_partner_required = COALESCE(fst.aqmm_review_partner_required, FALSE),
|
||||||
|
aqmm_blocks_final_release = COALESCE(fst.aqmm_blocks_final_release, FALSE),
|
||||||
|
aqmm_reference = fst.aqmm_reference,
|
||||||
|
aqmm_status = CASE WHEN COALESCE(fst.is_aqmm_task, FALSE) THEN 'pending' ELSE 'not_required' END,
|
||||||
|
aqmm_review_status = CASE
|
||||||
|
WHEN COALESCE(fst.aqmm_manager_review_required, FALSE)
|
||||||
|
OR COALESCE(fst.aqmm_partner_review_required, FALSE)
|
||||||
|
OR COALESCE(fst.aqmm_review_partner_required, FALSE)
|
||||||
|
THEN 'pending_review'
|
||||||
|
ELSE 'not_required'
|
||||||
|
END
|
||||||
|
FROM firm_service_task_templates fst
|
||||||
|
WHERE csti.firm_task_template_id = fst.id
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
for table in ["client_service_task_instances"]:
|
||||||
|
if _has_table(bind, table):
|
||||||
|
for col in [
|
||||||
|
"aqmm_completed_at_utc",
|
||||||
|
"aqmm_review_status",
|
||||||
|
"aqmm_status",
|
||||||
|
"aqmm_reference",
|
||||||
|
"aqmm_blocks_final_release",
|
||||||
|
"aqmm_review_partner_required",
|
||||||
|
"aqmm_partner_review_required",
|
||||||
|
"aqmm_manager_review_required",
|
||||||
|
"aqmm_evidence_required",
|
||||||
|
"aqmm_mandatory",
|
||||||
|
"is_aqmm_task",
|
||||||
|
]:
|
||||||
|
_drop_column_once(bind, table, col)
|
||||||
|
|
||||||
|
for table in ["firm_service_task_templates", "service_default_task_templates"]:
|
||||||
|
if _has_table(bind, table):
|
||||||
|
for col in [
|
||||||
|
"aqmm_reference",
|
||||||
|
"aqmm_blocks_final_release",
|
||||||
|
"aqmm_review_partner_required",
|
||||||
|
"aqmm_partner_review_required",
|
||||||
|
"aqmm_manager_review_required",
|
||||||
|
"aqmm_evidence_required",
|
||||||
|
"aqmm_mandatory",
|
||||||
|
"is_aqmm_task",
|
||||||
|
]:
|
||||||
|
_drop_column_once(bind, table, col)
|
||||||
@@ -73,6 +73,14 @@ DEFAULT_TASK_COLUMNS = [
|
|||||||
"default_role_name",
|
"default_role_name",
|
||||||
"is_mandatory",
|
"is_mandatory",
|
||||||
"requires_review",
|
"requires_review",
|
||||||
|
"is_aqmm_task",
|
||||||
|
"aqmm_mandatory",
|
||||||
|
"aqmm_evidence_required",
|
||||||
|
"aqmm_manager_review_required",
|
||||||
|
"aqmm_partner_review_required",
|
||||||
|
"aqmm_review_partner_required",
|
||||||
|
"aqmm_blocks_final_release",
|
||||||
|
"aqmm_reference",
|
||||||
"is_active",
|
"is_active",
|
||||||
"description",
|
"description",
|
||||||
]
|
]
|
||||||
@@ -84,6 +92,14 @@ FIRM_TASK_COLUMNS = [
|
|||||||
"default_role_name",
|
"default_role_name",
|
||||||
"is_mandatory",
|
"is_mandatory",
|
||||||
"requires_review",
|
"requires_review",
|
||||||
|
"is_aqmm_task",
|
||||||
|
"aqmm_mandatory",
|
||||||
|
"aqmm_evidence_required",
|
||||||
|
"aqmm_manager_review_required",
|
||||||
|
"aqmm_partner_review_required",
|
||||||
|
"aqmm_review_partner_required",
|
||||||
|
"aqmm_blocks_final_release",
|
||||||
|
"aqmm_reference",
|
||||||
"is_active",
|
"is_active",
|
||||||
"description",
|
"description",
|
||||||
]
|
]
|
||||||
@@ -1010,6 +1026,14 @@ def import_system_default_tasks(db: Session, *, current_user, file_bytes: bytes,
|
|||||||
task.default_role_name = _clean(_cell(row, headers, "default_role_name")) or None
|
task.default_role_name = _clean(_cell(row, headers, "default_role_name")) or None
|
||||||
task.is_mandatory = _bool(_cell(row, headers, "is_mandatory"), True)
|
task.is_mandatory = _bool(_cell(row, headers, "is_mandatory"), True)
|
||||||
task.requires_review = _bool(_cell(row, headers, "requires_review"), False)
|
task.requires_review = _bool(_cell(row, headers, "requires_review"), False)
|
||||||
|
task.is_aqmm_task = _bool(_cell(row, headers, "is_aqmm_task"), False)
|
||||||
|
task.aqmm_mandatory = _bool(_cell(row, headers, "aqmm_mandatory"), False)
|
||||||
|
task.aqmm_evidence_required = _bool(_cell(row, headers, "aqmm_evidence_required"), False)
|
||||||
|
task.aqmm_manager_review_required = _bool(_cell(row, headers, "aqmm_manager_review_required"), False)
|
||||||
|
task.aqmm_partner_review_required = _bool(_cell(row, headers, "aqmm_partner_review_required"), False)
|
||||||
|
task.aqmm_review_partner_required = _bool(_cell(row, headers, "aqmm_review_partner_required"), False)
|
||||||
|
task.aqmm_blocks_final_release = _bool(_cell(row, headers, "aqmm_blocks_final_release"), False)
|
||||||
|
task.aqmm_reference = _clean(_cell(row, headers, "aqmm_reference")) or None
|
||||||
task.is_active = _bool(_cell(row, headers, "is_active"), True)
|
task.is_active = _bool(_cell(row, headers, "is_active"), True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append({"row": row_no, "message": str(exc)})
|
errors.append({"row": row_no, "message": str(exc)})
|
||||||
@@ -1067,6 +1091,14 @@ def import_firm_task_templates(db: Session, *, current_user, tenant_id: int, fil
|
|||||||
task.default_role_name = _clean(_cell(row, headers, "default_role_name")) or None
|
task.default_role_name = _clean(_cell(row, headers, "default_role_name")) or None
|
||||||
task.is_mandatory = _bool(_cell(row, headers, "is_mandatory"), True)
|
task.is_mandatory = _bool(_cell(row, headers, "is_mandatory"), True)
|
||||||
task.requires_review = _bool(_cell(row, headers, "requires_review"), False)
|
task.requires_review = _bool(_cell(row, headers, "requires_review"), False)
|
||||||
|
task.is_aqmm_task = _bool(_cell(row, headers, "is_aqmm_task"), False)
|
||||||
|
task.aqmm_mandatory = _bool(_cell(row, headers, "aqmm_mandatory"), False)
|
||||||
|
task.aqmm_evidence_required = _bool(_cell(row, headers, "aqmm_evidence_required"), False)
|
||||||
|
task.aqmm_manager_review_required = _bool(_cell(row, headers, "aqmm_manager_review_required"), False)
|
||||||
|
task.aqmm_partner_review_required = _bool(_cell(row, headers, "aqmm_partner_review_required"), False)
|
||||||
|
task.aqmm_review_partner_required = _bool(_cell(row, headers, "aqmm_review_partner_required"), False)
|
||||||
|
task.aqmm_blocks_final_release = _bool(_cell(row, headers, "aqmm_blocks_final_release"), False)
|
||||||
|
task.aqmm_reference = _clean(_cell(row, headers, "aqmm_reference")) or None
|
||||||
task.is_active = _bool(_cell(row, headers, "is_active"), True)
|
task.is_active = _bool(_cell(row, headers, "is_active"), True)
|
||||||
task.updated_by_user_id = current_user.id
|
task.updated_by_user_id = current_user.id
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from app.core.security.session_auth import get_current_user
|
|||||||
from app.core.templating import templates
|
from app.core.templating import templates
|
||||||
from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance
|
from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance
|
||||||
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
|
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
|
||||||
|
from app.modules.services.execution import aqmm_task_summary_for_subscription
|
||||||
from app.modules.clients.models import Client
|
from app.modules.clients.models import Client
|
||||||
from app.modules.services.client_services import (
|
from app.modules.services.client_services import (
|
||||||
SUBSCRIPTION_STATUSES,
|
SUBSCRIPTION_STATUSES,
|
||||||
@@ -398,6 +399,7 @@ def subscription_detail(request: Request, subscription_id: int):
|
|||||||
my_pending_declarations = [d for d in declarations if d.requested_user_id == user.id and d.status == "pending"]
|
my_pending_declarations = [d for d in declarations if d.requested_user_id == user.id and d.status == "pending"]
|
||||||
kyc_verification = get_latest_engagement_kyc_verification(db, subscription_id=row.id)
|
kyc_verification = get_latest_engagement_kyc_verification(db, subscription_id=row.id)
|
||||||
engagement_letter = get_current_engagement_letter(db, subscription_id=row.id)
|
engagement_letter = get_current_engagement_letter(db, subscription_id=row.id)
|
||||||
|
aqmm_task_summary = aqmm_task_summary_for_subscription(db, subscription_id=row.id)
|
||||||
|
|
||||||
tasks = db.execute(
|
tasks = db.execute(
|
||||||
select(ClientServiceTaskInstance)
|
select(ClientServiceTaskInstance)
|
||||||
@@ -417,6 +419,7 @@ def subscription_detail(request: Request, subscription_id: int):
|
|||||||
my_pending_declarations=my_pending_declarations,
|
my_pending_declarations=my_pending_declarations,
|
||||||
kyc_verification=kyc_verification,
|
kyc_verification=kyc_verification,
|
||||||
engagement_letter=engagement_letter,
|
engagement_letter=engagement_letter,
|
||||||
|
aqmm_task_summary=aqmm_task_summary,
|
||||||
can_manage=_can_manage_client_services(db, user),
|
can_manage=_can_manage_client_services(db, user),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from sqlalchemy import func, or_, select
|
|||||||
from sqlalchemy.orm import Session, selectinload
|
from sqlalchemy.orm import Session, selectinload
|
||||||
|
|
||||||
from app.modules.clients.models import Client
|
from app.modules.clients.models import Client
|
||||||
|
from app.modules.documents.models import EngagementDocument
|
||||||
from app.modules.core.iam.models import User
|
from app.modules.core.iam.models import User
|
||||||
from app.modules.services.client_services import quality_required_for_engagement, QUALITY_APPROVED
|
from app.modules.services.client_services import quality_required_for_engagement, QUALITY_APPROVED
|
||||||
from app.modules.services.models import (
|
from app.modules.services.models import (
|
||||||
@@ -222,6 +223,16 @@ def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceS
|
|||||||
internal_target_date=_default_internal_target_date(subscription, template),
|
internal_target_date=_default_internal_target_date(subscription, template),
|
||||||
status="pending",
|
status="pending",
|
||||||
priority="normal",
|
priority="normal",
|
||||||
|
is_aqmm_task=getattr(template, "is_aqmm_task", False),
|
||||||
|
aqmm_mandatory=getattr(template, "aqmm_mandatory", False),
|
||||||
|
aqmm_evidence_required=getattr(template, "aqmm_evidence_required", False),
|
||||||
|
aqmm_manager_review_required=getattr(template, "aqmm_manager_review_required", False),
|
||||||
|
aqmm_partner_review_required=getattr(template, "aqmm_partner_review_required", False),
|
||||||
|
aqmm_review_partner_required=getattr(template, "aqmm_review_partner_required", False),
|
||||||
|
aqmm_blocks_final_release=getattr(template, "aqmm_blocks_final_release", False),
|
||||||
|
aqmm_reference=getattr(template, "aqmm_reference", None),
|
||||||
|
aqmm_status="pending" if getattr(template, "is_aqmm_task", False) else "not_required",
|
||||||
|
aqmm_review_status="pending_review" if (getattr(template, "aqmm_manager_review_required", False) or getattr(template, "aqmm_partner_review_required", False) or getattr(template, "aqmm_review_partner_required", False)) else "not_required",
|
||||||
is_active=True,
|
is_active=True,
|
||||||
created_by_user_id=user_id,
|
created_by_user_id=user_id,
|
||||||
updated_by_user_id=user_id,
|
updated_by_user_id=user_id,
|
||||||
@@ -231,6 +242,89 @@ def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceS
|
|||||||
return created
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _task_has_evidence(db: Session, task_id: int) -> bool:
|
||||||
|
return bool(
|
||||||
|
db.execute(
|
||||||
|
select(EngagementDocument.id).where(
|
||||||
|
EngagementDocument.task_instance_id == int(task_id),
|
||||||
|
EngagementDocument.is_deleted.is_(False),
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _aqmm_task_issue(db: Session, task: ClientServiceTaskInstance) -> str | None:
|
||||||
|
"""Return blocking reason for one AQMM-tagged task, or None if it passes.
|
||||||
|
|
||||||
|
This deliberately reuses the existing task status and task document upload
|
||||||
|
workflow. Review flags are captured for reporting and future review-specific
|
||||||
|
workflow; until dedicated manager/partner review statuses are added, the
|
||||||
|
task must at least be completed and evidence uploaded wherever marked.
|
||||||
|
"""
|
||||||
|
if not getattr(task, "is_aqmm_task", False):
|
||||||
|
return None
|
||||||
|
if getattr(task, "aqmm_mandatory", False) and task.status != "completed":
|
||||||
|
return "Task not completed"
|
||||||
|
if getattr(task, "aqmm_evidence_required", False) and not _task_has_evidence(db, task.id):
|
||||||
|
return "Evidence not uploaded"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_aqmm_quality_tasks(db: Session, *, subscription_id: int) -> list[dict]:
|
||||||
|
tasks = db.execute(
|
||||||
|
select(ClientServiceTaskInstance)
|
||||||
|
.where(
|
||||||
|
ClientServiceTaskInstance.subscription_id == int(subscription_id),
|
||||||
|
ClientServiceTaskInstance.is_active.is_(True),
|
||||||
|
ClientServiceTaskInstance.is_aqmm_task.is_(True),
|
||||||
|
)
|
||||||
|
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
|
||||||
|
).scalars().all()
|
||||||
|
rows: list[dict] = []
|
||||||
|
for task in tasks:
|
||||||
|
has_evidence = _task_has_evidence(db, task.id)
|
||||||
|
issue = _aqmm_task_issue(db, task)
|
||||||
|
rows.append({"task": task, "has_evidence": has_evidence, "issue": issue, "passes": issue is None})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def aqmm_task_summary_for_subscription(db: Session, *, subscription_id: int) -> dict:
|
||||||
|
rows = list_aqmm_quality_tasks(db, subscription_id=subscription_id)
|
||||||
|
total = len(rows)
|
||||||
|
completed = sum(1 for r in rows if r["task"].status == "completed")
|
||||||
|
mandatory = sum(1 for r in rows if getattr(r["task"], "aqmm_mandatory", False))
|
||||||
|
mandatory_done = sum(1 for r in rows if getattr(r["task"], "aqmm_mandatory", False) and r["passes"])
|
||||||
|
evidence_required = sum(1 for r in rows if getattr(r["task"], "aqmm_evidence_required", False))
|
||||||
|
evidence_missing = sum(1 for r in rows if getattr(r["task"], "aqmm_evidence_required", False) and not r["has_evidence"])
|
||||||
|
blockers = [r for r in rows if getattr(r["task"], "aqmm_blocks_final_release", False) and not r["passes"]]
|
||||||
|
status = "not_required"
|
||||||
|
if total:
|
||||||
|
status = "completed" if mandatory_done == mandatory and evidence_missing == 0 else "in_progress"
|
||||||
|
return {
|
||||||
|
"rows": rows,
|
||||||
|
"total": total,
|
||||||
|
"completed": completed,
|
||||||
|
"mandatory": mandatory,
|
||||||
|
"mandatory_done": mandatory_done,
|
||||||
|
"evidence_required": evidence_required,
|
||||||
|
"evidence_missing": evidence_missing,
|
||||||
|
"blockers": blockers,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def assert_aqmm_quality_tasks_complete(db: Session, *, subscription_id: int) -> None:
|
||||||
|
summary = aqmm_task_summary_for_subscription(db, subscription_id=subscription_id)
|
||||||
|
if summary["blockers"]:
|
||||||
|
first = summary["blockers"][0]
|
||||||
|
task = first["task"]
|
||||||
|
raise ValueError(f"AQMM final release blocked: {task.task_name} - {first['issue']}")
|
||||||
|
if summary["mandatory"] and summary["mandatory_done"] < summary["mandatory"]:
|
||||||
|
raise ValueError("AQMM mandatory quality checklist tasks are pending.")
|
||||||
|
if summary["evidence_missing"]:
|
||||||
|
raise ValueError("AQMM evidence upload is pending for one or more quality checklist tasks.")
|
||||||
|
|
||||||
def _decorate_task_for_tracker(task: ClientServiceTaskInstance, *, today: date) -> ClientServiceTaskInstance:
|
def _decorate_task_for_tracker(task: ClientServiceTaskInstance, *, today: date) -> ClientServiceTaskInstance:
|
||||||
target_date = getattr(task, "internal_target_date", None)
|
target_date = getattr(task, "internal_target_date", None)
|
||||||
subscription = getattr(task, "subscription", None)
|
subscription = getattr(task, "subscription", None)
|
||||||
|
|||||||
@@ -111,6 +111,19 @@ class ServiceDefaultTaskTemplate(CommonBase):
|
|||||||
default_role_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
default_role_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
|
# AQMM task tags. These flags allow the existing service checklist to become
|
||||||
|
# the engagement quality checklist for assurance engagements, without creating
|
||||||
|
# a separate duplicate AQMM checklist module.
|
||||||
|
is_aqmm_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
|
||||||
|
aqmm_mandatory: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_evidence_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_manager_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_partner_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_review_partner_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_blocks_final_release: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_reference: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
||||||
|
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
|
|
||||||
catalogue = relationship("ServiceCatalogue", back_populates="default_task_templates")
|
catalogue = relationship("ServiceCatalogue", back_populates="default_task_templates")
|
||||||
@@ -166,6 +179,17 @@ class FirmServiceTaskTemplate(CommonBase):
|
|||||||
default_role_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
default_role_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
|
# AQMM task tags copied into generated ClientServiceTaskInstance rows.
|
||||||
|
is_aqmm_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
|
||||||
|
aqmm_mandatory: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_evidence_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_manager_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_partner_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_review_partner_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_blocks_final_release: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_reference: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
||||||
|
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
|
|
||||||
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||||
@@ -572,6 +596,21 @@ class ClientServiceTaskInstance(CommonBase):
|
|||||||
internal_target_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
|
internal_target_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
|
||||||
status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending", index=True)
|
status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending", index=True)
|
||||||
priority: Mapped[str] = mapped_column(String(20), nullable=False, default="normal")
|
priority: Mapped[str] = mapped_column(String(20), nullable=False, default="normal")
|
||||||
|
|
||||||
|
# Copied from FirmServiceTaskTemplate at generation time. Existing task
|
||||||
|
# completion/evidence upload flow is reused to calculate AQMM quality status.
|
||||||
|
is_aqmm_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
|
||||||
|
aqmm_mandatory: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_evidence_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_manager_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_partner_review_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_review_partner_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_blocks_final_release: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
aqmm_reference: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
||||||
|
aqmm_status: Mapped[str] = mapped_column(String(40), default="not_required", nullable=False, index=True)
|
||||||
|
aqmm_review_status: Mapped[str] = mapped_column(String(40), default="not_required", nullable=False, index=True)
|
||||||
|
aqmm_completed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
remarks: Mapped[str | None] = mapped_column(Text, nullable=True)
|
remarks: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|
||||||
started_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
started_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|||||||
@@ -47,6 +47,24 @@
|
|||||||
<textarea name="description" rows="4" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">{{ task.description or '' }}</textarea>
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<div class="mt-3 grid gap-3 md:grid-cols-2 text-sm text-slate-700">
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="is_aqmm_task" {% if task.is_aqmm_task %}checked{% endif %}> Part of AQMM / quality checklist</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_mandatory" {% if task.aqmm_mandatory %}checked{% endif %}> Mandatory for AQMM completion</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_evidence_required" {% if task.aqmm_evidence_required %}checked{% endif %}> Evidence upload required</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_manager_review_required" {% if task.aqmm_manager_review_required %}checked{% endif %}> Manager review required</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_partner_review_required" {% if task.aqmm_partner_review_required %}checked{% endif %}> Partner review required</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_review_partner_required" {% if task.aqmm_review_partner_required %}checked{% endif %}> Review partner review required</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_blocks_final_release" {% if task.aqmm_blocks_final_release %}checked{% endif %}> Block final release until completed</label>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-indigo-800">AQMM Reference / Clause</label>
|
||||||
|
<input type="text" name="aqmm_reference" value="{{ task.aqmm_reference or '' }}" class="w-full rounded-xl border border-indigo-200 px-3 py-2 text-sm" placeholder="e.g. Acceptance / Planning / Review">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="md:col-span-2 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
<div class="md:col-span-2 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
||||||
To deactivate this system default task, untick <strong>Active</strong> and save. Existing firm-copied tasks will not be automatically changed.
|
To deactivate this system default task, untick <strong>Active</strong> and save. Existing firm-copied tasks will not be automatically changed.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="mt-1 text-xs text-slate-500">
|
<div class="mt-1 text-xs text-slate-500">
|
||||||
Role: {{ task.default_role_name or '-' }} ·
|
Role: {{ task.default_role_name or '-' }} ·
|
||||||
Mandatory: {{ 'Yes' if task.is_mandatory else 'No' }} ·
|
Mandatory: {{ 'Yes' if task.is_mandatory else 'No' }} ·
|
||||||
Review: {{ 'Yes' if task.requires_review else 'No' }}
|
Review: {{ 'Yes' if task.requires_review else 'No' }} · AQMM: {{ 'Yes' if task.is_aqmm_task else 'No' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@@ -46,6 +46,19 @@
|
|||||||
<div><label class="mb-2 block text-sm font-medium text-slate-700">Sequence No</label><input type="number" min="1" name="sequence_no" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"></div>
|
<div><label class="mb-2 block text-sm font-medium text-slate-700">Sequence No</label><input type="number" min="1" name="sequence_no" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"></div>
|
||||||
<div class="flex items-center gap-6 pt-7 text-sm text-slate-700"><label class="inline-flex items-center gap-2"><input type="checkbox" name="is_mandatory" checked> Mandatory</label><label class="inline-flex items-center gap-2"><input type="checkbox" name="requires_review"> Review</label><label class="inline-flex items-center gap-2"><input type="checkbox" name="is_active" checked> Active</label></div>
|
<div class="flex items-center gap-6 pt-7 text-sm text-slate-700"><label class="inline-flex items-center gap-2"><input type="checkbox" name="is_mandatory" checked> Mandatory</label><label class="inline-flex items-center gap-2"><input type="checkbox" name="requires_review"> Review</label><label class="inline-flex items-center gap-2"><input type="checkbox" name="is_active" checked> Active</label></div>
|
||||||
<div class="md:col-span-2"><label class="mb-2 block text-sm font-medium text-slate-700">Description</label><textarea name="description" rows="4" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"></textarea></div>
|
<div class="md:col-span-2"><label class="mb-2 block text-sm font-medium text-slate-700">Description</label><textarea name="description" rows="4" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"></textarea></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>
|
||||||
|
<div class="mt-3 grid gap-2 md:grid-cols-4 text-xs text-slate-700">
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="is_aqmm_task"> Part of AQMM</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_mandatory"> Mandatory</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_evidence_required"> Evidence required</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_manager_review_required"> Manager review</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_partner_review_required"> Partner review</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_review_partner_required"> Review partner review</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_blocks_final_release"> Blocks final release</label>
|
||||||
|
<input name="aqmm_reference" placeholder="AQMM reference / clause" class="rounded-lg border border-indigo-200 px-2 py-1 text-xs">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="md:col-span-2 flex justify-end"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Add Default Task</button></div>
|
<div class="md:col-span-2 flex justify-end"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Add Default Task</button></div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -45,6 +45,41 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-5 rounded-2xl border border-white bg-white p-4">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-slate-900">AQMM Quality Checklist Tasks</h4>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">These are your existing service tasks tagged as part of AQMM. No separate checklist is created.</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-xs md:grid-cols-4">
|
||||||
|
<span class="rounded-xl bg-slate-100 px-3 py-2">Total: <strong>{{ aqmm_task_summary.total if aqmm_task_summary else 0 }}</strong></span>
|
||||||
|
<span class="rounded-xl bg-emerald-100 px-3 py-2 text-emerald-800">Completed: <strong>{{ aqmm_task_summary.completed if aqmm_task_summary else 0 }}</strong></span>
|
||||||
|
<span class="rounded-xl bg-amber-100 px-3 py-2 text-amber-800">Evidence Missing: <strong>{{ aqmm_task_summary.evidence_missing if aqmm_task_summary else 0 }}</strong></span>
|
||||||
|
<span class="rounded-xl bg-purple-100 px-3 py-2 text-purple-800">Final Blockers: <strong>{{ aqmm_task_summary.blockers|length if aqmm_task_summary else 0 }}</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 overflow-hidden rounded-xl border border-slate-200">
|
||||||
|
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||||
|
<thead class="bg-slate-50"><tr><th class="px-3 py-2 text-left">Task</th><th class="px-3 py-2 text-left">AQMM Flags</th><th class="px-3 py-2 text-left">Evidence</th><th class="px-3 py-2 text-left">Status</th><th class="px-3 py-2 text-right">Action</th></tr></thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
{% for item in aqmm_task_summary.rows if aqmm_task_summary %}
|
||||||
|
{% set task = item.task %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-3 py-2"><div class="font-medium text-slate-900">{{ task.sequence_no }}. {{ task.task_name }}</div>{% if task.aqmm_reference %}<div class="text-xs text-slate-500">{{ task.aqmm_reference }}</div>{% endif %}</td>
|
||||||
|
<td class="px-3 py-2 text-xs"><div class="flex flex-wrap gap-1">{% if task.aqmm_mandatory %}<span class="rounded-full bg-rose-100 px-2 py-1 text-rose-800">Mandatory</span>{% endif %}{% if task.aqmm_evidence_required %}<span class="rounded-full bg-sky-100 px-2 py-1 text-sky-800">Evidence</span>{% endif %}{% if task.aqmm_manager_review_required %}<span class="rounded-full bg-amber-100 px-2 py-1 text-amber-800">Manager</span>{% endif %}{% if task.aqmm_partner_review_required %}<span class="rounded-full bg-indigo-100 px-2 py-1 text-indigo-800">Partner</span>{% endif %}{% if task.aqmm_review_partner_required %}<span class="rounded-full bg-purple-100 px-2 py-1 text-purple-800">Review Partner</span>{% endif %}{% if task.aqmm_blocks_final_release %}<span class="rounded-full bg-slate-900 px-2 py-1 text-white">Final Gate</span>{% endif %}</div></td>
|
||||||
|
<td class="px-3 py-2">{{ 'Uploaded' if item.has_evidence else ('Required' if task.aqmm_evidence_required else '-') }}</td>
|
||||||
|
<td class="px-3 py-2"><span class="rounded-full px-2 py-1 text-xs font-medium {{ 'bg-emerald-100 text-emerald-800' if item.passes else 'bg-amber-100 text-amber-800' }}">{{ 'OK' if item.passes else item.issue }}</span><div class="mt-1 text-xs text-slate-500">Task: {{ task.status|replace('_',' ')|title }}</div></td>
|
||||||
|
<td class="px-3 py-2 text-right"><a href="/documents/tasks/{{ task.id }}" class="text-xs font-medium text-brand-700 hover:underline">Open Evidence</a></td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="5" class="px-3 py-6 text-center text-sm text-slate-500">No AQMM-tagged task generated yet. Mark selected service tasks as AQMM and generate work tracker tasks.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if my_pending_declarations %}
|
{% if my_pending_declarations %}
|
||||||
<div class="mt-5 rounded-2xl border border-white bg-white p-4">
|
<div class="mt-5 rounded-2xl border border-white bg-white p-4">
|
||||||
<h4 class="text-sm font-semibold text-slate-900">My Pending Declarations</h4>
|
<h4 class="text-sm font-semibold text-slate-900">My Pending Declarations</h4>
|
||||||
@@ -83,7 +118,7 @@
|
|||||||
{% for task in tasks or [] %}
|
{% for task in tasks or [] %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-4 py-3 text-sm font-medium text-slate-900">{{ task.sequence_no }}</td>
|
<td class="px-4 py-3 text-sm font-medium text-slate-900">{{ task.sequence_no }}</td>
|
||||||
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ task.task_name }}</div>{% if task.description %}<div class="text-xs text-slate-500">{{ task.description }}</div>{% endif %}</td>
|
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ task.task_name }}</div>{% if task.description %}<div class="text-xs text-slate-500">{{ task.description }}</div>{% endif %}{% if task.is_aqmm_task %}<div class="mt-1 flex flex-wrap gap-1 text-[11px]"><span class="rounded-full bg-indigo-100 px-2 py-1 text-indigo-800">AQMM</span>{% if task.aqmm_mandatory %}<span class="rounded-full bg-rose-100 px-2 py-1 text-rose-800">Mandatory</span>{% endif %}{% if task.aqmm_evidence_required %}<span class="rounded-full bg-sky-100 px-2 py-1 text-sky-800">Evidence</span>{% endif %}</div>{% endif %}</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-700">{{ task.status.replace('_',' ').title() }}</td>
|
<td class="px-4 py-3 text-sm text-slate-700">{{ task.status.replace('_',' ').title() }}</td>
|
||||||
<td class="px-4 py-3 text-right"><a href="/documents/tasks/{{ task.id }}" class="text-sm font-medium text-brand-700 hover:underline">Open Task Documents</a></td>
|
<td class="px-4 py-3 text-right"><a href="/documents/tasks/{{ task.id }}" class="text-sm font-medium text-brand-700 hover:underline">Open Task Documents</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -47,6 +47,24 @@
|
|||||||
<textarea name="description" rows="4" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">{{ task.description or '' }}</textarea>
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<div class="mt-3 grid gap-3 md:grid-cols-2 text-sm text-slate-700">
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="is_aqmm_task" {% if task.is_aqmm_task %}checked{% endif %}> Part of AQMM / quality checklist</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_mandatory" {% if task.aqmm_mandatory %}checked{% endif %}> Mandatory for AQMM completion</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_evidence_required" {% if task.aqmm_evidence_required %}checked{% endif %}> Evidence upload required</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_manager_review_required" {% if task.aqmm_manager_review_required %}checked{% endif %}> Manager review required</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_partner_review_required" {% if task.aqmm_partner_review_required %}checked{% endif %}> Partner review required</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_review_partner_required" {% if task.aqmm_review_partner_required %}checked{% endif %}> Review partner review required</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input type="checkbox" name="aqmm_blocks_final_release" {% if task.aqmm_blocks_final_release %}checked{% endif %}> Block final release until completed</label>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-indigo-800">AQMM Reference / Clause</label>
|
||||||
|
<input type="text" name="aqmm_reference" value="{{ task.aqmm_reference or '' }}" class="w-full rounded-xl border border-indigo-200 px-3 py-2 text-sm" placeholder="e.g. Acceptance / Planning / Review">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="md:col-span-2 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
<div class="md:col-span-2 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
||||||
To deactivate this task, untick <strong>Active</strong> and save. No hard delete is used.
|
To deactivate this task, untick <strong>Active</strong> and save. No hard delete is used.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,6 +74,20 @@
|
|||||||
<label class="inline-flex items-center gap-1"><input type="checkbox" name="is_active" checked> Active</label>
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="is_active" checked> Active</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="lg:col-span-12 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">Tick only the service checklist tasks that should form part of assurance engagement quality evidence.</p>
|
||||||
|
<div class="mt-3 grid gap-2 md:grid-cols-4 text-xs text-slate-700">
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="is_aqmm_task"> Part of AQMM</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_mandatory"> Mandatory</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_evidence_required"> Evidence required</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_manager_review_required"> Manager review</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_partner_review_required"> Partner review</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_review_partner_required"> Review partner review</label>
|
||||||
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="aqmm_blocks_final_release"> Blocks final release</label>
|
||||||
|
<input name="aqmm_reference" placeholder="AQMM reference / clause" class="rounded-lg border border-indigo-200 px-2 py-1 text-xs">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="lg:col-span-10">
|
<div class="lg:col-span-10">
|
||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Description</label>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Description</label>
|
||||||
<textarea name="description" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Optional instructions for staff or reviewer"></textarea>
|
<textarea name="description" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Optional instructions for staff or reviewer"></textarea>
|
||||||
@@ -115,6 +129,10 @@
|
|||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{% if task.is_mandatory %}<span class="rounded-full bg-slate-100 px-2 py-1">Mandatory</span>{% endif %}
|
{% if task.is_mandatory %}<span class="rounded-full bg-slate-100 px-2 py-1">Mandatory</span>{% endif %}
|
||||||
{% if task.requires_review %}<span class="rounded-full bg-amber-100 px-2 py-1 text-amber-800">Review</span>{% endif %}
|
{% if task.requires_review %}<span class="rounded-full bg-amber-100 px-2 py-1 text-amber-800">Review</span>{% endif %}
|
||||||
|
{% if task.is_aqmm_task %}<span class="rounded-full bg-indigo-100 px-2 py-1 text-indigo-800">AQMM</span>{% endif %}
|
||||||
|
{% if task.aqmm_mandatory %}<span class="rounded-full bg-rose-100 px-2 py-1 text-rose-800">AQMM Mandatory</span>{% endif %}
|
||||||
|
{% if task.aqmm_evidence_required %}<span class="rounded-full bg-sky-100 px-2 py-1 text-sky-800">Evidence</span>{% endif %}
|
||||||
|
{% if task.aqmm_blocks_final_release %}<span class="rounded-full bg-purple-100 px-2 py-1 text-purple-800">Final Gate</span>{% endif %}
|
||||||
<span class="rounded-full px-2 py-1 {{ 'bg-emerald-100 text-emerald-800' if task.is_active else 'bg-slate-100 text-slate-500' }}">{{ 'Active' if task.is_active else 'Inactive' }}</span>
|
<span class="rounded-full px-2 py-1 {{ 'bg-emerald-100 text-emerald-800' if task.is_active else 'bg-slate-100 text-slate-500' }}">{{ 'Active' if task.is_active else 'Inactive' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -814,7 +814,7 @@ def task_templates_detail(request: Request, catalogue_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@router.post('/templates/{catalogue_id}/tasks/new')
|
@router.post('/templates/{catalogue_id}/tasks/new')
|
||||||
def task_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
def task_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), is_aqmm_task: str | None = Form(None), aqmm_mandatory: str | None = Form(None), aqmm_evidence_required: str | None = Form(None), aqmm_manager_review_required: str | None = Form(None), aqmm_partner_review_required: str | None = Form(None), aqmm_review_partner_required: str | None = Form(None), aqmm_blocks_final_release: str | None = Form(None), aqmm_reference: str = Form(''), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
||||||
validate_csrf(request, csrf_token)
|
validate_csrf(request, csrf_token)
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
@@ -839,6 +839,14 @@ def task_template_create_submit(request: Request, catalogue_id: int, task_name:
|
|||||||
sequence_no=sequence_no or next_task_sequence(db, tenant_id=tenant_id, catalogue_id=catalogue_id),
|
sequence_no=sequence_no or next_task_sequence(db, tenant_id=tenant_id, catalogue_id=catalogue_id),
|
||||||
is_mandatory=is_mandatory is not None,
|
is_mandatory=is_mandatory is not None,
|
||||||
requires_review=requires_review 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,
|
is_active=is_active is not None,
|
||||||
created_by_user_id=user.id,
|
created_by_user_id=user.id,
|
||||||
updated_by_user_id=user.id,
|
updated_by_user_id=user.id,
|
||||||
@@ -888,7 +896,7 @@ def default_templates_detail(request: Request, catalogue_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@router.post('/catalogue/{catalogue_id}/defaults/new')
|
@router.post('/catalogue/{catalogue_id}/defaults/new')
|
||||||
def default_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
def default_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), is_aqmm_task: str | None = Form(None), aqmm_mandatory: str | None = Form(None), aqmm_evidence_required: str | None = Form(None), aqmm_manager_review_required: str | None = Form(None), aqmm_partner_review_required: str | None = Form(None), aqmm_review_partner_required: str | None = Form(None), aqmm_blocks_final_release: str | None = Form(None), aqmm_reference: str = Form(''), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
||||||
validate_csrf(request, csrf_token)
|
validate_csrf(request, csrf_token)
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
@@ -901,7 +909,7 @@ def default_template_create_submit(request: Request, catalogue_id: int, task_nam
|
|||||||
catalogue = get_catalogue(db, catalogue_id)
|
catalogue = get_catalogue(db, catalogue_id)
|
||||||
if not catalogue:
|
if not catalogue:
|
||||||
return RedirectResponse(url='/services/defaults', status_code=303)
|
return RedirectResponse(url='/services/defaults', status_code=303)
|
||||||
row = ServiceDefaultTaskTemplate(service_catalogue_id=catalogue_id, task_name=task_name.strip(), description=description.strip() or None, default_role_name=default_role_name.strip() or None, sequence_no=sequence_no or next_default_task_sequence(db, catalogue_id=catalogue_id), is_mandatory=is_mandatory is not None, requires_review=requires_review is not None, is_active=is_active is not None)
|
row = ServiceDefaultTaskTemplate(service_catalogue_id=catalogue_id, task_name=task_name.strip(), description=description.strip() or None, default_role_name=default_role_name.strip() or None, sequence_no=sequence_no or next_default_task_sequence(db, catalogue_id=catalogue_id), is_mandatory=is_mandatory is not None, requires_review=requires_review is not None, is_aqmm_task=is_aqmm_task is not None, aqmm_mandatory=aqmm_mandatory is not None, aqmm_evidence_required=aqmm_evidence_required is not None, aqmm_manager_review_required=aqmm_manager_review_required is not None, aqmm_partner_review_required=aqmm_partner_review_required is not None, aqmm_review_partner_required=aqmm_review_partner_required is not None, aqmm_blocks_final_release=aqmm_blocks_final_release is not None, aqmm_reference=aqmm_reference.strip() or None, is_active=is_active is not None)
|
||||||
db.add(row); db.commit()
|
db.add(row); db.commit()
|
||||||
return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults', status_code=303)
|
return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults', status_code=303)
|
||||||
finally:
|
finally:
|
||||||
@@ -940,6 +948,14 @@ def copy_defaults_to_firm(request: Request, catalogue_id: int, csrf_token: str =
|
|||||||
default_role_name=d.default_role_name,
|
default_role_name=d.default_role_name,
|
||||||
is_mandatory=d.is_mandatory,
|
is_mandatory=d.is_mandatory,
|
||||||
requires_review=d.requires_review,
|
requires_review=d.requires_review,
|
||||||
|
is_aqmm_task=getattr(d, "is_aqmm_task", False),
|
||||||
|
aqmm_mandatory=getattr(d, "aqmm_mandatory", False),
|
||||||
|
aqmm_evidence_required=getattr(d, "aqmm_evidence_required", False),
|
||||||
|
aqmm_manager_review_required=getattr(d, "aqmm_manager_review_required", False),
|
||||||
|
aqmm_partner_review_required=getattr(d, "aqmm_partner_review_required", False),
|
||||||
|
aqmm_review_partner_required=getattr(d, "aqmm_review_partner_required", False),
|
||||||
|
aqmm_blocks_final_release=getattr(d, "aqmm_blocks_final_release", False),
|
||||||
|
aqmm_reference=getattr(d, "aqmm_reference", None),
|
||||||
is_active=d.is_active,
|
is_active=d.is_active,
|
||||||
created_by_user_id=user.id,
|
created_by_user_id=user.id,
|
||||||
updated_by_user_id=user.id,
|
updated_by_user_id=user.id,
|
||||||
@@ -1136,6 +1152,14 @@ def firm_task_template_edit_submit(
|
|||||||
sequence_no: int = Form(1),
|
sequence_no: int = Form(1),
|
||||||
is_mandatory: str | None = Form(None),
|
is_mandatory: str | None = Form(None),
|
||||||
requires_review: 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),
|
is_active: str | None = Form(None),
|
||||||
csrf_token: str = Form(...),
|
csrf_token: str = Form(...),
|
||||||
):
|
):
|
||||||
@@ -1166,6 +1190,14 @@ def firm_task_template_edit_submit(
|
|||||||
task.sequence_no = sequence_no
|
task.sequence_no = sequence_no
|
||||||
task.is_mandatory = is_mandatory is not None
|
task.is_mandatory = is_mandatory is not None
|
||||||
task.requires_review = requires_review is not None
|
task.requires_review = requires_review is not None
|
||||||
|
task.is_aqmm_task = is_aqmm_task is not None
|
||||||
|
task.aqmm_mandatory = aqmm_mandatory is not None
|
||||||
|
task.aqmm_evidence_required = aqmm_evidence_required is not None
|
||||||
|
task.aqmm_manager_review_required = aqmm_manager_review_required is not None
|
||||||
|
task.aqmm_partner_review_required = aqmm_partner_review_required is not None
|
||||||
|
task.aqmm_review_partner_required = aqmm_review_partner_required is not None
|
||||||
|
task.aqmm_blocks_final_release = aqmm_blocks_final_release is not None
|
||||||
|
task.aqmm_reference = aqmm_reference.strip() or None
|
||||||
task.is_active = is_active is not None
|
task.is_active = is_active is not None
|
||||||
task.updated_by_user_id = user.id
|
task.updated_by_user_id = user.id
|
||||||
|
|
||||||
@@ -1227,6 +1259,14 @@ def default_task_template_edit_submit(
|
|||||||
sequence_no: int = Form(1),
|
sequence_no: int = Form(1),
|
||||||
is_mandatory: str | None = Form(None),
|
is_mandatory: str | None = Form(None),
|
||||||
requires_review: 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),
|
is_active: str | None = Form(None),
|
||||||
csrf_token: str = Form(...),
|
csrf_token: str = Form(...),
|
||||||
):
|
):
|
||||||
@@ -1259,6 +1299,14 @@ def default_task_template_edit_submit(
|
|||||||
task.sequence_no = sequence_no
|
task.sequence_no = sequence_no
|
||||||
task.is_mandatory = is_mandatory is not None
|
task.is_mandatory = is_mandatory is not None
|
||||||
task.requires_review = requires_review is not None
|
task.requires_review = requires_review is not None
|
||||||
|
task.is_aqmm_task = is_aqmm_task is not None
|
||||||
|
task.aqmm_mandatory = aqmm_mandatory is not None
|
||||||
|
task.aqmm_evidence_required = aqmm_evidence_required is not None
|
||||||
|
task.aqmm_manager_review_required = aqmm_manager_review_required is not None
|
||||||
|
task.aqmm_partner_review_required = aqmm_partner_review_required is not None
|
||||||
|
task.aqmm_review_partner_required = aqmm_review_partner_required is not None
|
||||||
|
task.aqmm_blocks_final_release = aqmm_blocks_final_release is not None
|
||||||
|
task.aqmm_reference = aqmm_reference.strip() or None
|
||||||
task.is_active = is_active is not None
|
task.is_active = is_active is not None
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user