Restore review step visibility and dependency locking
This commit is contained in:
@@ -3573,17 +3573,77 @@ def _i1_approval_blocker(
|
||||
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(
|
||||
task: ClientServiceTaskInstance,
|
||||
*,
|
||||
tasks: list[ClientServiceTaskInstance],
|
||||
task_document_counts: dict[int, int],
|
||||
read_only: bool = False,
|
||||
) -> None:
|
||||
status = (getattr(task, "status", None) or "pending").strip().lower()
|
||||
task.i1_status = status
|
||||
task.i1_review_levels = _i1_required_review_levels(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_documents = []
|
||||
task.i1_communications = [
|
||||
@@ -3597,7 +3657,8 @@ def _i1_prepare_task_display(
|
||||
)
|
||||
task.i1_needs_evidence = bool(getattr(task, "evidence_required", False))
|
||||
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_evidence or task.i1_has_evidence)
|
||||
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.
|
||||
"""
|
||||
today = date.today()
|
||||
stmt = _employee_work_task_query(
|
||||
assigned_stmt = _employee_work_task_query(
|
||||
db, scope, assigned_only=True, financial_year=financial_year
|
||||
).where(ClientServiceTaskInstance.subscription_id == engagement_id)
|
||||
tasks = db.execute(
|
||||
stmt.order_by(
|
||||
assigned_tasks = db.execute(
|
||||
assigned_stmt.order_by(
|
||||
ClientServiceTaskInstance.sequence_no.asc(),
|
||||
ClientServiceTaskInstance.internal_target_date.is_(None),
|
||||
ClientServiceTaskInstance.internal_target_date.asc(),
|
||||
ClientServiceTaskInstance.id.asc(),
|
||||
)
|
||||
).scalars().all()
|
||||
if not tasks:
|
||||
if not assigned_tasks:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
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 = {
|
||||
"total": len(tasks),
|
||||
"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)
|
||||
if selected_task is None:
|
||||
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,
|
||||
)
|
||||
if selected_task is None:
|
||||
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)
|
||||
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)
|
||||
overall_progress = weighted_overall["progress_percent"]
|
||||
subscription = getattr(tasks[0], "subscription", None)
|
||||
@@ -3904,8 +3996,9 @@ def get_employee_engagement_work_board(
|
||||
for task in tasks:
|
||||
_i1_prepare_task_display(
|
||||
task,
|
||||
tasks=tasks,
|
||||
tasks=all_tasks,
|
||||
task_document_counts=task_document_counts,
|
||||
read_only=int(task.id) not in assigned_ids,
|
||||
)
|
||||
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)
|
||||
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
|
||||
).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"}:
|
||||
raise ValueError(approval_blocker["label"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user