Restore review step visibility and dependency locking

This commit is contained in:
A R R R Associates
2026-08-11 13:22:54 +05:30
parent 70bd5da065
commit b464f11625
2 changed files with 111 additions and 13 deletions
+105 -12
View File
@@ -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"])