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]:
|
||||
"""Return configured review levels for I1 using existing task-template controls."""
|
||||
"""Return normal and AQMM review gates without mixing their state."""
|
||||
levels: list[str] = []
|
||||
if getattr(task, "aqmm_manager_review_required", False):
|
||||
levels.append("manager")
|
||||
if getattr(task, "aqmm_partner_review_required", False):
|
||||
levels.append("partner")
|
||||
if getattr(task, "aqmm_review_partner_required", False):
|
||||
levels.append("review_partner")
|
||||
normal_role = (getattr(task, "normal_review_role", None) or "").strip().lower()
|
||||
if getattr(task, "normal_review_required", False):
|
||||
if normal_role == "manager":
|
||||
levels.append("normal_manager")
|
||||
elif normal_role == "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
|
||||
# reviewer flag was configured, route to Manager when available, otherwise
|
||||
# Engagement Partner. No new review table/state is introduced.
|
||||
template = getattr(task, "template", None)
|
||||
if not levels and getattr(template, "requires_review", False):
|
||||
subscription = getattr(task, "subscription", None)
|
||||
if getattr(subscription, "assigned_manager_user_id", None):
|
||||
levels.append("manager")
|
||||
elif getattr(subscription, "assigned_partner_user_id", None):
|
||||
levels.append("partner")
|
||||
if getattr(task, "aqmm_manager_review_required", False):
|
||||
levels.append("aqmm_manager")
|
||||
if getattr(task, "aqmm_partner_review_required", False):
|
||||
levels.append("aqmm_partner")
|
||||
if getattr(task, "aqmm_review_partner_required", False):
|
||||
levels.append("aqmm_review_partner")
|
||||
return levels
|
||||
|
||||
|
||||
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"
|
||||
if level == "partner":
|
||||
if level == "aqmm_partner":
|
||||
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 False
|
||||
|
||||
@@ -3531,9 +3531,12 @@ def _i1_review_label(task: ClientServiceTaskInstance) -> str | None:
|
||||
if not pending:
|
||||
return "Approved"
|
||||
labels = {
|
||||
"manager": "Manager",
|
||||
"partner": "Partner",
|
||||
"review_partner": "Review Partner",
|
||||
"normal_manager": "Manager",
|
||||
"normal_partner": "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"
|
||||
|
||||
@@ -3554,9 +3557,12 @@ def _i1_approval_blocker(
|
||||
if not pending:
|
||||
continue
|
||||
labels = {
|
||||
"manager": "Manager",
|
||||
"partner": "Partner",
|
||||
"review_partner": "Review Partner",
|
||||
"normal_manager": "Manager",
|
||||
"normal_partner": "Partner",
|
||||
"normal_manager_or_partner": "Manager / Partner",
|
||||
"aqmm_manager": "AQMM Manager",
|
||||
"aqmm_partner": "AQMM Partner",
|
||||
"aqmm_review_partner": "AQMM Review Partner",
|
||||
}
|
||||
return {
|
||||
"task_id": int(row.id),
|
||||
@@ -3620,6 +3626,8 @@ def _workflow_task_weight(task: ClientServiceTaskInstance) -> int:
|
||||
weight += 2
|
||||
if bool(getattr(task, "aqmm_blocks_final_release", False)):
|
||||
weight += 2
|
||||
if bool(getattr(task, "normal_review_required", False)):
|
||||
weight += 1
|
||||
if any(bool(getattr(task, name, False)) for name in (
|
||||
"aqmm_manager_review_required",
|
||||
"aqmm_partner_review_required",
|
||||
@@ -3971,7 +3979,7 @@ def save_employee_workflow_task(
|
||||
|
||||
engagement_tasks_for_gate = db.execute(
|
||||
_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)
|
||||
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
|
||||
@@ -4025,7 +4033,7 @@ def save_employee_workflow_task(
|
||||
|
||||
engagement_tasks = db.execute(
|
||||
_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)
|
||||
.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)
|
||||
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)
|
||||
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:
|
||||
status = "completed"
|
||||
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"}
|
||||
|
||||
|
||||
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()
|
||||
task_status = (getattr(task, "status", None) or "pending").strip().lower()
|
||||
rework_status = (getattr(task, "rework_status", None) or "none").strip().lower()
|
||||
if manager_status in _MANAGER_REVIEW_REWORK or rework_status == "open":
|
||||
return "rework"
|
||||
if manager_status in _MANAGER_REVIEW_DONE:
|
||||
return "reviewed"
|
||||
if manager_status in _MANAGER_REVIEW_PENDING or task_status in REVIEW_STATUSES:
|
||||
return "pending_review"
|
||||
if getattr(task, "aqmm_manager_review_required", False) and task_status in CLOSED_TASK_STATUSES:
|
||||
return "pending_review"
|
||||
return "not_ready"
|
||||
|
||||
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":
|
||||
return "rework", "aqmm_manager"
|
||||
if manager_status in _MANAGER_REVIEW_DONE:
|
||||
return "reviewed", "aqmm_manager"
|
||||
if manager_status in _MANAGER_REVIEW_PENDING:
|
||||
return "pending_review", "aqmm_manager"
|
||||
task_status = (getattr(task, "status", None) or "pending").strip().lower()
|
||||
if task_status in CLOSED_TASK_STATUSES:
|
||||
return "pending_review", "aqmm_manager"
|
||||
|
||||
return "not_ready", None
|
||||
|
||||
|
||||
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]:
|
||||
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)]
|
||||
documents = list(getattr(task, "documents", None) or [])
|
||||
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)),
|
||||
"review_state": state,
|
||||
"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_note": getattr(task, "manager_review_note", None) or "",
|
||||
"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 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 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 class="grid gap-4 lg:grid-cols-2">
|
||||
@@ -66,12 +66,12 @@
|
||||
</section>
|
||||
|
||||
<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 %}
|
||||
<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 }}">
|
||||
<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>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.modules.manager_dashboard.service import (
|
||||
get_manager_review_workspace,
|
||||
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
|
||||
|
||||
router = APIRouter(prefix="/manager", tags=["manager-dashboard-v2-ui"])
|
||||
@@ -160,14 +160,23 @@ def manager_review_task_submit(
|
||||
if not task:
|
||||
return RedirectResponse(url="/manager/dashboard?tab=review-queue", status_code=303)
|
||||
try:
|
||||
apply_task_review(
|
||||
db,
|
||||
task=task,
|
||||
review_level="manager",
|
||||
decision=decision,
|
||||
note=review_note,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
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(
|
||||
db,
|
||||
task=task,
|
||||
review_level="manager",
|
||||
decision=decision,
|
||||
note=review_note,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
except ValueError:
|
||||
db.rollback()
|
||||
|
||||
@@ -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:
|
||||
"""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.
|
||||
"""
|
||||
"""Return the review level the current partner is authorised to perform."""
|
||||
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_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):
|
||||
return "review_partner"
|
||||
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:
|
||||
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":
|
||||
return "rework"
|
||||
if review_level == "review_partner":
|
||||
@@ -476,10 +489,8 @@ def _partner_review_state(task: ClientServiceTaskInstance, review_level: str | N
|
||||
return "reviewed"
|
||||
if status == "rework_required":
|
||||
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":
|
||||
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":
|
||||
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:
|
||||
@@ -523,7 +534,10 @@ def _partner_task_payload(task: ClientServiceTaskInstance, subscription: ClientS
|
||||
"blocks_final_release": bool(getattr(task, "aqmm_blocks_final_release", False)),
|
||||
"partner_review_required": bool(getattr(task, "aqmm_partner_review_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_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,
|
||||
"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",
|
||||
@@ -562,12 +576,18 @@ def _partner_review_queue_rows(db: Session, request, current_user, *, limit: int
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
)
|
||||
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_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")
|
||||
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")
|
||||
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
|
||||
weighted = _weighted_progress(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",
|
||||
"service_name": getattr(getattr(sub, "catalogue", None), "service_name", None) or "Service",
|
||||
"financial_year": sub.financial_year or "-",
|
||||
"pending_normal_partner": pending_normal_partner,
|
||||
"pending_partner": pending_partner,
|
||||
"pending_review_partner": pending_review_partner,
|
||||
"rework_count": rework,
|
||||
@@ -585,7 +606,7 @@ def _partner_review_queue_rows(db: Session, request, current_user, *, limit: int
|
||||
"sla": sla,
|
||||
"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
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -63,7 +63,7 @@
|
||||
|
||||
<main class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft">
|
||||
{% 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">
|
||||
<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 }}">
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.modules.partner_dashboard.service import (
|
||||
get_next_partner_review_task_id,
|
||||
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
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
from sqlalchemy import select
|
||||
@@ -149,14 +149,23 @@ def partner_review_task_submit(
|
||||
if not task:
|
||||
return RedirectResponse(url="/partner/dashboard?tab=review", status_code=303)
|
||||
try:
|
||||
apply_task_review(
|
||||
db,
|
||||
task=task,
|
||||
review_level=active["review_level"],
|
||||
decision=decision,
|
||||
note=review_note,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
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(
|
||||
db,
|
||||
task=task,
|
||||
review_level=active["review_level"],
|
||||
decision=decision,
|
||||
note=review_note,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
except ValueError:
|
||||
db.rollback()
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
|
||||
<tr class="align-top hover:bg-slate-50/70">
|
||||
<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-[11px] text-slate-400">Engagement #{{ task.subscription_id }}</div>
|
||||
</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>
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
+33
-14
@@ -152,23 +152,40 @@ def _decorate_task(task: ClientServiceTaskInstance, today: date) -> ClientServic
|
||||
return task
|
||||
|
||||
|
||||
def _task_bucket(task: ClientServiceTaskInstance) -> str:
|
||||
def _task_bucket(task: ClientServiceTaskInstance, current_user) -> str:
|
||||
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"
|
||||
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"
|
||||
if status in CLOSED_TASK_STATUSES:
|
||||
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"
|
||||
|
||||
|
||||
@@ -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.assigned_manager),
|
||||
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_staff),
|
||||
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.review_partner),
|
||||
selectinload(ClientServiceTaskInstance.assigned_to),
|
||||
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)
|
||||
if getattr(task, "subscription_id", None):
|
||||
summary["engagements"].add(task.subscription_id)
|
||||
if status == "completed":
|
||||
bucket = _task_bucket(task, current_user)
|
||||
if bucket == "pending_review":
|
||||
summary["pending_review"] += 1
|
||||
if status == "blocked":
|
||||
summary["blocked"] += 1
|
||||
@@ -237,7 +256,7 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str
|
||||
summary["due_today"] += 1
|
||||
if status in CLOSED_TASK_STATUSES:
|
||||
summary["completed"] += 1
|
||||
lookup[_task_bucket(task)]["tasks"].append(task)
|
||||
lookup[bucket]["tasks"].append(task)
|
||||
|
||||
summary["clients"] = len(summary["clients"])
|
||||
summary["engagements"] = len(summary["engagements"])
|
||||
|
||||
@@ -31,6 +31,29 @@ TASK_STATUSES = [
|
||||
OPEN_TASK_STATUSES = {"pending", "in_progress", "blocked"}
|
||||
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 = [
|
||||
("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(),
|
||||
evidence_required=getattr(template, "evidence_required", 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),
|
||||
aqmm_mandatory=getattr(template, "aqmm_mandatory", 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.evidence_required = getattr(template, "evidence_required", 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.aqmm_mandatory = getattr(template, "aqmm_mandatory", 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(),
|
||||
evidence_required=getattr(template, "evidence_required", 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),
|
||||
aqmm_mandatory=getattr(template, "aqmm_mandatory", 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)
|
||||
task.submitted_for_review_by_user_id = user_id
|
||||
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":
|
||||
task.manager_review_status = "pending"
|
||||
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)
|
||||
|
||||
|
||||
|
||||
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(
|
||||
db: Session,
|
||||
*,
|
||||
|
||||
@@ -114,6 +114,7 @@ class ServiceDefaultTaskTemplate(CommonBase):
|
||||
eligible_role_names: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, 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
|
||||
# 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)
|
||||
is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, 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.
|
||||
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_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_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)
|
||||
|
||||
@@ -56,6 +56,14 @@
|
||||
<input type="checkbox" name="requires_review" {% if task.requires_review %}checked{% endif %}>
|
||||
Review
|
||||
</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">
|
||||
<input type="checkbox" name="is_active" {% if task.is_active %}checked{% endif %}>
|
||||
Active
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="mt-1 text-xs text-slate-500">
|
||||
Role: {{ task.default_role_name or '-' }} ·
|
||||
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 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">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 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 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>
|
||||
|
||||
@@ -56,6 +56,14 @@
|
||||
<input type="checkbox" name="requires_review" {% if task.requires_review %}checked{% endif %}>
|
||||
Review
|
||||
</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">
|
||||
<input type="checkbox" name="is_active" {% if task.is_active %}checked{% endif %}>
|
||||
Active
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@
|
||||
<td class="px-4 py-3 text-xs text-slate-600">
|
||||
<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.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.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 %}
|
||||
|
||||
@@ -138,6 +138,15 @@ def _validated_registration_type_code(db, value: str, *, existing_value: str | N
|
||||
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():
|
||||
from app.core.http_responses import 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,
|
||||
is_mandatory=default.is_mandatory,
|
||||
requires_review=default.requires_review,
|
||||
normal_review_role=getattr(default, "normal_review_role", None),
|
||||
is_aqmm_task=getattr(default, "is_aqmm_task", False),
|
||||
aqmm_mandatory=getattr(default, "aqmm_mandatory", 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')
|
||||
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)
|
||||
db = CommonSessionLocal()
|
||||
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),
|
||||
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"),
|
||||
@@ -1351,7 +1362,7 @@ def default_templates_detail(request: Request, catalogue_id: int):
|
||||
|
||||
|
||||
@router.post('/catalogue/{catalogue_id}/defaults/new')
|
||||
def default_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), 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)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
@@ -1364,7 +1375,7 @@ def default_template_create_submit(request: Request, catalogue_id: int, task_nam
|
||||
catalogue = get_catalogue(db, catalogue_id)
|
||||
if not catalogue:
|
||||
return RedirectResponse(url='/services/defaults', status_code=303)
|
||||
row = ServiceDefaultTaskTemplate(service_catalogue_id=catalogue_id, task_name=task_name.strip(), description=description.strip() or None, default_role_name=default_role_name.strip() or None, sequence_no=sequence_no or next_default_task_sequence(db, catalogue_id=catalogue_id), is_mandatory=is_mandatory is not None, requires_review=requires_review is not None, 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()
|
||||
return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults', status_code=303)
|
||||
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,
|
||||
is_mandatory=d.is_mandatory,
|
||||
requires_review=d.requires_review,
|
||||
normal_review_role=getattr(d, "normal_review_role", None),
|
||||
task_category=getattr(d, "task_category", None),
|
||||
response_required=getattr(d, "response_required", False),
|
||||
response_type=getattr(d, "response_type", "NONE") or "NONE",
|
||||
@@ -1707,6 +1719,7 @@ def firm_task_template_edit_submit(
|
||||
sequence_no: int = Form(1),
|
||||
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'),
|
||||
@@ -1753,6 +1766,7 @@ def firm_task_template_edit_submit(
|
||||
task.sequence_no = sequence_no
|
||||
task.is_mandatory = is_mandatory is not None
|
||||
task.requires_review = requires_review is not None
|
||||
task.normal_review_role = _normal_review_role_value(normal_review_role, requires_review=task.requires_review)
|
||||
task.task_category = task_category.strip() or None
|
||||
task.response_required = response_required is not None
|
||||
task.response_type = response_type.strip().upper() or "NONE"
|
||||
@@ -1849,6 +1863,7 @@ def default_task_template_edit_submit(
|
||||
sequence_no: int = Form(1),
|
||||
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'),
|
||||
@@ -1895,6 +1910,7 @@ def default_task_template_edit_submit(
|
||||
task.sequence_no = sequence_no
|
||||
task.is_mandatory = is_mandatory is not None
|
||||
task.requires_review = requires_review is not None
|
||||
task.normal_review_role = _normal_review_role_value(normal_review_role, requires_review=task.requires_review)
|
||||
task.task_category = task_category.strip() or None
|
||||
task.response_required = response_required is not None
|
||||
task.response_type = response_type.strip().upper() or "NONE"
|
||||
|
||||
Reference in New Issue
Block a user