Separate normal task review from AQMM review
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user