Restore review step visibility and dependency locking
This commit is contained in:
@@ -3573,17 +3573,77 @@ def _i1_approval_blocker(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _i1_review_execution_blocker(
|
||||||
|
tasks: list[ClientServiceTaskInstance],
|
||||||
|
target_task: ClientServiceTaskInstance,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Lock downstream staff work behind an unfinished Manager/Partner review-step task.
|
||||||
|
|
||||||
|
Some service templates model the review itself as a separate execution task
|
||||||
|
(for example, "Review of working and draft filing") rather than as a review
|
||||||
|
status on the preceding Staff task. Such tasks must remain visible to Staff
|
||||||
|
for workflow context, but Staff cannot execute them and downstream Staff work
|
||||||
|
must wait until the reviewer completes the review-step task.
|
||||||
|
"""
|
||||||
|
ordered = sorted(tasks, key=lambda row: (int(getattr(row, "sequence_no", 0) or 0), int(row.id)))
|
||||||
|
reviewer_roles = {"manager", "partner", "review partner"}
|
||||||
|
|
||||||
|
for row in ordered:
|
||||||
|
if row.id == target_task.id:
|
||||||
|
break
|
||||||
|
|
||||||
|
status = (getattr(row, "status", None) or "pending").strip().lower()
|
||||||
|
if status in CLOSED_TASK_STATUSES:
|
||||||
|
continue
|
||||||
|
|
||||||
|
default_role = (getattr(row, "default_role_name", None) or "").strip().lower()
|
||||||
|
if default_role not in reviewer_roles:
|
||||||
|
continue
|
||||||
|
|
||||||
|
eligible = [
|
||||||
|
item.strip()
|
||||||
|
for item in (getattr(row, "eligible_role_names", None) or "").split(",")
|
||||||
|
if item.strip()
|
||||||
|
]
|
||||||
|
reviewer_label = " / ".join(
|
||||||
|
role for role in ("Manager", "Partner", "Review Partner") if role in eligible
|
||||||
|
)
|
||||||
|
if not reviewer_label:
|
||||||
|
reviewer_label = (getattr(row, "default_role_name", None) or "Reviewer").strip()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"task_id": int(row.id),
|
||||||
|
"task_name": row.task_name,
|
||||||
|
"label": f"Awaiting {reviewer_label} review step",
|
||||||
|
"levels": [reviewer_label],
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _i1_workflow_blocker(
|
||||||
|
tasks: list[ClientServiceTaskInstance],
|
||||||
|
target_task: ClientServiceTaskInstance,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Return the first applicable normal/AQMM approval or reviewer-task gate."""
|
||||||
|
return (
|
||||||
|
_i1_approval_blocker(tasks, target_task)
|
||||||
|
or _i1_review_execution_blocker(tasks, target_task)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _i1_prepare_task_display(
|
def _i1_prepare_task_display(
|
||||||
task: ClientServiceTaskInstance,
|
task: ClientServiceTaskInstance,
|
||||||
*,
|
*,
|
||||||
tasks: list[ClientServiceTaskInstance],
|
tasks: list[ClientServiceTaskInstance],
|
||||||
task_document_counts: dict[int, int],
|
task_document_counts: dict[int, int],
|
||||||
|
read_only: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
status = (getattr(task, "status", None) or "pending").strip().lower()
|
status = (getattr(task, "status", None) or "pending").strip().lower()
|
||||||
task.i1_status = status
|
task.i1_status = status
|
||||||
task.i1_review_levels = _i1_required_review_levels(task)
|
task.i1_review_levels = _i1_required_review_levels(task)
|
||||||
task.i1_review_label = _i1_review_label(task)
|
task.i1_review_label = _i1_review_label(task)
|
||||||
task.i1_approval_blocker = _i1_approval_blocker(tasks, task)
|
task.i1_approval_blocker = _i1_workflow_blocker(tasks, task)
|
||||||
|
task.i1_read_only = bool(read_only)
|
||||||
task.i1_has_evidence = task_document_counts.get(int(task.id), 0) > 0
|
task.i1_has_evidence = task_document_counts.get(int(task.id), 0) > 0
|
||||||
task.i1_documents = []
|
task.i1_documents = []
|
||||||
task.i1_communications = [
|
task.i1_communications = [
|
||||||
@@ -3597,7 +3657,8 @@ def _i1_prepare_task_display(
|
|||||||
)
|
)
|
||||||
task.i1_needs_evidence = bool(getattr(task, "evidence_required", False))
|
task.i1_needs_evidence = bool(getattr(task, "evidence_required", False))
|
||||||
task.i1_quick_complete = bool(
|
task.i1_quick_complete = bool(
|
||||||
not task.i1_approval_blocker
|
not task.i1_read_only
|
||||||
|
and not task.i1_approval_blocker
|
||||||
and not task.i1_needs_response
|
and not task.i1_needs_response
|
||||||
and (not task.i1_needs_evidence or task.i1_has_evidence)
|
and (not task.i1_needs_evidence or task.i1_has_evidence)
|
||||||
and not getattr(task, "is_locked", False)
|
and not getattr(task, "is_locked", False)
|
||||||
@@ -3785,23 +3846,48 @@ def get_employee_engagement_work_board(
|
|||||||
document, checklist, AQMM and lock controls remain the source of truth.
|
document, checklist, AQMM and lock controls remain the source of truth.
|
||||||
"""
|
"""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
stmt = _employee_work_task_query(
|
assigned_stmt = _employee_work_task_query(
|
||||||
db, scope, assigned_only=True, financial_year=financial_year
|
db, scope, assigned_only=True, financial_year=financial_year
|
||||||
).where(ClientServiceTaskInstance.subscription_id == engagement_id)
|
).where(ClientServiceTaskInstance.subscription_id == engagement_id)
|
||||||
tasks = db.execute(
|
assigned_tasks = db.execute(
|
||||||
stmt.order_by(
|
assigned_stmt.order_by(
|
||||||
ClientServiceTaskInstance.sequence_no.asc(),
|
ClientServiceTaskInstance.sequence_no.asc(),
|
||||||
ClientServiceTaskInstance.internal_target_date.is_(None),
|
ClientServiceTaskInstance.internal_target_date.is_(None),
|
||||||
ClientServiceTaskInstance.internal_target_date.asc(),
|
ClientServiceTaskInstance.internal_target_date.asc(),
|
||||||
ClientServiceTaskInstance.id.asc(),
|
ClientServiceTaskInstance.id.asc(),
|
||||||
)
|
)
|
||||||
).scalars().all()
|
).scalars().all()
|
||||||
if not tasks:
|
if not assigned_tasks:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
detail="Engagement work not found or not assigned to you",
|
detail="Engagement work not found or not assigned to you",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Keep the Staff assignment boundary, but include Manager/Partner review-step
|
||||||
|
# tasks from the same engagement as read-only workflow context. This keeps
|
||||||
|
# sequence numbers visible and allows downstream Staff tasks to show the
|
||||||
|
# exact reviewer dependency without giving Staff execution rights.
|
||||||
|
all_stmt = _employee_work_task_query(
|
||||||
|
db, scope, assigned_only=False, financial_year=financial_year
|
||||||
|
).where(ClientServiceTaskInstance.subscription_id == engagement_id)
|
||||||
|
all_tasks = db.execute(
|
||||||
|
all_stmt.order_by(
|
||||||
|
ClientServiceTaskInstance.sequence_no.asc(),
|
||||||
|
ClientServiceTaskInstance.internal_target_date.is_(None),
|
||||||
|
ClientServiceTaskInstance.internal_target_date.asc(),
|
||||||
|
ClientServiceTaskInstance.id.asc(),
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
|
||||||
|
assigned_ids = {int(row.id) for row in assigned_tasks}
|
||||||
|
reviewer_roles = {"manager", "partner", "review partner"}
|
||||||
|
tasks = [
|
||||||
|
row
|
||||||
|
for row in all_tasks
|
||||||
|
if int(row.id) in assigned_ids
|
||||||
|
or (getattr(row, "default_role_name", None) or "").strip().lower() in reviewer_roles
|
||||||
|
]
|
||||||
|
|
||||||
summary = {
|
summary = {
|
||||||
"total": len(tasks),
|
"total": len(tasks),
|
||||||
"open": 0,
|
"open": 0,
|
||||||
@@ -3869,17 +3955,23 @@ def get_employee_engagement_work_board(
|
|||||||
selected_task = next((row for row in tasks if row.id == active_task_id), None)
|
selected_task = next((row for row in tasks if row.id == active_task_id), None)
|
||||||
if selected_task is None:
|
if selected_task is None:
|
||||||
selected_task = next(
|
selected_task = next(
|
||||||
(row for row in tasks if (row.status or "pending").strip().lower() == "in_progress"),
|
(
|
||||||
|
row for row in assigned_tasks
|
||||||
|
if (row.status or "pending").strip().lower() == "in_progress"
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if selected_task is None:
|
if selected_task is None:
|
||||||
selected_task = next(
|
selected_task = next(
|
||||||
(row for row in tasks if (row.status or "pending").strip().lower() not in CLOSED_TASK_STATUSES),
|
(
|
||||||
tasks[0],
|
row for row in assigned_tasks
|
||||||
|
if (row.status or "pending").strip().lower() not in CLOSED_TASK_STATUSES
|
||||||
|
),
|
||||||
|
assigned_tasks[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
selected_category = _employee_task_category(selected_task)
|
selected_category = _employee_task_category(selected_task)
|
||||||
next_task = _employee_workflow_next_task(tasks, selected_task.id)
|
next_task = _employee_workflow_next_task(assigned_tasks, selected_task.id)
|
||||||
weighted_overall = _weighted_progress(tasks)
|
weighted_overall = _weighted_progress(tasks)
|
||||||
overall_progress = weighted_overall["progress_percent"]
|
overall_progress = weighted_overall["progress_percent"]
|
||||||
subscription = getattr(tasks[0], "subscription", None)
|
subscription = getattr(tasks[0], "subscription", None)
|
||||||
@@ -3904,8 +3996,9 @@ def get_employee_engagement_work_board(
|
|||||||
for task in tasks:
|
for task in tasks:
|
||||||
_i1_prepare_task_display(
|
_i1_prepare_task_display(
|
||||||
task,
|
task,
|
||||||
tasks=tasks,
|
tasks=all_tasks,
|
||||||
task_document_counts=task_document_counts,
|
task_document_counts=task_document_counts,
|
||||||
|
read_only=int(task.id) not in assigned_ids,
|
||||||
)
|
)
|
||||||
task.i1_documents = task_documents.get(int(task.id), [])
|
task.i1_documents = task_documents.get(int(task.id), [])
|
||||||
|
|
||||||
@@ -3984,7 +4077,7 @@ def save_employee_workflow_task(
|
|||||||
.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())
|
||||||
).scalars().all()
|
).scalars().all()
|
||||||
approval_blocker = _i1_approval_blocker(engagement_tasks_for_gate, task)
|
approval_blocker = _i1_workflow_blocker(engagement_tasks_for_gate, task)
|
||||||
if approval_blocker and normalised_status in {"in_progress", "completed", "not_applicable"}:
|
if approval_blocker and normalised_status in {"in_progress", "completed", "not_applicable"}:
|
||||||
raise ValueError(approval_blocker["label"])
|
raise ValueError(approval_blocker["label"])
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,9 @@
|
|||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="text-xs font-semibold text-slate-400">#{{ task.sequence_no }}</span>
|
<span class="text-xs font-semibold text-slate-400">#{{ task.sequence_no }}</span>
|
||||||
<h4 class="font-semibold text-slate-900">{{ task.task_name }}</h4>
|
<h4 class="font-semibold text-slate-900">{{ task.task_name }}</h4>
|
||||||
|
{% if task.i1_read_only %}
|
||||||
|
<span class="rounded-full bg-slate-100 px-2 py-1 text-[11px] font-semibold text-slate-600">Read Only</span>
|
||||||
|
{% endif %}
|
||||||
{% if task.i1_review_label %}
|
{% if task.i1_review_label %}
|
||||||
{% if task.i1_review_label == 'Approved' %}
|
{% if task.i1_review_label == 'Approved' %}
|
||||||
<span class="rounded-full bg-emerald-50 px-2 py-1 text-[11px] font-semibold text-emerald-700">◆ Approved</span>
|
<span class="rounded-full bg-emerald-50 px-2 py-1 text-[11px] font-semibold text-emerald-700">◆ Approved</span>
|
||||||
@@ -159,7 +162,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 lg:justify-end">
|
<div class="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||||
{% if not task.i1_approval_blocker and not task.is_locked and not (task.subscription and task.subscription.is_locked) %}
|
{% if task.i1_read_only %}
|
||||||
|
<span class="rounded-xl bg-slate-100 px-3 py-2 text-sm font-semibold text-slate-500">Review step</span>
|
||||||
|
{% elif not task.i1_approval_blocker and not task.is_locked and not (task.subscription and task.subscription.is_locked) %}
|
||||||
{% if status not in ['completed', 'not_applicable'] %}
|
{% if status not in ['completed', 'not_applicable'] %}
|
||||||
{% if task.i1_quick_complete %}
|
{% if task.i1_quick_complete %}
|
||||||
<form method="post" action="/employee/work/engagements/{{ board.engagement_id }}/tasks/{{ task.id }}/quick-action">
|
<form method="post" action="/employee/work/engagements/{{ board.engagement_id }}/tasks/{{ task.id }}/quick-action">
|
||||||
|
|||||||
Reference in New Issue
Block a user