Add phase 3 consultant task execution workflow

This commit is contained in:
A R R R Associates
2026-07-23 11:37:04 +05:30
parent e9db81b32f
commit 0f7d6f4396
9 changed files with 365 additions and 2 deletions
+115
View File
@@ -136,6 +136,7 @@ def _load_tasks(db: Session, engagement: ClientServiceSubscription) -> list[Clie
select(ClientServiceTaskInstance)
.options(
selectinload(ClientServiceTaskInstance.assigned_to),
selectinload(ClientServiceTaskInstance.assigned_consultant),
selectinload(ClientServiceTaskInstance.catalogue),
selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
)
@@ -277,6 +278,7 @@ def load_unified_engagement_detail(db: Session, *, request, user, engagement_id:
"permanent_documents": permanent_documents,
"timeline": timeline,
"document_requests": document_requests,
"linked_consultants": get_linked_consultants_for_engagement(db, engagement=engagement) if access.can_update_tasks else [],
"access": access,
"role_context": access.role_context,
"task_statuses": TASK_STATUSES,
@@ -357,3 +359,116 @@ def update_document_request(db: Session, *, request_row: ServiceTaskDocumentRequ
request_row.verified_at_utc = now
request_row.verified_by_user_id = user_id
return True
CONSULTANT_ASSIGNMENT_STATUSES = {
"not_applicable", "offered", "accepted", "declined", "in_progress",
"submitted", "rework_required", "approved", "cancelled",
}
def get_linked_consultants_for_engagement(db: Session, *, engagement: ClientServiceSubscription) -> list[dict]:
today = date.today()
rows = db.execute(
select(ClientConsultantLink, ConsultantProfile)
.join(ConsultantProfile, ConsultantProfile.id == ClientConsultantLink.consultant_id)
.where(
ClientConsultantLink.tenant_id == engagement.tenant_id,
ClientConsultantLink.client_id == engagement.client_id,
ClientConsultantLink.is_active.is_(True),
ConsultantProfile.is_active.is_(True),
or_(ClientConsultantLink.service_catalogue_id.is_(None), ClientConsultantLink.service_catalogue_id == engagement.service_catalogue_id),
or_(ClientConsultantLink.effective_from.is_(None), ClientConsultantLink.effective_from <= today),
or_(ClientConsultantLink.effective_to.is_(None), ClientConsultantLink.effective_to >= today),
)
.order_by(ClientConsultantLink.is_primary.desc(), ConsultantProfile.full_name.asc())
).all()
return [{"link": link, "consultant": consultant} for link, consultant in rows]
def assign_task_to_consultant(
db: Session,
*,
task: ClientServiceTaskInstance,
access: WorkAccess,
consultant_id: int | None,
due_date: date | None,
user_id: int,
) -> bool:
if not access.can_update_tasks or task.is_locked:
return False
if not consultant_id:
task.execution_mode = "internal"
task.assigned_consultant_id = None
task.consultant_assignment_status = "not_applicable"
task.consultant_due_date = None
task.consultant_offered_at_utc = None
task.consultant_accepted_at_utc = None
task.consultant_declined_at_utc = None
task.consultant_decline_reason = None
task.consultant_started_at_utc = None
task.consultant_submitted_at_utc = None
task.consultant_submission_note = None
task.consultant_approved_at_utc = None
task.consultant_approved_by_user_id = None
task.updated_by_user_id = user_id
return True
consultant = db.get(ConsultantProfile, int(consultant_id))
if not consultant or not consultant.is_active or consultant.tenant_id != task.tenant_id:
return False
engagement = db.get(ClientServiceSubscription, task.subscription_id)
if not engagement:
return False
link = _consultant_link_for_engagement(db, consultant=consultant, engagement=engagement)
if not link or not link.can_act_for_client:
return False
now = datetime.now(timezone.utc)
task.execution_mode = "consultant"
task.assigned_consultant_id = consultant.id
task.assigned_to_user_id = consultant.user_id
task.consultant_assignment_status = "offered"
task.consultant_due_date = due_date or task.internal_target_date
task.consultant_offered_at_utc = now
task.consultant_accepted_at_utc = None
task.consultant_declined_at_utc = None
task.consultant_decline_reason = None
task.consultant_started_at_utc = None
task.consultant_submitted_at_utc = None
task.consultant_submission_note = None
task.consultant_approved_at_utc = None
task.consultant_approved_by_user_id = None
task.status = "pending"
task.updated_by_user_id = user_id
add_task_comment(db, task=task, comment_type="assignment_update", visibility="consultant", message=f"Assignment offered to consultant. Due date: {task.consultant_due_date or 'Not specified'}.", user_id=user_id)
return True
def review_consultant_submission(db: Session, *, task: ClientServiceTaskInstance, access: WorkAccess, action: str, note: str, user_id: int) -> bool:
if not access.can_update_tasks or task.execution_mode != "consultant" or task.consultant_assignment_status != "submitted" or task.is_locked:
return False
action = (action or "").strip().lower()
note = (note or "").strip()
now = datetime.now(timezone.utc)
if action == "approve":
task.consultant_assignment_status = "approved"
task.consultant_approved_at_utc = now
task.consultant_approved_by_user_id = user_id
task.status = "completed"
task.completed_at_utc = now
task.rework_status = "none"
task.rework_reason = None
message = "Consultant submission approved by the firm."
elif action == "rework":
if not note:
return False
task.consultant_assignment_status = "rework_required"
task.status = "in_progress"
task.rework_status = "requested"
task.rework_reason = note
task.rework_requested_by_user_id = user_id
task.rework_requested_at_utc = now
message = f"Rework requested: {note}"
else:
return False
task.updated_by_user_id = user_id
add_task_comment(db, task=task, comment_type="review_update", visibility="consultant", message=message, user_id=user_id)
return True