Add employee engagement pause and dependency workflow phase 3
This commit is contained in:
@@ -13,6 +13,7 @@ from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from app.core.security.passwords import hash_password
|
||||
from app.modules.core.iam.models import User
|
||||
from app.modules.alerts.service import create_alert
|
||||
from app.modules.core.iam.scope import build_scope, list_visible_branches, list_visible_tenants, validate_branch_matches_tenant
|
||||
from app.modules.core.rbac.deps import get_user_roles
|
||||
from app.modules.core.rbac.models import Role, UserRole
|
||||
@@ -57,6 +58,20 @@ TASK_COMMUNICATION_VISIBILITIES = [
|
||||
]
|
||||
|
||||
TASK_COMMUNICATION_TYPE_CODES = {code for code, _ in TASK_COMMUNICATION_TYPES}
|
||||
|
||||
WORKFLOW_HOLD_REASONS = {
|
||||
"documents_from_client": "Documents awaited from client",
|
||||
"clarification_from_client": "Clarification awaited from client",
|
||||
"client_requested_hold": "Client requested hold",
|
||||
"manager_review": "Manager review pending",
|
||||
"partner_review": "Partner review pending",
|
||||
"review_partner_review": "Review Partner review pending",
|
||||
"payment_pending": "Payment pending",
|
||||
"portal_issue": "Portal or system issue",
|
||||
"internal_dependency": "Internal dependency",
|
||||
"other": "Other",
|
||||
}
|
||||
|
||||
TASK_COMMUNICATION_VISIBILITY_CODES = {code for code, _ in TASK_COMMUNICATION_VISIBILITIES}
|
||||
|
||||
|
||||
@@ -2701,6 +2716,85 @@ def _phase7i_task_card_enrich(task: ClientServiceTaskInstance, *, today: date) -
|
||||
task.engagement_label = _subscription_label(getattr(task, "subscription", None), task)
|
||||
|
||||
|
||||
def _workflow_manual_blocker(subscription: ClientServiceSubscription | None) -> dict[str, Any] | None:
|
||||
if not subscription or not getattr(subscription, "workflow_pause_reason", None):
|
||||
return None
|
||||
code = str(subscription.workflow_pause_reason).strip()
|
||||
return {
|
||||
"type": "manual",
|
||||
"code": code,
|
||||
"label": WORKFLOW_HOLD_REASONS.get(code, code.replace("_", " ").title()),
|
||||
"notes": (getattr(subscription, "workflow_pause_notes", None) or "").strip() or None,
|
||||
"follow_up_date": getattr(subscription, "workflow_follow_up_date", None),
|
||||
}
|
||||
|
||||
|
||||
def _workflow_automatic_blocker(subscription: ClientServiceSubscription | None, tasks: list[ClientServiceTaskInstance]) -> dict[str, Any] | None:
|
||||
if subscription and getattr(subscription, "quality_workflow_required", False):
|
||||
qstatus = (getattr(subscription, "quality_acceptance_status", None) or "").strip().lower()
|
||||
if qstatus not in {"approved", "not_required"}:
|
||||
return {"type": "automatic", "code": "quality_acceptance", "label": "AQMM acceptance pending", "notes": getattr(subscription, "quality_block_reason", None), "follow_up_date": None}
|
||||
checks = [
|
||||
("rework", "rework_status", {"open"}, "Rework response pending"),
|
||||
("manager_review", "manager_review_status", {"pending", "rework_required"}, "Manager review pending"),
|
||||
("partner_review", "partner_review_status", {"pending", "rework_required"}, "Partner review pending"),
|
||||
("review_partner_review", "review_partner_review_status", {"pending", "rework_required"}, "Review Partner review pending"),
|
||||
]
|
||||
for code, field, values, label in checks:
|
||||
for task in tasks:
|
||||
if (getattr(task, field, None) or "").strip().lower() in values:
|
||||
return {"type": "automatic", "code": code, "label": label, "notes": task.task_name, "follow_up_date": None}
|
||||
return None
|
||||
|
||||
|
||||
def pause_employee_engagement_workflow(db: Session, scope: EmployeeScope, engagement_id: int, *, reason: str, notes: str, follow_up_date: str, actor_user_id: int, financial_year: str | None = None) -> ClientServiceSubscription:
|
||||
code = (reason or "").strip().lower()
|
||||
if code not in WORKFLOW_HOLD_REASONS:
|
||||
raise ValueError("Select a valid hold reason.")
|
||||
clean_notes = (notes or "").strip()
|
||||
if code == "other" and not clean_notes:
|
||||
raise ValueError("Remarks are required for Other hold reason.")
|
||||
tasks = db.execute(_employee_work_task_query(db, scope, assigned_only=True, financial_year=financial_year).where(ClientServiceTaskInstance.subscription_id == engagement_id)).scalars().all()
|
||||
if not tasks:
|
||||
raise HTTPException(status_code=404, detail="Engagement work not found or not assigned to you")
|
||||
subscription = tasks[0].subscription
|
||||
if subscription.is_locked:
|
||||
raise ValueError("Locked engagement cannot be paused.")
|
||||
parsed_follow_up = parse_date(follow_up_date) if (follow_up_date or "").strip() else None
|
||||
subscription.workflow_pause_reason = code
|
||||
subscription.workflow_pause_notes = clean_notes or None
|
||||
subscription.workflow_follow_up_date = parsed_follow_up
|
||||
subscription.workflow_paused_at_utc = datetime.now(timezone.utc)
|
||||
subscription.workflow_paused_by_user_id = actor_user_id
|
||||
subscription.workflow_resumed_at_utc = None
|
||||
subscription.workflow_resumed_by_user_id = None
|
||||
subscription.updated_by_user_id = actor_user_id
|
||||
manager_id = getattr(subscription, "assigned_manager_user_id", None)
|
||||
if manager_id and manager_id != actor_user_id:
|
||||
create_alert(db, user_id=manager_id, title=f"Engagement paused: {_subscription_label(subscription, tasks[0])}", message=f"{WORKFLOW_HOLD_REASONS[code]}" + (f". {clean_notes}" if clean_notes else "") + (f" Follow-up: {parsed_follow_up}." if parsed_follow_up else ""), tenant_id=subscription.tenant_id, branch_id=subscription.branch_id, role_context="manager", alert_type="clarification", priority="high", target_url=f"/employees/work/engagements/{engagement_id}", created_by_user_id=actor_user_id, commit=False)
|
||||
db.commit(); db.refresh(subscription); return subscription
|
||||
|
||||
|
||||
def resume_employee_engagement_workflow(db: Session, scope: EmployeeScope, engagement_id: int, *, actor_user_id: int, financial_year: str | None = None) -> ClientServiceSubscription:
|
||||
tasks = db.execute(_employee_work_task_query(db, scope, assigned_only=True, financial_year=financial_year).where(ClientServiceTaskInstance.subscription_id == engagement_id)).scalars().all()
|
||||
if not tasks:
|
||||
raise HTTPException(status_code=404, detail="Engagement work not found or not assigned to you")
|
||||
subscription = tasks[0].subscription
|
||||
if subscription.is_locked:
|
||||
raise ValueError("Locked engagement cannot be resumed.")
|
||||
old_reason = WORKFLOW_HOLD_REASONS.get(getattr(subscription, "workflow_pause_reason", None), "Paused")
|
||||
subscription.workflow_pause_reason = None
|
||||
subscription.workflow_pause_notes = None
|
||||
subscription.workflow_follow_up_date = None
|
||||
subscription.workflow_resumed_at_utc = datetime.now(timezone.utc)
|
||||
subscription.workflow_resumed_by_user_id = actor_user_id
|
||||
subscription.updated_by_user_id = actor_user_id
|
||||
manager_id = getattr(subscription, "assigned_manager_user_id", None)
|
||||
if manager_id and manager_id != actor_user_id:
|
||||
create_alert(db, user_id=manager_id, title=f"Engagement resumed: {_subscription_label(subscription, tasks[0])}", message=f"The employee resumed work previously held for: {old_reason}.", tenant_id=subscription.tenant_id, branch_id=subscription.branch_id, role_context="manager", alert_type="general", priority="normal", target_url=f"/employees/work/engagements/{engagement_id}", created_by_user_id=actor_user_id, commit=False)
|
||||
db.commit(); db.refresh(subscription); return subscription
|
||||
|
||||
|
||||
def list_employee_work_kanban(
|
||||
db: Session,
|
||||
scope: EmployeeScope,
|
||||
@@ -2864,10 +2958,20 @@ def list_employee_work_kanban(
|
||||
card["progress_percent"] = int(round((completed * 100) / total)) if total else 0
|
||||
card["open_count"] = max(total - completed, 0)
|
||||
|
||||
manual_blocker = _workflow_manual_blocker(card.get("subscription"))
|
||||
automatic_blocker = _workflow_automatic_blocker(card.get("subscription"), card.get("tasks", []))
|
||||
card["manual_blocker"] = manual_blocker
|
||||
card["automatic_blocker"] = automatic_blocker
|
||||
card["workflow_blocker"] = manual_blocker or automatic_blocker
|
||||
if card["workflow_blocker"]:
|
||||
card["blocked_reason"] = card["workflow_blocker"]["label"]
|
||||
card["blocked_notes"] = card["workflow_blocker"].get("notes")
|
||||
card["follow_up_date"] = card["workflow_blocker"].get("follow_up_date")
|
||||
|
||||
if total and completed == total:
|
||||
card_status = "completed"
|
||||
card["action_label"] = "View"
|
||||
elif card["blocked_count"]:
|
||||
elif card["workflow_blocker"] or card["blocked_count"]:
|
||||
card_status = "blocked"
|
||||
card["action_label"] = "Open / Follow Up"
|
||||
elif card["in_progress_count"] or completed:
|
||||
@@ -3122,10 +3226,17 @@ def get_employee_engagement_work_board(
|
||||
selected_category = _employee_task_category(selected_task)
|
||||
next_task = _employee_workflow_next_task(tasks, selected_task.id)
|
||||
overall_progress = round((summary["completed"] / summary["total"]) * 100) if summary["total"] else 0
|
||||
subscription = getattr(tasks[0], "subscription", None)
|
||||
manual_blocker = _workflow_manual_blocker(subscription)
|
||||
automatic_blocker = _workflow_automatic_blocker(subscription, tasks)
|
||||
|
||||
return {
|
||||
"engagement_id": engagement_id,
|
||||
"subscription": getattr(tasks[0], "subscription", None),
|
||||
"subscription": subscription,
|
||||
"manual_blocker": manual_blocker,
|
||||
"automatic_blocker": automatic_blocker,
|
||||
"workflow_blocker": manual_blocker or automatic_blocker,
|
||||
"hold_reasons": WORKFLOW_HOLD_REASONS,
|
||||
"client": getattr(tasks[0], "client", None),
|
||||
"label": _subscription_label(getattr(tasks[0], "subscription", None), tasks[0]),
|
||||
"summary": summary,
|
||||
|
||||
Reference in New Issue
Block a user