Separate normal task review from AQMM review
This commit is contained in:
@@ -0,0 +1,112 @@
|
|||||||
|
"""separate normal operational review from AQMM review
|
||||||
|
|
||||||
|
Revision ID: 20260811_normal_review
|
||||||
|
Revises: 20260808_task_role_eligibility
|
||||||
|
Create Date: 2026-08-11
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "20260811_normal_review"
|
||||||
|
down_revision = "20260808_task_role_eligibility"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("service_default_task_templates", sa.Column("normal_review_role", sa.String(length=30), nullable=True))
|
||||||
|
op.add_column("firm_service_task_templates", sa.Column("normal_review_role", sa.String(length=30), nullable=True))
|
||||||
|
|
||||||
|
op.add_column("client_service_task_instances", sa.Column("normal_review_required", sa.Boolean(), nullable=False, server_default=sa.false()))
|
||||||
|
op.add_column("client_service_task_instances", sa.Column("normal_review_role", sa.String(length=30), nullable=True))
|
||||||
|
op.add_column("client_service_task_instances", sa.Column("normal_review_status", sa.String(length=30), nullable=False, server_default="not_required"))
|
||||||
|
op.add_column("client_service_task_instances", sa.Column("normal_review_note", sa.Text(), nullable=True))
|
||||||
|
op.add_column("client_service_task_instances", sa.Column("normal_reviewed_by_user_id", sa.Integer(), nullable=True))
|
||||||
|
op.add_column("client_service_task_instances", sa.Column("normal_reviewed_at_utc", sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_task_instance_normal_reviewed_by",
|
||||||
|
"client_service_task_instances",
|
||||||
|
"users",
|
||||||
|
["normal_reviewed_by_user_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.create_index("ix_task_instance_normal_review_required", "client_service_task_instances", ["normal_review_required"])
|
||||||
|
op.create_index("ix_task_instance_normal_review_role", "client_service_task_instances", ["normal_review_role"])
|
||||||
|
op.create_index("ix_task_instance_normal_review_status", "client_service_task_instances", ["normal_review_status"])
|
||||||
|
op.create_index("ix_task_instance_normal_reviewed_by", "client_service_task_instances", ["normal_reviewed_by_user_id"])
|
||||||
|
|
||||||
|
# Preserve the old intent of requires_review while making the reviewer explicit.
|
||||||
|
# Reviewer inference follows task hierarchy: Staff -> Manager, Manager -> Partner.
|
||||||
|
role_case = """
|
||||||
|
CASE
|
||||||
|
WHEN NOT requires_review THEN NULL
|
||||||
|
WHEN lower(coalesce(default_role_name,'')) IN ('manager','branch manager')
|
||||||
|
AND lower(coalesce(eligible_role_names,'')) LIKE '%partner%' THEN 'partner'
|
||||||
|
WHEN lower(coalesce(default_role_name,'')) NOT IN ('manager','branch manager','partner')
|
||||||
|
AND (lower(coalesce(eligible_role_names,'')) LIKE '%manager%' OR lower(coalesce(eligible_role_names,'')) LIKE '%branch manager%') THEN 'manager'
|
||||||
|
WHEN lower(coalesce(eligible_role_names,'')) LIKE '%partner%'
|
||||||
|
AND lower(coalesce(eligible_role_names,'')) NOT LIKE '%manager%' THEN 'partner'
|
||||||
|
ELSE 'manager'
|
||||||
|
END
|
||||||
|
"""
|
||||||
|
op.execute(sa.text(f"UPDATE service_default_task_templates SET normal_review_role = {role_case} WHERE requires_review = TRUE"))
|
||||||
|
op.execute(sa.text(f"UPDATE firm_service_task_templates SET normal_review_role = {role_case} WHERE requires_review = TRUE"))
|
||||||
|
|
||||||
|
# Snapshot normal-review configuration into every existing engagement task.
|
||||||
|
op.execute(sa.text("""
|
||||||
|
UPDATE client_service_task_instances AS t
|
||||||
|
SET normal_review_required = f.requires_review,
|
||||||
|
normal_review_role = f.normal_review_role
|
||||||
|
FROM firm_service_task_templates AS f
|
||||||
|
WHERE t.firm_task_template_id = f.id
|
||||||
|
"""))
|
||||||
|
|
||||||
|
# Preserve prior generic approvals where they were already recorded in the legacy
|
||||||
|
# manager/partner fields. Otherwise completed/submitted work becomes Pending Review;
|
||||||
|
# unstarted work remains Not Ready. AQMM fields themselves are not changed.
|
||||||
|
op.execute(sa.text("""
|
||||||
|
UPDATE client_service_task_instances
|
||||||
|
SET normal_review_status = CASE
|
||||||
|
WHEN NOT normal_review_required THEN 'not_required'
|
||||||
|
WHEN normal_review_role IN ('manager','manager_or_partner') AND manager_review_status = 'reviewed' THEN 'reviewed'
|
||||||
|
WHEN normal_review_role = 'partner' AND partner_review_status = 'reviewed' THEN 'reviewed'
|
||||||
|
WHEN rework_status = 'open' THEN 'rework_required'
|
||||||
|
WHEN submitted_for_review_at_utc IS NOT NULL OR lower(coalesce(status,'')) = 'completed' THEN 'pending'
|
||||||
|
ELSE 'not_ready'
|
||||||
|
END,
|
||||||
|
normal_review_note = CASE
|
||||||
|
WHEN normal_review_role IN ('manager','manager_or_partner') AND manager_review_status IN ('reviewed','rework_required') THEN manager_review_note
|
||||||
|
WHEN normal_review_role = 'partner' AND partner_review_status IN ('reviewed','rework_required') THEN partner_review_note
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
normal_reviewed_by_user_id = CASE
|
||||||
|
WHEN normal_review_role IN ('manager','manager_or_partner') AND manager_review_status = 'reviewed' THEN manager_reviewed_by_user_id
|
||||||
|
WHEN normal_review_role = 'partner' AND partner_review_status = 'reviewed' THEN partner_reviewed_by_user_id
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
normal_reviewed_at_utc = CASE
|
||||||
|
WHEN normal_review_role IN ('manager','manager_or_partner') AND manager_review_status = 'reviewed' THEN manager_reviewed_at_utc
|
||||||
|
WHEN normal_review_role = 'partner' AND partner_review_status = 'reviewed' THEN partner_reviewed_at_utc
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
"""))
|
||||||
|
|
||||||
|
op.alter_column("client_service_task_instances", "normal_review_required", server_default=None)
|
||||||
|
op.alter_column("client_service_task_instances", "normal_review_status", server_default=None)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_task_instance_normal_reviewed_by", table_name="client_service_task_instances")
|
||||||
|
op.drop_index("ix_task_instance_normal_review_status", table_name="client_service_task_instances")
|
||||||
|
op.drop_index("ix_task_instance_normal_review_role", table_name="client_service_task_instances")
|
||||||
|
op.drop_index("ix_task_instance_normal_review_required", table_name="client_service_task_instances")
|
||||||
|
op.drop_constraint("fk_task_instance_normal_reviewed_by", "client_service_task_instances", type_="foreignkey")
|
||||||
|
op.drop_column("client_service_task_instances", "normal_reviewed_at_utc")
|
||||||
|
op.drop_column("client_service_task_instances", "normal_reviewed_by_user_id")
|
||||||
|
op.drop_column("client_service_task_instances", "normal_review_note")
|
||||||
|
op.drop_column("client_service_task_instances", "normal_review_status")
|
||||||
|
op.drop_column("client_service_task_instances", "normal_review_role")
|
||||||
|
op.drop_column("client_service_task_instances", "normal_review_required")
|
||||||
|
op.drop_column("firm_service_task_templates", "normal_review_role")
|
||||||
|
op.drop_column("service_default_task_templates", "normal_review_role")
|
||||||
@@ -3489,34 +3489,34 @@ def _employee_task_category(task: ClientServiceTaskInstance) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _i1_required_review_levels(task: ClientServiceTaskInstance) -> list[str]:
|
def _i1_required_review_levels(task: ClientServiceTaskInstance) -> list[str]:
|
||||||
"""Return configured review levels for I1 using existing task-template controls."""
|
"""Return normal and AQMM review gates without mixing their state."""
|
||||||
levels: list[str] = []
|
levels: list[str] = []
|
||||||
if getattr(task, "aqmm_manager_review_required", False):
|
normal_role = (getattr(task, "normal_review_role", None) or "").strip().lower()
|
||||||
levels.append("manager")
|
if getattr(task, "normal_review_required", False):
|
||||||
if getattr(task, "aqmm_partner_review_required", False):
|
if normal_role == "manager":
|
||||||
levels.append("partner")
|
levels.append("normal_manager")
|
||||||
if getattr(task, "aqmm_review_partner_required", False):
|
elif normal_role == "partner":
|
||||||
levels.append("review_partner")
|
levels.append("normal_partner")
|
||||||
|
elif normal_role == "manager_or_partner":
|
||||||
|
levels.append("normal_manager_or_partner")
|
||||||
|
|
||||||
# Preserve the pre-existing generic requires_review flag. Where no explicit
|
if getattr(task, "aqmm_manager_review_required", False):
|
||||||
# reviewer flag was configured, route to Manager when available, otherwise
|
levels.append("aqmm_manager")
|
||||||
# Engagement Partner. No new review table/state is introduced.
|
if getattr(task, "aqmm_partner_review_required", False):
|
||||||
template = getattr(task, "template", None)
|
levels.append("aqmm_partner")
|
||||||
if not levels and getattr(template, "requires_review", False):
|
if getattr(task, "aqmm_review_partner_required", False):
|
||||||
subscription = getattr(task, "subscription", None)
|
levels.append("aqmm_review_partner")
|
||||||
if getattr(subscription, "assigned_manager_user_id", None):
|
|
||||||
levels.append("manager")
|
|
||||||
elif getattr(subscription, "assigned_partner_user_id", None):
|
|
||||||
levels.append("partner")
|
|
||||||
return levels
|
return levels
|
||||||
|
|
||||||
|
|
||||||
def _i1_review_level_pending(task: ClientServiceTaskInstance, level: str) -> bool:
|
def _i1_review_level_pending(task: ClientServiceTaskInstance, level: str) -> bool:
|
||||||
if level == "manager":
|
if level.startswith("normal_"):
|
||||||
|
return (getattr(task, "normal_review_status", None) or "not_required") != "reviewed"
|
||||||
|
if level == "aqmm_manager":
|
||||||
return (getattr(task, "manager_review_status", None) or "not_required") != "reviewed"
|
return (getattr(task, "manager_review_status", None) or "not_required") != "reviewed"
|
||||||
if level == "partner":
|
if level == "aqmm_partner":
|
||||||
return (getattr(task, "partner_review_status", None) or "not_required") != "reviewed"
|
return (getattr(task, "partner_review_status", None) or "not_required") != "reviewed"
|
||||||
if level == "review_partner":
|
if level == "aqmm_review_partner":
|
||||||
return (getattr(task, "review_partner_review_status", None) or "not_required") != "reviewed"
|
return (getattr(task, "review_partner_review_status", None) or "not_required") != "reviewed"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -3531,9 +3531,12 @@ def _i1_review_label(task: ClientServiceTaskInstance) -> str | None:
|
|||||||
if not pending:
|
if not pending:
|
||||||
return "Approved"
|
return "Approved"
|
||||||
labels = {
|
labels = {
|
||||||
"manager": "Manager",
|
"normal_manager": "Manager",
|
||||||
"partner": "Partner",
|
"normal_partner": "Partner",
|
||||||
"review_partner": "Review Partner",
|
"normal_manager_or_partner": "Manager / Partner",
|
||||||
|
"aqmm_manager": "AQMM Manager",
|
||||||
|
"aqmm_partner": "AQMM Partner",
|
||||||
|
"aqmm_review_partner": "AQMM Review Partner",
|
||||||
}
|
}
|
||||||
return "Awaiting " + " + ".join(labels[level] for level in pending) + " approval"
|
return "Awaiting " + " + ".join(labels[level] for level in pending) + " approval"
|
||||||
|
|
||||||
@@ -3554,9 +3557,12 @@ def _i1_approval_blocker(
|
|||||||
if not pending:
|
if not pending:
|
||||||
continue
|
continue
|
||||||
labels = {
|
labels = {
|
||||||
"manager": "Manager",
|
"normal_manager": "Manager",
|
||||||
"partner": "Partner",
|
"normal_partner": "Partner",
|
||||||
"review_partner": "Review Partner",
|
"normal_manager_or_partner": "Manager / Partner",
|
||||||
|
"aqmm_manager": "AQMM Manager",
|
||||||
|
"aqmm_partner": "AQMM Partner",
|
||||||
|
"aqmm_review_partner": "AQMM Review Partner",
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
"task_id": int(row.id),
|
"task_id": int(row.id),
|
||||||
@@ -3620,6 +3626,8 @@ def _workflow_task_weight(task: ClientServiceTaskInstance) -> int:
|
|||||||
weight += 2
|
weight += 2
|
||||||
if bool(getattr(task, "aqmm_blocks_final_release", False)):
|
if bool(getattr(task, "aqmm_blocks_final_release", False)):
|
||||||
weight += 2
|
weight += 2
|
||||||
|
if bool(getattr(task, "normal_review_required", False)):
|
||||||
|
weight += 1
|
||||||
if any(bool(getattr(task, name, False)) for name in (
|
if any(bool(getattr(task, name, False)) for name in (
|
||||||
"aqmm_manager_review_required",
|
"aqmm_manager_review_required",
|
||||||
"aqmm_partner_review_required",
|
"aqmm_partner_review_required",
|
||||||
@@ -3971,7 +3979,7 @@ def save_employee_workflow_task(
|
|||||||
|
|
||||||
engagement_tasks_for_gate = db.execute(
|
engagement_tasks_for_gate = db.execute(
|
||||||
_employee_work_task_query(
|
_employee_work_task_query(
|
||||||
db, scope, assigned_only=True, financial_year=financial_year
|
db, scope, assigned_only=False, financial_year=financial_year
|
||||||
)
|
)
|
||||||
.where(ClientServiceTaskInstance.subscription_id == task.subscription_id)
|
.where(ClientServiceTaskInstance.subscription_id == task.subscription_id)
|
||||||
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
|
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
|
||||||
@@ -4025,7 +4033,7 @@ def save_employee_workflow_task(
|
|||||||
|
|
||||||
engagement_tasks = db.execute(
|
engagement_tasks = db.execute(
|
||||||
_employee_work_task_query(
|
_employee_work_task_query(
|
||||||
db, scope, assigned_only=True, financial_year=financial_year
|
db, scope, assigned_only=False, financial_year=financial_year
|
||||||
)
|
)
|
||||||
.where(ClientServiceTaskInstance.subscription_id == task.subscription_id)
|
.where(ClientServiceTaskInstance.subscription_id == task.subscription_id)
|
||||||
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
|
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
|
||||||
|
|||||||
@@ -309,7 +309,19 @@ def _advanced_engagement_rows(tasks: list[ClientServiceTaskInstance]) -> list[di
|
|||||||
team = _engagement_team(subscription)
|
team = _engagement_team(subscription)
|
||||||
status_values = {(getattr(t, "status", None) or "pending").strip().lower() for t in group}
|
status_values = {(getattr(t, "status", None) or "pending").strip().lower() for t in group}
|
||||||
completed = sum(1 for t in group if (getattr(t, "status", None) or "pending").strip().lower() in CLOSED_TASK_STATUSES)
|
completed = sum(1 for t in group if (getattr(t, "status", None) or "pending").strip().lower() in CLOSED_TASK_STATUSES)
|
||||||
review_pending = sum(1 for t in group if (getattr(t, "manager_review_status", None) or "") == "pending" or (getattr(t, "partner_review_status", None) or "") == "pending" or (getattr(t, "review_partner_review_status", None) or "") == "pending")
|
review_pending = sum(
|
||||||
|
1
|
||||||
|
for t in group
|
||||||
|
if (
|
||||||
|
getattr(t, "normal_review_required", False)
|
||||||
|
and (getattr(t, "normal_review_role", None) or "").strip().lower() in {"manager", "manager_or_partner"}
|
||||||
|
and (getattr(t, "normal_review_status", None) or "not_required") in {"pending", "rework_required"}
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
getattr(t, "aqmm_manager_review_required", False)
|
||||||
|
and (getattr(t, "manager_review_status", None) or "not_required") in {"pending", "rework_required"}
|
||||||
|
)
|
||||||
|
)
|
||||||
if completed == len(group) and group:
|
if completed == len(group) and group:
|
||||||
status = "completed"
|
status = "completed"
|
||||||
elif "blocked" in status_values or getattr(subscription, "workflow_pause_reason", None):
|
elif "blocked" in status_values or getattr(subscription, "workflow_pause_reason", None):
|
||||||
@@ -482,19 +494,33 @@ _MANAGER_REVIEW_DONE = {"reviewed"}
|
|||||||
_MANAGER_REVIEW_REWORK = {"rework_required"}
|
_MANAGER_REVIEW_REWORK = {"rework_required"}
|
||||||
|
|
||||||
|
|
||||||
def _manager_review_task_state(task: ClientServiceTaskInstance) -> str:
|
def _manager_review_task_state(task: ClientServiceTaskInstance) -> tuple[str, str | None]:
|
||||||
|
normal_role = (getattr(task, "normal_review_role", None) or "").strip().lower()
|
||||||
|
normal_status = (getattr(task, "normal_review_status", None) or "not_required").strip().lower()
|
||||||
manager_status = (getattr(task, "manager_review_status", None) or "not_required").strip().lower()
|
manager_status = (getattr(task, "manager_review_status", None) or "not_required").strip().lower()
|
||||||
task_status = (getattr(task, "status", None) or "pending").strip().lower()
|
|
||||||
rework_status = (getattr(task, "rework_status", None) or "none").strip().lower()
|
rework_status = (getattr(task, "rework_status", None) or "none").strip().lower()
|
||||||
|
|
||||||
|
if getattr(task, "normal_review_required", False) and normal_role in {"manager", "manager_or_partner"}:
|
||||||
|
if normal_status == "rework_required" or rework_status == "open":
|
||||||
|
return "rework", "normal"
|
||||||
|
if normal_status == "reviewed":
|
||||||
|
if not getattr(task, "aqmm_manager_review_required", False):
|
||||||
|
return "reviewed", "normal"
|
||||||
|
elif normal_status == "pending":
|
||||||
|
return "pending_review", "normal"
|
||||||
|
|
||||||
|
if getattr(task, "aqmm_manager_review_required", False):
|
||||||
if manager_status in _MANAGER_REVIEW_REWORK or rework_status == "open":
|
if manager_status in _MANAGER_REVIEW_REWORK or rework_status == "open":
|
||||||
return "rework"
|
return "rework", "aqmm_manager"
|
||||||
if manager_status in _MANAGER_REVIEW_DONE:
|
if manager_status in _MANAGER_REVIEW_DONE:
|
||||||
return "reviewed"
|
return "reviewed", "aqmm_manager"
|
||||||
if manager_status in _MANAGER_REVIEW_PENDING or task_status in REVIEW_STATUSES:
|
if manager_status in _MANAGER_REVIEW_PENDING:
|
||||||
return "pending_review"
|
return "pending_review", "aqmm_manager"
|
||||||
if getattr(task, "aqmm_manager_review_required", False) and task_status in CLOSED_TASK_STATUSES:
|
task_status = (getattr(task, "status", None) or "pending").strip().lower()
|
||||||
return "pending_review"
|
if task_status in CLOSED_TASK_STATUSES:
|
||||||
return "not_ready"
|
return "pending_review", "aqmm_manager"
|
||||||
|
|
||||||
|
return "not_ready", None
|
||||||
|
|
||||||
|
|
||||||
def _manager_review_category(task: ClientServiceTaskInstance) -> str:
|
def _manager_review_category(task: ClientServiceTaskInstance) -> str:
|
||||||
@@ -502,7 +528,7 @@ def _manager_review_category(task: ClientServiceTaskInstance) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _manager_review_task_row(task: ClientServiceTaskInstance) -> dict[str, Any]:
|
def _manager_review_task_row(task: ClientServiceTaskInstance) -> dict[str, Any]:
|
||||||
state = _manager_review_task_state(task)
|
state, review_kind = _manager_review_task_state(task)
|
||||||
comments = [c for c in (getattr(task, "comments", None) or []) if not getattr(c, "is_deleted", False)]
|
comments = [c for c in (getattr(task, "comments", None) or []) if not getattr(c, "is_deleted", False)]
|
||||||
documents = list(getattr(task, "documents", None) or [])
|
documents = list(getattr(task, "documents", None) or [])
|
||||||
assignee = getattr(task, "assigned_to", None)
|
assignee = getattr(task, "assigned_to", None)
|
||||||
@@ -516,6 +542,10 @@ def _manager_review_task_row(task: ClientServiceTaskInstance) -> dict[str, Any]:
|
|||||||
"task_status_label": _status_label(getattr(task, "status", None)),
|
"task_status_label": _status_label(getattr(task, "status", None)),
|
||||||
"review_state": state,
|
"review_state": state,
|
||||||
"review_state_label": state.replace("_", " ").title(),
|
"review_state_label": state.replace("_", " ").title(),
|
||||||
|
"review_kind": review_kind,
|
||||||
|
"review_kind_label": "Normal Review" if review_kind == "normal" else ("AQMM Manager Review" if review_kind == "aqmm_manager" else ""),
|
||||||
|
"normal_review_status": (getattr(task, "normal_review_status", None) or "not_required").strip().lower(),
|
||||||
|
"normal_review_note": getattr(task, "normal_review_note", None) or "",
|
||||||
"manager_review_status": (getattr(task, "manager_review_status", None) or "not_required").strip().lower(),
|
"manager_review_status": (getattr(task, "manager_review_status", None) or "not_required").strip().lower(),
|
||||||
"manager_review_note": getattr(task, "manager_review_note", None) or "",
|
"manager_review_note": getattr(task, "manager_review_note", None) or "",
|
||||||
"rework_status": (getattr(task, "rework_status", None) or "none").strip().lower(),
|
"rework_status": (getattr(task, "rework_status", None) or "none").strip().lower(),
|
||||||
|
|||||||
+3
-3
@@ -57,7 +57,7 @@
|
|||||||
<div><div class="text-xs font-semibold uppercase text-slate-500">{{ task.category }}</div><h2 class="mt-1 text-xl font-semibold text-slate-900">{{ task.task_name }}</h2><p class="mt-2 whitespace-pre-line text-sm text-slate-600">{{ task.description }}</p></div>
|
<div><div class="text-xs font-semibold uppercase text-slate-500">{{ task.category }}</div><h2 class="mt-1 text-xl font-semibold text-slate-900">{{ task.task_name }}</h2><p class="mt-2 whitespace-pre-line text-sm text-slate-600">{{ task.description }}</p></div>
|
||||||
<div class="flex flex-wrap gap-2"><span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold">{{ task.task_status_label }}</span>{% if task.is_aqmm_task %}<span class="rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold text-indigo-700">AQMM</span>{% endif %}{% if task.blocks_final_release %}<span class="rounded-full bg-red-100 px-3 py-1 text-xs font-semibold text-red-700">Final Gate</span>{% endif %}</div>
|
<div class="flex flex-wrap gap-2"><span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold">{{ task.task_status_label }}</span>{% if task.is_aqmm_task %}<span class="rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold text-indigo-700">AQMM</span>{% endif %}{% if task.blocks_final_release %}<span class="rounded-full bg-red-100 px-3 py-1 text-xs font-semibold text-red-700">Final Gate</span>{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4"><div class="rounded-xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Prepared By</div><div class="mt-1 font-semibold">{{ task.assigned_to }}</div></div><div class="rounded-xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Evidence</div><div class="mt-1 font-semibold">{{ task.evidence_count }} document(s)</div></div><div class="rounded-xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Comments</div><div class="mt-1 font-semibold">{{ task.comment_count }}</div></div><div class="rounded-xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Manager Review</div><div class="mt-1 font-semibold">{{ task.review_state_label }}</div></div></div>
|
<div class="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4"><div class="rounded-xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Prepared By</div><div class="mt-1 font-semibold">{{ task.assigned_to }}</div></div><div class="rounded-xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Evidence</div><div class="mt-1 font-semibold">{{ task.evidence_count }} document(s)</div></div><div class="rounded-xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Comments</div><div class="mt-1 font-semibold">{{ task.comment_count }}</div></div><div class="rounded-xl bg-slate-50 p-3"><div class="text-xs text-slate-500">Manager Review</div><div class="mt-1 font-semibold">{{ task.review_state_label }}</div><div class="mt-1 text-xs text-slate-500">{{ task.review_kind_label }}</div></div></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid gap-4 lg:grid-cols-2">
|
<section class="grid gap-4 lg:grid-cols-2">
|
||||||
@@ -66,12 +66,12 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="af-card">
|
<section class="af-card">
|
||||||
<h3 class="font-semibold text-slate-900">Manager Review Decision</h3>
|
<h3 class="font-semibold text-slate-900">{{ task.review_kind_label or "Manager Review" }} Decision</h3>
|
||||||
{% if task.is_locked or workspace.is_locked %}<div class="mt-3 rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">This task or engagement is locked. Review is read-only.</div>{% endif %}
|
{% if task.is_locked or workspace.is_locked %}<div class="mt-3 rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">This task or engagement is locked. Review is read-only.</div>{% endif %}
|
||||||
<form method="post" action="/manager/reviews/engagements/{{ workspace.subscription_id }}/tasks/{{ task.id }}" class="mt-4 space-y-4">
|
<form method="post" action="/manager/reviews/engagements/{{ workspace.subscription_id }}/tasks/{{ task.id }}" class="mt-4 space-y-4">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<div class="grid gap-3 md:grid-cols-2"><label class="block"><span class="text-sm font-semibold text-slate-700">Decision</span><select name="decision" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" {% if task.is_locked or workspace.is_locked or task.review_state == 'not_ready' %}disabled{% endif %}><option value="reviewed">Approve / Reviewed</option><option value="rework_required">Return for Rework</option></select></label><label class="block"><span class="text-sm font-semibold text-slate-700">Review state</span><input class="mt-1 w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2" value="{{ task.review_state_label }}" readonly></label></div>
|
<div class="grid gap-3 md:grid-cols-2"><label class="block"><span class="text-sm font-semibold text-slate-700">Decision</span><select name="decision" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" {% if task.is_locked or workspace.is_locked or task.review_state == 'not_ready' %}disabled{% endif %}><option value="reviewed">Approve / Reviewed</option><option value="rework_required">Return for Rework</option></select></label><label class="block"><span class="text-sm font-semibold text-slate-700">Review state</span><input class="mt-1 w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2" value="{{ task.review_state_label }}" readonly></label></div>
|
||||||
<label class="block"><span class="text-sm font-semibold text-slate-700">Manager review note</span><textarea name="review_note" rows="5" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" placeholder="Required when returning for rework; recommended for significant conclusions." {% if task.is_locked or workspace.is_locked or task.review_state == 'not_ready' %}disabled{% endif %}>{{ task.manager_review_note }}</textarea></label>
|
<label class="block"><span class="text-sm font-semibold text-slate-700">Manager review note</span><textarea name="review_note" rows="5" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" placeholder="Required when returning for rework; recommended for significant conclusions." {% if task.is_locked or workspace.is_locked or task.review_state == 'not_ready' %}disabled{% endif %}>{{ task.normal_review_note if task.review_kind == 'normal' else task.manager_review_note }}</textarea></label>
|
||||||
<div class="flex flex-wrap gap-2"><button type="submit" name="workflow_action" value="save" class="af-btn af-btn-secondary" {% if task.is_locked or workspace.is_locked or task.review_state == 'not_ready' %}disabled{% endif %}>Save Review</button><button type="submit" name="workflow_action" value="save_next" class="af-btn af-btn-primary" {% if task.is_locked or workspace.is_locked or task.review_state == 'not_ready' %}disabled{% endif %}>Save & Next Review</button><a href="/manager/dashboard?tab=review-queue" class="af-btn af-btn-secondary">Close Workspace</a></div>
|
<div class="flex flex-wrap gap-2"><button type="submit" name="workflow_action" value="save" class="af-btn af-btn-secondary" {% if task.is_locked or workspace.is_locked or task.review_state == 'not_ready' %}disabled{% endif %}>Save Review</button><button type="submit" name="workflow_action" value="save_next" class="af-btn af-btn-primary" {% if task.is_locked or workspace.is_locked or task.review_state == 'not_ready' %}disabled{% endif %}>Save & Next Review</button><a href="/manager/dashboard?tab=review-queue" class="af-btn af-btn-secondary">Close Workspace</a></div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from app.modules.manager_dashboard.service import (
|
|||||||
get_manager_review_workspace,
|
get_manager_review_workspace,
|
||||||
get_next_manager_review_task_id,
|
get_next_manager_review_task_id,
|
||||||
)
|
)
|
||||||
from app.modules.services.execution import apply_task_review
|
from app.modules.services.execution import apply_normal_task_review, apply_task_review
|
||||||
from app.modules.alerts.workflow_escalations import update_workflow_escalation
|
from app.modules.alerts.workflow_escalations import update_workflow_escalation
|
||||||
|
|
||||||
router = APIRouter(prefix="/manager", tags=["manager-dashboard-v2-ui"])
|
router = APIRouter(prefix="/manager", tags=["manager-dashboard-v2-ui"])
|
||||||
@@ -160,6 +160,15 @@ def manager_review_task_submit(
|
|||||||
if not task:
|
if not task:
|
||||||
return RedirectResponse(url="/manager/dashboard?tab=review-queue", status_code=303)
|
return RedirectResponse(url="/manager/dashboard?tab=review-queue", status_code=303)
|
||||||
try:
|
try:
|
||||||
|
if active.get("review_kind") == "normal":
|
||||||
|
apply_normal_task_review(
|
||||||
|
db,
|
||||||
|
task=task,
|
||||||
|
decision=decision,
|
||||||
|
note=review_note,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
apply_task_review(
|
apply_task_review(
|
||||||
db,
|
db,
|
||||||
task=task,
|
task=task,
|
||||||
|
|||||||
@@ -446,16 +446,21 @@ def _partner_task_category(task: ClientServiceTaskInstance) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _partner_review_level_for_task(task: ClientServiceTaskInstance, subscription: ClientServiceSubscription, current_user) -> str | None:
|
def _partner_review_level_for_task(task: ClientServiceTaskInstance, subscription: ClientServiceSubscription, current_user) -> str | None:
|
||||||
"""Return the review level the current partner is authorised to perform.
|
"""Return the review level the current partner is authorised to perform."""
|
||||||
|
|
||||||
Review Partner work is kept separate from Engagement Partner work. System and
|
|
||||||
Firm Admin users may inspect the workspace, but review decisions are selected
|
|
||||||
from the engagement assignments rather than from a new permission model.
|
|
||||||
"""
|
|
||||||
uid = int(getattr(current_user, "id", 0) or 0)
|
uid = int(getattr(current_user, "id", 0) or 0)
|
||||||
is_review_partner = uid and uid == int(getattr(subscription, "review_partner_user_id", 0) or 0)
|
is_review_partner = uid and uid == int(getattr(subscription, "review_partner_user_id", 0) or 0)
|
||||||
is_engagement_partner = uid and uid == int(getattr(subscription, "assigned_partner_user_id", 0) or 0)
|
is_engagement_partner = uid and uid == int(getattr(subscription, "assigned_partner_user_id", 0) or 0)
|
||||||
|
|
||||||
|
normal_role = (getattr(task, "normal_review_role", None) or "").strip().lower()
|
||||||
|
normal_status = (getattr(task, "normal_review_status", None) or "not_required").strip().lower()
|
||||||
|
if (
|
||||||
|
is_engagement_partner
|
||||||
|
and getattr(task, "normal_review_required", False)
|
||||||
|
and normal_role in {"partner", "manager_or_partner"}
|
||||||
|
and normal_status in {"pending", "reviewed", "rework_required"}
|
||||||
|
):
|
||||||
|
return "normal_partner"
|
||||||
|
|
||||||
if is_review_partner and getattr(task, "aqmm_review_partner_required", False):
|
if is_review_partner and getattr(task, "aqmm_review_partner_required", False):
|
||||||
return "review_partner"
|
return "review_partner"
|
||||||
if is_engagement_partner and getattr(task, "aqmm_partner_review_required", False):
|
if is_engagement_partner and getattr(task, "aqmm_partner_review_required", False):
|
||||||
@@ -464,6 +469,14 @@ def _partner_review_level_for_task(task: ClientServiceTaskInstance, subscription
|
|||||||
|
|
||||||
|
|
||||||
def _partner_review_state(task: ClientServiceTaskInstance, review_level: str | None) -> str:
|
def _partner_review_state(task: ClientServiceTaskInstance, review_level: str | None) -> str:
|
||||||
|
if review_level == "normal_partner":
|
||||||
|
status = (getattr(task, "normal_review_status", "not_required") or "not_required").strip().lower()
|
||||||
|
if status == "reviewed":
|
||||||
|
return "reviewed"
|
||||||
|
if status == "rework_required" or getattr(task, "rework_status", "none") == "open":
|
||||||
|
return "rework"
|
||||||
|
return "pending_review" if status == "pending" else "not_ready"
|
||||||
|
|
||||||
if getattr(task, "rework_status", "none") == "open":
|
if getattr(task, "rework_status", "none") == "open":
|
||||||
return "rework"
|
return "rework"
|
||||||
if review_level == "review_partner":
|
if review_level == "review_partner":
|
||||||
@@ -476,10 +489,8 @@ def _partner_review_state(task: ClientServiceTaskInstance, review_level: str | N
|
|||||||
return "reviewed"
|
return "reviewed"
|
||||||
if status == "rework_required":
|
if status == "rework_required":
|
||||||
return "rework"
|
return "rework"
|
||||||
# Partner review must follow any required Manager review.
|
|
||||||
if getattr(task, "aqmm_manager_review_required", False) and getattr(task, "manager_review_status", "not_required") != "reviewed":
|
if getattr(task, "aqmm_manager_review_required", False) and getattr(task, "manager_review_status", "not_required") != "reviewed":
|
||||||
return "not_ready"
|
return "not_ready"
|
||||||
# Review Partner review must follow required Engagement Partner review.
|
|
||||||
if review_level == "review_partner" and getattr(task, "aqmm_partner_review_required", False) and getattr(task, "partner_review_status", "not_required") != "reviewed":
|
if review_level == "review_partner" and getattr(task, "aqmm_partner_review_required", False) and getattr(task, "partner_review_status", "not_required") != "reviewed":
|
||||||
return "not_ready"
|
return "not_ready"
|
||||||
if (getattr(task, "status", "pending") or "pending") not in CLOSED_TASK_STATUSES and getattr(task, "submitted_for_review_at_utc", None) is None:
|
if (getattr(task, "status", "pending") or "pending") not in CLOSED_TASK_STATUSES and getattr(task, "submitted_for_review_at_utc", None) is None:
|
||||||
@@ -523,7 +534,10 @@ def _partner_task_payload(task: ClientServiceTaskInstance, subscription: ClientS
|
|||||||
"blocks_final_release": bool(getattr(task, "aqmm_blocks_final_release", False)),
|
"blocks_final_release": bool(getattr(task, "aqmm_blocks_final_release", False)),
|
||||||
"partner_review_required": bool(getattr(task, "aqmm_partner_review_required", False)),
|
"partner_review_required": bool(getattr(task, "aqmm_partner_review_required", False)),
|
||||||
"review_partner_required": bool(getattr(task, "aqmm_review_partner_required", False)),
|
"review_partner_required": bool(getattr(task, "aqmm_review_partner_required", False)),
|
||||||
|
"normal_review_status": getattr(task, "normal_review_status", "not_required"),
|
||||||
|
"normal_review_note": getattr(task, "normal_review_note", None),
|
||||||
"review_level": review_level,
|
"review_level": review_level,
|
||||||
|
"review_kind_label": "Normal Review" if review_level == "normal_partner" else ("AQMM Review Partner" if review_level == "review_partner" else ("AQMM Partner Review" if review_level == "partner" else "")),
|
||||||
"review_state": review_state,
|
"review_state": review_state,
|
||||||
"can_review": review_level is not None and review_state in {"pending_review", "reviewed", "rework"},
|
"can_review": review_level is not None and review_state in {"pending_review", "reviewed", "rework"},
|
||||||
"is_exception": getattr(task, "checklist_response", None) == "NO" or getattr(task, "rework_status", "none") == "open",
|
"is_exception": getattr(task, "checklist_response", None) == "NO" or getattr(task, "rework_status", "none") == "open",
|
||||||
@@ -562,12 +576,18 @@ def _partner_review_queue_rows(db: Session, request, current_user, *, limit: int
|
|||||||
ClientServiceTaskInstance.is_active.is_(True),
|
ClientServiceTaskInstance.is_active.is_(True),
|
||||||
)
|
)
|
||||||
tasks = list(db.execute(task_stmt.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())).scalars().all())
|
tasks = list(db.execute(task_stmt.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())).scalars().all())
|
||||||
|
pending_normal_partner = sum(
|
||||||
|
1 for t in tasks
|
||||||
|
if getattr(t, "normal_review_required", False)
|
||||||
|
and (getattr(t, "normal_review_role", None) or "").strip().lower() in {"partner", "manager_or_partner"}
|
||||||
|
and (getattr(t, "normal_review_status", None) or "not_required") in {"pending", "rework_required"}
|
||||||
|
)
|
||||||
pending_partner = sum(1 for t in tasks if getattr(t, "aqmm_partner_review_required", False) and getattr(t, "partner_review_status", "not_required") != "reviewed")
|
pending_partner = sum(1 for t in tasks if getattr(t, "aqmm_partner_review_required", False) and getattr(t, "partner_review_status", "not_required") != "reviewed")
|
||||||
pending_review_partner = sum(1 for t in tasks if getattr(t, "aqmm_review_partner_required", False) and getattr(t, "review_partner_review_status", "not_required") != "reviewed")
|
pending_review_partner = sum(1 for t in tasks if getattr(t, "aqmm_review_partner_required", False) and getattr(t, "review_partner_review_status", "not_required") != "reviewed")
|
||||||
rework = sum(1 for t in tasks if getattr(t, "rework_status", "none") == "open")
|
rework = sum(1 for t in tasks if getattr(t, "rework_status", "none") == "open")
|
||||||
blockers = sum(1 for t in tasks if getattr(t, "aqmm_blocks_final_release", False) and getattr(t, "aqmm_status", "not_required") != "completed")
|
blockers = sum(1 for t in tasks if getattr(t, "aqmm_blocks_final_release", False) and getattr(t, "aqmm_status", "not_required") != "completed")
|
||||||
exceptions = sum(1 for t in tasks if getattr(t, "checklist_response", None) == "NO")
|
exceptions = sum(1 for t in tasks if getattr(t, "checklist_response", None) == "NO")
|
||||||
if not any((pending_partner, pending_review_partner, rework, blockers, exceptions)):
|
if not any((pending_normal_partner, pending_partner, pending_review_partner, rework, blockers, exceptions)):
|
||||||
continue
|
continue
|
||||||
weighted = _weighted_progress(tasks)
|
weighted = _weighted_progress(tasks)
|
||||||
sla = _engagement_sla(sub, tasks)
|
sla = _engagement_sla(sub, tasks)
|
||||||
@@ -576,6 +596,7 @@ def _partner_review_queue_rows(db: Session, request, current_user, *, limit: int
|
|||||||
"client_name": getattr(getattr(sub, "client", None), "client_name", None) or "Unlinked Client",
|
"client_name": getattr(getattr(sub, "client", None), "client_name", None) or "Unlinked Client",
|
||||||
"service_name": getattr(getattr(sub, "catalogue", None), "service_name", None) or "Service",
|
"service_name": getattr(getattr(sub, "catalogue", None), "service_name", None) or "Service",
|
||||||
"financial_year": sub.financial_year or "-",
|
"financial_year": sub.financial_year or "-",
|
||||||
|
"pending_normal_partner": pending_normal_partner,
|
||||||
"pending_partner": pending_partner,
|
"pending_partner": pending_partner,
|
||||||
"pending_review_partner": pending_review_partner,
|
"pending_review_partner": pending_review_partner,
|
||||||
"rework_count": rework,
|
"rework_count": rework,
|
||||||
@@ -585,7 +606,7 @@ def _partner_review_queue_rows(db: Session, request, current_user, *, limit: int
|
|||||||
"sla": sla,
|
"sla": sla,
|
||||||
"href": f"/partner/reviews/engagements/{sub.id}",
|
"href": f"/partner/reviews/engagements/{sub.id}",
|
||||||
})
|
})
|
||||||
rows.sort(key=lambda r: (r["release_blockers"], r["rework_count"], r["pending_partner"] + r["pending_review_partner"]), reverse=True)
|
rows.sort(key=lambda r: (r["release_blockers"], r["rework_count"], r["pending_normal_partner"] + r["pending_partner"] + r["pending_review_partner"]), reverse=True)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
<main class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft">
|
<main class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||||
{% if active %}
|
{% if active %}
|
||||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between"><div><div class="text-xs font-semibold uppercase tracking-wide text-brand-700">{{ active.category }}</div><h2 class="mt-1 text-xl font-semibold text-slate-900">{{ active.task_name }}</h2><p class="mt-2 text-sm text-slate-500">{{ active.description or 'No additional procedure description.' }}</p></div><div class="flex flex-wrap gap-2 text-xs"><span class="rounded-full bg-slate-100 px-3 py-1.5">Assigned: {{ active.assigned_to }}</span>{% if active.review_level %}<span class="rounded-full bg-indigo-100 px-3 py-1.5 font-semibold text-indigo-700">{{ active.review_level.replace('_',' ').title() }}</span>{% endif %}</div></div>
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between"><div><div class="text-xs font-semibold uppercase tracking-wide text-brand-700">{{ active.category }}</div><h2 class="mt-1 text-xl font-semibold text-slate-900">{{ active.task_name }}</h2><p class="mt-2 text-sm text-slate-500">{{ active.description or 'No additional procedure description.' }}</p></div><div class="flex flex-wrap gap-2 text-xs"><span class="rounded-full bg-slate-100 px-3 py-1.5">Assigned: {{ active.assigned_to }}</span>{% if active.review_level %}<span class="rounded-full bg-indigo-100 px-3 py-1.5 font-semibold text-indigo-700">{{ active.review_kind_label or active.review_level.replace('_',' ').title() }}</span>{% endif %}</div></div>
|
||||||
|
|
||||||
<div class="mt-6 grid gap-4 md:grid-cols-2">
|
<div class="mt-6 grid gap-4 md:grid-cols-2">
|
||||||
<section class="rounded-2xl border border-slate-200 p-4"><h3 class="font-semibold text-slate-900">Staff Response & Conclusion</h3><dl class="mt-3 space-y-2 text-sm"><div><dt class="text-slate-500">Response</dt><dd class="font-semibold">{{ active.checklist_response or active.checklist_text_response or active.checklist_number_response or active.checklist_date_response or '-' }}</dd></div><div><dt class="text-slate-500">Checklist remarks</dt><dd class="whitespace-pre-wrap">{{ active.checklist_remarks or '-' }}</dd></div><div><dt class="text-slate-500">Work remarks</dt><dd class="whitespace-pre-wrap">{{ active.work_remarks or '-' }}</dd></div></dl></section>
|
<section class="rounded-2xl border border-slate-200 p-4"><h3 class="font-semibold text-slate-900">Staff Response & Conclusion</h3><dl class="mt-3 space-y-2 text-sm"><div><dt class="text-slate-500">Response</dt><dd class="font-semibold">{{ active.checklist_response or active.checklist_text_response or active.checklist_number_response or active.checklist_date_response or '-' }}</dd></div><div><dt class="text-slate-500">Checklist remarks</dt><dd class="whitespace-pre-wrap">{{ active.checklist_remarks or '-' }}</dd></div><div><dt class="text-slate-500">Work remarks</dt><dd class="whitespace-pre-wrap">{{ active.work_remarks or '-' }}</dd></div></dl></section>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<label class="text-sm font-semibold text-slate-800">Partner conclusion</label>
|
<label class="text-sm font-semibold text-slate-800">Partner conclusion</label>
|
||||||
<div class="mt-3 grid gap-3 sm:grid-cols-2"><label class="rounded-2xl border border-slate-200 p-3"><input type="radio" name="decision" value="reviewed" checked> <span class="ml-2 font-semibold">Approve / Reviewed</span></label><label class="rounded-2xl border border-slate-200 p-3"><input type="radio" name="decision" value="rework_required"> <span class="ml-2 font-semibold">Return for Rework</span></label></div>
|
<div class="mt-3 grid gap-3 sm:grid-cols-2"><label class="rounded-2xl border border-slate-200 p-3"><input type="radio" name="decision" value="reviewed" checked> <span class="ml-2 font-semibold">Approve / Reviewed</span></label><label class="rounded-2xl border border-slate-200 p-3"><input type="radio" name="decision" value="rework_required"> <span class="ml-2 font-semibold">Return for Rework</span></label></div>
|
||||||
<textarea name="review_note" rows="4" placeholder="Partner conclusion, clarification or mandatory rework reason" class="mt-3 w-full rounded-2xl border border-slate-300 px-3 py-2 text-sm">{{ active.partner_review_note if active.review_level == 'partner' else active.review_partner_review_note or '' }}</textarea>
|
<textarea name="review_note" rows="4" placeholder="Partner conclusion, clarification or mandatory rework reason" class="mt-3 w-full rounded-2xl border border-slate-300 px-3 py-2 text-sm">{{ active.normal_review_note if active.review_level == 'normal_partner' else (active.partner_review_note if active.review_level == 'partner' else active.review_partner_review_note or '') }}</textarea>
|
||||||
<div class="mt-4 flex flex-wrap gap-2"><button name="workflow_action" value="save" class="rounded-2xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Save Review</button><button name="workflow_action" value="save_next" class="rounded-2xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Save & Next Review</button><a href="/partner/dashboard?tab=review" class="rounded-2xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700">Close Workspace</a></div>
|
<div class="mt-4 flex flex-wrap gap-2"><button name="workflow_action" value="save" class="rounded-2xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Save Review</button><button name="workflow_action" value="save_next" class="rounded-2xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Save & Next Review</button><a href="/partner/dashboard?tab=review" class="rounded-2xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700">Close Workspace</a></div>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from app.modules.partner_dashboard.service import (
|
|||||||
get_next_partner_review_task_id,
|
get_next_partner_review_task_id,
|
||||||
get_partner_review_workspace,
|
get_partner_review_workspace,
|
||||||
)
|
)
|
||||||
from app.modules.services.execution import apply_task_review
|
from app.modules.services.execution import apply_normal_task_review, apply_task_review
|
||||||
from app.modules.alerts.workflow_escalations import update_workflow_escalation
|
from app.modules.alerts.workflow_escalations import update_workflow_escalation
|
||||||
|
|
||||||
router = APIRouter(prefix="/partner", tags=["partner-dashboard-v2-ui"])
|
router = APIRouter(prefix="/partner", tags=["partner-dashboard-v2-ui"])
|
||||||
@@ -137,7 +137,7 @@ def partner_review_task_submit(
|
|||||||
return RedirectResponse(url=f"/partner/reviews/engagements/{subscription_id}?review_error=task", status_code=303)
|
return RedirectResponse(url=f"/partner/reviews/engagements/{subscription_id}?review_error=task", status_code=303)
|
||||||
if active.get("is_locked") or workspace.get("is_locked"):
|
if active.get("is_locked") or workspace.get("is_locked"):
|
||||||
return RedirectResponse(url=f"/partner/reviews/engagements/{subscription_id}?task_id={task_id}&review_error=locked", status_code=303)
|
return RedirectResponse(url=f"/partner/reviews/engagements/{subscription_id}?task_id={task_id}&review_error=locked", status_code=303)
|
||||||
if not active.get("can_review") or active.get("review_level") not in {"partner", "review_partner"}:
|
if not active.get("can_review") or active.get("review_level") not in {"normal_partner", "partner", "review_partner"}:
|
||||||
return RedirectResponse(url=f"/partner/reviews/engagements/{subscription_id}?task_id={task_id}&review_error=not_ready", status_code=303)
|
return RedirectResponse(url=f"/partner/reviews/engagements/{subscription_id}?task_id={task_id}&review_error=not_ready", status_code=303)
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -149,6 +149,15 @@ def partner_review_task_submit(
|
|||||||
if not task:
|
if not task:
|
||||||
return RedirectResponse(url="/partner/dashboard?tab=review", status_code=303)
|
return RedirectResponse(url="/partner/dashboard?tab=review", status_code=303)
|
||||||
try:
|
try:
|
||||||
|
if active.get("review_level") == "normal_partner":
|
||||||
|
apply_normal_task_review(
|
||||||
|
db,
|
||||||
|
task=task,
|
||||||
|
decision=decision,
|
||||||
|
note=review_note,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
apply_task_review(
|
apply_task_review(
|
||||||
db,
|
db,
|
||||||
task=task,
|
task=task,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
<tr class="align-top hover:bg-slate-50/70">
|
<tr class="align-top hover:bg-slate-50/70">
|
||||||
<td class="px-4 py-4 text-sm">
|
<td class="px-4 py-4 text-sm">
|
||||||
<a href="/work/engagements/{{ task.subscription_id }}" class="font-semibold text-slate-900 hover:text-brand-700">{{ task.client_display }}</a>
|
<a href="/partner/reviews/engagements/{{ task.subscription_id }}" class="font-semibold text-slate-900 hover:text-brand-700">{{ task.client_display }}</a>
|
||||||
<div class="mt-1 text-xs text-slate-500">{{ task.engagement_label }}</div>
|
<div class="mt-1 text-xs text-slate-500">{{ task.engagement_label }}</div>
|
||||||
<div class="mt-1 text-[11px] text-slate-400">Engagement #{{ task.subscription_id }}</div>
|
<div class="mt-1 text-[11px] text-slate-400">Engagement #{{ task.subscription_id }}</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
<div class="mt-1 text-xs text-slate-500">{% if task.internal_target_date %}Target {{ task.internal_target_date }}{% else %}No task target{% endif %}</div>
|
<div class="mt-1 text-xs text-slate-500">{% if task.internal_target_date %}Target {{ task.internal_target_date }}{% else %}No task target{% endif %}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-4 text-right">
|
<td class="px-4 py-4 text-right">
|
||||||
<a href="/work/engagements/{{ task.subscription_id }}" class="inline-flex rounded-xl border border-slate-300 px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-50">Open Review</a>
|
<a href="/partner/reviews/engagements/{{ task.subscription_id }}" class="inline-flex rounded-xl border border-slate-300 px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-50">Open Review</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+33
-14
@@ -152,23 +152,40 @@ def _decorate_task(task: ClientServiceTaskInstance, today: date) -> ClientServic
|
|||||||
return task
|
return task
|
||||||
|
|
||||||
|
|
||||||
def _task_bucket(task: ClientServiceTaskInstance) -> str:
|
def _task_bucket(task: ClientServiceTaskInstance, current_user) -> str:
|
||||||
status = (task.status or "pending").strip().lower()
|
status = (task.status or "pending").strip().lower()
|
||||||
if status == "blocked":
|
subscription = getattr(task, "subscription", None)
|
||||||
|
uid = int(getattr(current_user, "id", 0) or 0)
|
||||||
|
is_engagement_partner = uid and uid == int(getattr(subscription, "assigned_partner_user_id", 0) or 0)
|
||||||
|
is_review_partner = uid and uid == int(getattr(subscription, "review_partner_user_id", 0) or 0)
|
||||||
|
|
||||||
|
normal_role = (getattr(task, "normal_review_role", None) or "").strip().lower()
|
||||||
|
normal_status = (getattr(task, "normal_review_status", None) or "not_required").strip().lower()
|
||||||
|
normal_partner_review = (
|
||||||
|
is_engagement_partner
|
||||||
|
and getattr(task, "normal_review_required", False)
|
||||||
|
and normal_role in {"partner", "manager_or_partner"}
|
||||||
|
)
|
||||||
|
aqmm_partner_review = is_engagement_partner and getattr(task, "aqmm_partner_review_required", False)
|
||||||
|
aqmm_review_partner_review = is_review_partner and getattr(task, "aqmm_review_partner_required", False)
|
||||||
|
|
||||||
|
if status == "blocked" and getattr(task, "rework_status", "none") != "open":
|
||||||
return "clarification_required"
|
return "clarification_required"
|
||||||
if status == "completed":
|
if (
|
||||||
|
(normal_partner_review and normal_status == "rework_required")
|
||||||
|
or (aqmm_partner_review and getattr(task, "partner_review_status", "not_required") == "rework_required")
|
||||||
|
or (aqmm_review_partner_review and getattr(task, "review_partner_review_status", "not_required") == "rework_required")
|
||||||
|
or getattr(task, "rework_status", "none") == "open"
|
||||||
|
):
|
||||||
|
return "rework_sent"
|
||||||
|
if (
|
||||||
|
(normal_partner_review and normal_status == "pending")
|
||||||
|
or (aqmm_partner_review and getattr(task, "partner_review_status", "not_required") == "pending")
|
||||||
|
or (aqmm_review_partner_review and getattr(task, "review_partner_review_status", "not_required") == "pending")
|
||||||
|
):
|
||||||
return "pending_review"
|
return "pending_review"
|
||||||
if status in CLOSED_TASK_STATUSES:
|
if status in CLOSED_TASK_STATUSES:
|
||||||
return "completed"
|
return "completed"
|
||||||
if status in {"rework", "rework_required"}:
|
|
||||||
return "rework_sent"
|
|
||||||
if status == "pending":
|
|
||||||
has_partner_rework = any(
|
|
||||||
(getattr(comment, "comment_type", "") or "").strip().lower() == "partner_review_note"
|
|
||||||
and not getattr(comment, "is_deleted", False)
|
|
||||||
for comment in getattr(task, "comments", [])
|
|
||||||
)
|
|
||||||
return "rework_sent" if has_partner_rework else "approved"
|
|
||||||
return "approved"
|
return "approved"
|
||||||
|
|
||||||
|
|
||||||
@@ -186,6 +203,7 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str
|
|||||||
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue),
|
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue),
|
||||||
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_manager),
|
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_manager),
|
||||||
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_staff),
|
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_staff),
|
||||||
|
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.review_partner),
|
||||||
selectinload(ClientServiceTaskInstance.assigned_to),
|
selectinload(ClientServiceTaskInstance.assigned_to),
|
||||||
selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
|
selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
|
||||||
)
|
)
|
||||||
@@ -227,7 +245,8 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str
|
|||||||
summary["clients"].add(task.client_id)
|
summary["clients"].add(task.client_id)
|
||||||
if getattr(task, "subscription_id", None):
|
if getattr(task, "subscription_id", None):
|
||||||
summary["engagements"].add(task.subscription_id)
|
summary["engagements"].add(task.subscription_id)
|
||||||
if status == "completed":
|
bucket = _task_bucket(task, current_user)
|
||||||
|
if bucket == "pending_review":
|
||||||
summary["pending_review"] += 1
|
summary["pending_review"] += 1
|
||||||
if status == "blocked":
|
if status == "blocked":
|
||||||
summary["blocked"] += 1
|
summary["blocked"] += 1
|
||||||
@@ -237,7 +256,7 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str
|
|||||||
summary["due_today"] += 1
|
summary["due_today"] += 1
|
||||||
if status in CLOSED_TASK_STATUSES:
|
if status in CLOSED_TASK_STATUSES:
|
||||||
summary["completed"] += 1
|
summary["completed"] += 1
|
||||||
lookup[_task_bucket(task)]["tasks"].append(task)
|
lookup[bucket]["tasks"].append(task)
|
||||||
|
|
||||||
summary["clients"] = len(summary["clients"])
|
summary["clients"] = len(summary["clients"])
|
||||||
summary["engagements"] = len(summary["engagements"])
|
summary["engagements"] = len(summary["engagements"])
|
||||||
|
|||||||
@@ -31,6 +31,29 @@ TASK_STATUSES = [
|
|||||||
OPEN_TASK_STATUSES = {"pending", "in_progress", "blocked"}
|
OPEN_TASK_STATUSES = {"pending", "in_progress", "blocked"}
|
||||||
CLOSED_TASK_STATUSES = {"completed", "not_applicable", "cancelled"}
|
CLOSED_TASK_STATUSES = {"completed", "not_applicable", "cancelled"}
|
||||||
|
|
||||||
|
NORMAL_REVIEW_ROLES = {"manager", "partner", "manager_or_partner"}
|
||||||
|
|
||||||
|
|
||||||
|
def normal_review_role_for_template(template) -> str | None:
|
||||||
|
if not getattr(template, "requires_review", False):
|
||||||
|
return None
|
||||||
|
explicit = (getattr(template, "normal_review_role", None) or "").strip().lower()
|
||||||
|
if explicit in NORMAL_REVIEW_ROLES:
|
||||||
|
return explicit
|
||||||
|
default_role = (getattr(template, "default_role_name", None) or "").strip().lower()
|
||||||
|
eligible = {
|
||||||
|
part.strip().lower()
|
||||||
|
for part in (getattr(template, "eligible_role_names", None) or "").split(",")
|
||||||
|
if part.strip()
|
||||||
|
}
|
||||||
|
if default_role in {"manager", "branch manager"} and "partner" in eligible:
|
||||||
|
return "partner"
|
||||||
|
if default_role not in {"manager", "branch manager", "partner"} and ({"manager", "branch manager"} & eligible):
|
||||||
|
return "manager"
|
||||||
|
if "partner" in eligible and not ({"manager", "branch manager"} & eligible):
|
||||||
|
return "partner"
|
||||||
|
return "manager"
|
||||||
|
|
||||||
|
|
||||||
TASK_COMMENT_TYPES = [
|
TASK_COMMENT_TYPES = [
|
||||||
("internal_note", "Internal Note"),
|
("internal_note", "Internal Note"),
|
||||||
@@ -307,6 +330,9 @@ def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceS
|
|||||||
response_type=(getattr(template, "response_type", "NONE") or "NONE").upper(),
|
response_type=(getattr(template, "response_type", "NONE") or "NONE").upper(),
|
||||||
evidence_required=getattr(template, "evidence_required", False),
|
evidence_required=getattr(template, "evidence_required", False),
|
||||||
remarks_required_if_no=getattr(template, "remarks_required_if_no", False),
|
remarks_required_if_no=getattr(template, "remarks_required_if_no", False),
|
||||||
|
normal_review_required=bool(getattr(template, "requires_review", False)),
|
||||||
|
normal_review_role=normal_review_role_for_template(template),
|
||||||
|
normal_review_status="not_ready" if getattr(template, "requires_review", False) else "not_required",
|
||||||
is_aqmm_task=getattr(template, "is_aqmm_task", False),
|
is_aqmm_task=getattr(template, "is_aqmm_task", False),
|
||||||
aqmm_mandatory=getattr(template, "aqmm_mandatory", False),
|
aqmm_mandatory=getattr(template, "aqmm_mandatory", False),
|
||||||
aqmm_evidence_required=getattr(template, "aqmm_evidence_required", False),
|
aqmm_evidence_required=getattr(template, "aqmm_evidence_required", False),
|
||||||
@@ -372,6 +398,12 @@ def _copy_template_snapshot_to_task(
|
|||||||
task.response_type = (getattr(template, "response_type", "NONE") or "NONE").upper()
|
task.response_type = (getattr(template, "response_type", "NONE") or "NONE").upper()
|
||||||
task.evidence_required = getattr(template, "evidence_required", False)
|
task.evidence_required = getattr(template, "evidence_required", False)
|
||||||
task.remarks_required_if_no = getattr(template, "remarks_required_if_no", False)
|
task.remarks_required_if_no = getattr(template, "remarks_required_if_no", False)
|
||||||
|
task.normal_review_required = bool(getattr(template, "requires_review", False))
|
||||||
|
task.normal_review_role = normal_review_role_for_template(template)
|
||||||
|
if not task.normal_review_required:
|
||||||
|
task.normal_review_status = "not_required"
|
||||||
|
elif (getattr(task, "normal_review_status", None) or "not_required") == "not_required":
|
||||||
|
task.normal_review_status = "not_ready"
|
||||||
task.is_aqmm_task = getattr(template, "is_aqmm_task", False)
|
task.is_aqmm_task = getattr(template, "is_aqmm_task", False)
|
||||||
task.aqmm_mandatory = getattr(template, "aqmm_mandatory", False)
|
task.aqmm_mandatory = getattr(template, "aqmm_mandatory", False)
|
||||||
task.aqmm_evidence_required = getattr(template, "aqmm_evidence_required", False)
|
task.aqmm_evidence_required = getattr(template, "aqmm_evidence_required", False)
|
||||||
@@ -465,6 +497,9 @@ def sync_open_engagement_tasks_for_service(
|
|||||||
response_type=(getattr(template, "response_type", "NONE") or "NONE").upper(),
|
response_type=(getattr(template, "response_type", "NONE") or "NONE").upper(),
|
||||||
evidence_required=getattr(template, "evidence_required", False),
|
evidence_required=getattr(template, "evidence_required", False),
|
||||||
remarks_required_if_no=getattr(template, "remarks_required_if_no", False),
|
remarks_required_if_no=getattr(template, "remarks_required_if_no", False),
|
||||||
|
normal_review_required=bool(getattr(template, "requires_review", False)),
|
||||||
|
normal_review_role=normal_review_role_for_template(template),
|
||||||
|
normal_review_status="not_ready" if getattr(template, "requires_review", False) else "not_required",
|
||||||
is_aqmm_task=getattr(template, "is_aqmm_task", False),
|
is_aqmm_task=getattr(template, "is_aqmm_task", False),
|
||||||
aqmm_mandatory=getattr(template, "aqmm_mandatory", False),
|
aqmm_mandatory=getattr(template, "aqmm_mandatory", False),
|
||||||
aqmm_evidence_required=getattr(template, "aqmm_evidence_required", False),
|
aqmm_evidence_required=getattr(template, "aqmm_evidence_required", False),
|
||||||
@@ -1323,6 +1358,11 @@ def submit_task_for_review(db: Session, *, task: ClientServiceTaskInstance, note
|
|||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
task.submitted_for_review_by_user_id = user_id
|
task.submitted_for_review_by_user_id = user_id
|
||||||
task.submitted_for_review_at_utc = now
|
task.submitted_for_review_at_utc = now
|
||||||
|
if getattr(task, "normal_review_required", False):
|
||||||
|
task.normal_review_status = "pending"
|
||||||
|
task.normal_review_note = None
|
||||||
|
task.normal_reviewed_by_user_id = None
|
||||||
|
task.normal_reviewed_at_utc = None
|
||||||
if getattr(task, "aqmm_manager_review_required", False) and task.manager_review_status != "reviewed":
|
if getattr(task, "aqmm_manager_review_required", False) and task.manager_review_status != "reviewed":
|
||||||
task.manager_review_status = "pending"
|
task.manager_review_status = "pending"
|
||||||
if getattr(task, "aqmm_partner_review_required", False) and task.partner_review_status != "reviewed":
|
if getattr(task, "aqmm_partner_review_required", False) and task.partner_review_status != "reviewed":
|
||||||
@@ -1339,6 +1379,53 @@ def submit_task_for_review(db: Session, *, task: ClientServiceTaskInstance, note
|
|||||||
recalculate_task_aqmm_status(db, task)
|
recalculate_task_aqmm_status(db, task)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def apply_normal_task_review(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
task: ClientServiceTaskInstance,
|
||||||
|
decision: str,
|
||||||
|
note: str,
|
||||||
|
user_id: int,
|
||||||
|
) -> None:
|
||||||
|
if getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False):
|
||||||
|
return
|
||||||
|
if not getattr(task, "normal_review_required", False):
|
||||||
|
raise ValueError("Normal review is not required for this task.")
|
||||||
|
clean_decision = (decision or "reviewed").strip().lower()
|
||||||
|
if clean_decision not in {"reviewed", "rework_required"}:
|
||||||
|
clean_decision = "reviewed"
|
||||||
|
clean_note = (note or "").strip()
|
||||||
|
if clean_decision == "rework_required" and not clean_note:
|
||||||
|
raise ValueError("Rework reason is required.")
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
task.normal_review_status = clean_decision
|
||||||
|
task.normal_review_note = clean_note or task.normal_review_note
|
||||||
|
task.normal_reviewed_by_user_id = user_id
|
||||||
|
task.normal_reviewed_at_utc = now
|
||||||
|
if clean_decision == "rework_required":
|
||||||
|
task.rework_status = "open"
|
||||||
|
task.rework_reason = clean_note
|
||||||
|
task.rework_requested_by_user_id = user_id
|
||||||
|
task.rework_requested_at_utc = now
|
||||||
|
task.rework_resolved_at_utc = None
|
||||||
|
task.status = "blocked"
|
||||||
|
comment_type = "rework_note"
|
||||||
|
else:
|
||||||
|
if getattr(task, "rework_status", "none") == "open":
|
||||||
|
task.rework_status = "resolved"
|
||||||
|
task.rework_resolved_at_utc = now
|
||||||
|
comment_type = "manager_review_note" if (getattr(task, "normal_review_role", "") or "").startswith("manager") else "partner_review_note"
|
||||||
|
task.updated_by_user_id = user_id
|
||||||
|
_append_system_task_comment(
|
||||||
|
db,
|
||||||
|
task=task,
|
||||||
|
comment_type=comment_type,
|
||||||
|
message=clean_note or ("Normal review approved." if clean_decision == "reviewed" else clean_note),
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def apply_task_review(
|
def apply_task_review(
|
||||||
db: Session,
|
db: Session,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class ServiceDefaultTaskTemplate(CommonBase):
|
|||||||
eligible_role_names: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
eligible_role_names: Mapped[str | None] = mapped_column(String(200), 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)
|
||||||
|
normal_review_role: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
||||||
|
|
||||||
# AQMM task tags. These flags allow the existing service checklist to become
|
# AQMM task tags. These flags allow the existing service checklist to become
|
||||||
# the engagement quality checklist for assurance engagements, without creating
|
# the engagement quality checklist for assurance engagements, without creating
|
||||||
@@ -183,6 +184,7 @@ class FirmServiceTaskTemplate(CommonBase):
|
|||||||
eligible_role_names: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
eligible_role_names: Mapped[str | None] = mapped_column(String(200), 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)
|
||||||
|
normal_review_role: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
||||||
|
|
||||||
# AQMM task tags copied into generated ClientServiceTaskInstance rows.
|
# AQMM task tags copied into generated ClientServiceTaskInstance rows.
|
||||||
is_aqmm_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
|
is_aqmm_task: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
|
||||||
@@ -783,6 +785,14 @@ class ClientServiceTaskInstance(CommonBase):
|
|||||||
submitted_for_review_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
submitted_for_review_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||||
submitted_for_review_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
submitted_for_review_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
# Normal operational review is intentionally separate from AQMM review.
|
||||||
|
normal_review_required: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
|
||||||
|
normal_review_role: Mapped[str | None] = mapped_column(String(30), nullable=True, index=True)
|
||||||
|
normal_review_status: Mapped[str] = mapped_column(String(30), nullable=False, default="not_required", index=True)
|
||||||
|
normal_review_note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
normal_reviewed_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
|
normal_reviewed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
manager_review_status: Mapped[str] = mapped_column(String(30), nullable=False, default="not_required", index=True)
|
manager_review_status: Mapped[str] = mapped_column(String(30), nullable=False, default="not_required", index=True)
|
||||||
manager_review_note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
manager_review_note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
manager_reviewed_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
manager_reviewed_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||||
|
|||||||
@@ -56,6 +56,14 @@
|
|||||||
<input type="checkbox" name="requires_review" {% if task.requires_review %}checked{% endif %}>
|
<input type="checkbox" name="requires_review" {% if task.requires_review %}checked{% endif %}>
|
||||||
Review
|
Review
|
||||||
</label>
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
|
<span>Normal reviewer</span>
|
||||||
|
<select name="normal_review_role" class="rounded-lg border border-slate-300 px-2 py-1 text-xs">
|
||||||
|
<option value="manager" {% if (task.normal_review_role or 'manager') == 'manager' %}selected{% endif %}>Manager</option>
|
||||||
|
<option value="partner" {% if task.normal_review_role == 'partner' %}selected{% endif %}>Partner</option>
|
||||||
|
<option value="manager_or_partner" {% if task.normal_review_role == 'manager_or_partner' %}selected{% endif %}>Manager / Partner</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="inline-flex items-center gap-2">
|
<label class="inline-flex items-center gap-2">
|
||||||
<input type="checkbox" name="is_active" {% if task.is_active %}checked{% endif %}>
|
<input type="checkbox" name="is_active" {% if task.is_active %}checked{% endif %}>
|
||||||
Active
|
Active
|
||||||
|
|||||||
@@ -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' }} · AQMM: {{ 'Yes' if task.is_aqmm_task else 'No' }}
|
Review: {{ ((task.normal_review_role or 'manager').replace('_',' ').title()) 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">
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<div><label class="mb-2 block text-sm font-medium text-slate-700">Task Name</label><input type="text" name="task_name" required 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">Task Name</label><input type="text" name="task_name" required 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">Default Role</label><input type="text" name="default_role_name" 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">Default Role</label><input type="text" name="default_role_name" 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><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">Normal reviewer <select name="normal_review_role" class="rounded border border-slate-300 px-2 py-1 text-xs"><option value="manager">Manager</option><option value="partner">Partner</option><option value="manager_or_partner">Manager / Partner</option></select></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="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="text-sm font-semibold text-indigo-950">AQMM / Quality Control Tagging</div>
|
||||||
|
|||||||
@@ -56,6 +56,14 @@
|
|||||||
<input type="checkbox" name="requires_review" {% if task.requires_review %}checked{% endif %}>
|
<input type="checkbox" name="requires_review" {% if task.requires_review %}checked{% endif %}>
|
||||||
Review
|
Review
|
||||||
</label>
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
|
<span>Normal reviewer</span>
|
||||||
|
<select name="normal_review_role" class="rounded-lg border border-slate-300 px-2 py-1 text-xs">
|
||||||
|
<option value="manager" {% if (task.normal_review_role or 'manager') == 'manager' %}selected{% endif %}>Manager</option>
|
||||||
|
<option value="partner" {% if task.normal_review_role == 'partner' %}selected{% endif %}>Partner</option>
|
||||||
|
<option value="manager_or_partner" {% if task.normal_review_role == 'manager_or_partner' %}selected{% endif %}>Manager / Partner</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="inline-flex items-center gap-2">
|
<label class="inline-flex items-center gap-2">
|
||||||
<input type="checkbox" name="is_active" {% if task.is_active %}checked{% endif %}>
|
<input type="checkbox" name="is_active" {% if task.is_active %}checked{% endif %}>
|
||||||
Active
|
Active
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Options</label>
|
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Options</label>
|
||||||
<div class="flex flex-wrap gap-3 rounded-xl border border-slate-200 px-3 py-2 text-xs text-slate-700">
|
<div class="flex flex-wrap gap-3 rounded-xl border border-slate-200 px-3 py-2 text-xs text-slate-700">
|
||||||
<label class="inline-flex items-center gap-1"><input type="checkbox" name="is_mandatory" checked> Mandatory</label>
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="is_mandatory" checked> Mandatory</label>
|
||||||
<label class="inline-flex items-center gap-1"><input type="checkbox" name="requires_review"> Review</label>
|
<label class="inline-flex items-center gap-1"><input type="checkbox" name="requires_review"> Review</label><label class="inline-flex items-center gap-1">Normal reviewer <select name="normal_review_role" class="rounded border border-slate-300 px-1 py-0.5"><option value="manager">Manager</option><option value="partner">Partner</option><option value="manager_or_partner">Manager / Partner</option></select></label>
|
||||||
<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>
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
<td class="px-4 py-3 text-xs text-slate-600">
|
<td class="px-4 py-3 text-xs text-slate-600">
|
||||||
<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: {{ (task.normal_review_role or "manager").replace("_"," ").title() }}</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.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_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_evidence_required %}<span class="rounded-full bg-sky-100 px-2 py-1 text-sky-800">Evidence</span>{% endif %}
|
||||||
|
|||||||
@@ -138,6 +138,15 @@ def _validated_registration_type_code(db, value: str, *, existing_value: str | N
|
|||||||
raise ValueError("registration_type")
|
raise ValueError("registration_type")
|
||||||
|
|
||||||
|
|
||||||
|
def _normal_review_role_value(value: str, *, requires_review: bool) -> str | None:
|
||||||
|
if not requires_review:
|
||||||
|
return None
|
||||||
|
clean = (value or "").strip().lower()
|
||||||
|
if clean in {"manager", "partner", "manager_or_partner"}:
|
||||||
|
return clean
|
||||||
|
return "manager"
|
||||||
|
|
||||||
|
|
||||||
def _redirect_denied():
|
def _redirect_denied():
|
||||||
from app.core.http_responses import ui_access_denied
|
from app.core.http_responses import ui_access_denied
|
||||||
return ui_access_denied()
|
return ui_access_denied()
|
||||||
@@ -948,6 +957,7 @@ def _copy_system_defaults_if_firm_tasks_empty(
|
|||||||
eligible_role_names=getattr(default, "eligible_role_names", None) or default.default_role_name,
|
eligible_role_names=getattr(default, "eligible_role_names", None) or default.default_role_name,
|
||||||
is_mandatory=default.is_mandatory,
|
is_mandatory=default.is_mandatory,
|
||||||
requires_review=default.requires_review,
|
requires_review=default.requires_review,
|
||||||
|
normal_review_role=getattr(default, "normal_review_role", None),
|
||||||
is_aqmm_task=getattr(default, "is_aqmm_task", False),
|
is_aqmm_task=getattr(default, "is_aqmm_task", False),
|
||||||
aqmm_mandatory=getattr(default, "aqmm_mandatory", False),
|
aqmm_mandatory=getattr(default, "aqmm_mandatory", False),
|
||||||
aqmm_evidence_required=getattr(default, "aqmm_evidence_required", False),
|
aqmm_evidence_required=getattr(default, "aqmm_evidence_required", False),
|
||||||
@@ -1241,7 +1251,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(''), eligible_roles: list[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(...)):
|
def task_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), eligible_roles: list[str] = Form([]), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), normal_review_role: str = Form('manager'), 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)
|
validate_csrf(request, csrf_token)
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
@@ -1267,6 +1277,7 @@ 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,
|
||||||
|
normal_review_role=_normal_review_role_value(normal_review_role, requires_review=requires_review is not None),
|
||||||
task_category=task_category.strip() or None,
|
task_category=task_category.strip() or None,
|
||||||
response_required=response_required is not None,
|
response_required=response_required is not None,
|
||||||
response_type=(response_type.strip().upper() or "NONE"),
|
response_type=(response_type.strip().upper() or "NONE"),
|
||||||
@@ -1351,7 +1362,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), 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(...)):
|
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), normal_review_role: str = Form('manager'), 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)
|
validate_csrf(request, csrf_token)
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
@@ -1364,7 +1375,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, 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)
|
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, normal_review_role=_normal_review_role_value(normal_review_role, 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()
|
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:
|
||||||
@@ -1403,6 +1414,7 @@ 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,
|
||||||
|
normal_review_role=getattr(d, "normal_review_role", None),
|
||||||
task_category=getattr(d, "task_category", None),
|
task_category=getattr(d, "task_category", None),
|
||||||
response_required=getattr(d, "response_required", False),
|
response_required=getattr(d, "response_required", False),
|
||||||
response_type=getattr(d, "response_type", "NONE") or "NONE",
|
response_type=getattr(d, "response_type", "NONE") or "NONE",
|
||||||
@@ -1707,6 +1719,7 @@ 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),
|
||||||
|
normal_review_role: str = Form('manager'),
|
||||||
task_category: str = Form(''),
|
task_category: str = Form(''),
|
||||||
response_required: str | None = Form(None),
|
response_required: str | None = Form(None),
|
||||||
response_type: str = Form('NONE'),
|
response_type: str = Form('NONE'),
|
||||||
@@ -1753,6 +1766,7 @@ 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.normal_review_role = _normal_review_role_value(normal_review_role, requires_review=task.requires_review)
|
||||||
task.task_category = task_category.strip() or None
|
task.task_category = task_category.strip() or None
|
||||||
task.response_required = response_required is not None
|
task.response_required = response_required is not None
|
||||||
task.response_type = response_type.strip().upper() or "NONE"
|
task.response_type = response_type.strip().upper() or "NONE"
|
||||||
@@ -1849,6 +1863,7 @@ 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),
|
||||||
|
normal_review_role: str = Form('manager'),
|
||||||
task_category: str = Form(''),
|
task_category: str = Form(''),
|
||||||
response_required: str | None = Form(None),
|
response_required: str | None = Form(None),
|
||||||
response_type: str = Form('NONE'),
|
response_type: str = Form('NONE'),
|
||||||
@@ -1895,6 +1910,7 @@ 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.normal_review_role = _normal_review_role_value(normal_review_role, requires_review=task.requires_review)
|
||||||
task.task_category = task_category.strip() or None
|
task.task_category = task_category.strip() or None
|
||||||
task.response_required = response_required is not None
|
task.response_required = response_required is not None
|
||||||
task.response_type = response_type.strip().upper() or "NONE"
|
task.response_type = response_type.strip().upper() or "NONE"
|
||||||
|
|||||||
Reference in New Issue
Block a user