Add task role eligibility and safe open engagement sync

This commit is contained in:
A R R R Associates
2026-08-08 15:38:05 +05:30
parent 1a02af213d
commit 01a1c1abc1
7 changed files with 452 additions and 13 deletions
+230 -5
View File
@@ -103,14 +103,70 @@ def parse_date_value(value: str | None) -> date | None:
return date.fromisoformat(text)
def _default_assignee_for_template(subscription: ClientServiceSubscription, template: FirmServiceTaskTemplate) -> int | None:
role = (template.default_role_name or "").strip().lower()
if "partner" in role:
TASK_EXECUTION_ROLES = ("Partner", "Manager", "Staff")
def normalise_task_execution_role(value: str | None) -> str | None:
text = (value or "").strip().lower()
aliases = {
"partner": "Partner",
"engagement partner": "Partner",
"manager": "Manager",
"branch manager": "Manager",
"staff": "Staff",
"employee": "Staff",
}
return aliases.get(text)
def normalise_eligible_task_roles(value: str | None, default_role_name: str | None = None) -> list[str]:
roles: list[str] = []
for piece in (value or "").replace("|", ",").replace(";", ",").split(","):
role = normalise_task_execution_role(piece)
if role and role not in roles:
roles.append(role)
default_role = normalise_task_execution_role(default_role_name)
if default_role and default_role not in roles:
roles.insert(0, default_role)
return roles
def serialise_eligible_task_roles(roles, default_role_name: str | None = None) -> str | None:
values: list[str] = []
for raw in roles or []:
role = normalise_task_execution_role(raw)
if role and role not in values:
values.append(role)
default_role = normalise_task_execution_role(default_role_name)
if default_role and default_role not in values:
values.insert(0, default_role)
return ",".join(values) if values else (default_role or None)
def _assignee_for_role(subscription: ClientServiceSubscription, role: str | None) -> int | None:
role = normalise_task_execution_role(role)
if role == "Partner":
return subscription.assigned_partner_user_id
if "manager" in role:
if role == "Manager":
return subscription.assigned_manager_user_id
if "staff" in role or "employee" in role:
if role == "Staff":
return subscription.assigned_staff_user_id
return None
def _default_assignee_for_template(subscription: ClientServiceSubscription, template: FirmServiceTaskTemplate) -> int | None:
default_role = normalise_task_execution_role(template.default_role_name)
eligible = normalise_eligible_task_roles(getattr(template, "eligible_role_names", None), template.default_role_name)
assignee = _assignee_for_role(subscription, default_role)
if assignee:
return assignee
for role in eligible:
assignee = _assignee_for_role(subscription, role)
if assignee:
return assignee
return subscription.assigned_staff_user_id or subscription.assigned_manager_user_id or subscription.assigned_partner_user_id
@@ -241,6 +297,7 @@ def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceS
description=template.description,
sequence_no=template.sequence_no,
default_role_name=template.default_role_name,
eligible_role_names=serialise_eligible_task_roles((getattr(template, "eligible_role_names", None) or "").split(","), template.default_role_name),
assigned_to_user_id=_default_assignee_for_template(subscription, template),
internal_target_date=_default_internal_target_date(subscription, template),
status="pending",
@@ -295,6 +352,174 @@ def generate_tasks_for_subscription_if_ready(
)
def _copy_template_snapshot_to_task(
task: ClientServiceTaskInstance,
*,
template: FirmServiceTaskTemplate,
subscription: ClientServiceSubscription,
update_assignment: bool,
) -> None:
task.task_name = template.task_name
task.description = template.description
task.sequence_no = template.sequence_no
task.default_role_name = template.default_role_name
task.eligible_role_names = serialise_eligible_task_roles(
(getattr(template, "eligible_role_names", None) or "").split(","),
template.default_role_name,
)
task.task_category = getattr(template, "task_category", None)
task.response_required = getattr(template, "response_required", False)
task.response_type = (getattr(template, "response_type", "NONE") or "NONE").upper()
task.evidence_required = getattr(template, "evidence_required", False)
task.remarks_required_if_no = getattr(template, "remarks_required_if_no", False)
task.is_aqmm_task = getattr(template, "is_aqmm_task", False)
task.aqmm_mandatory = getattr(template, "aqmm_mandatory", False)
task.aqmm_evidence_required = getattr(template, "aqmm_evidence_required", False)
task.aqmm_manager_review_required = getattr(template, "aqmm_manager_review_required", False)
task.aqmm_partner_review_required = getattr(template, "aqmm_partner_review_required", False)
task.aqmm_review_partner_required = getattr(template, "aqmm_review_partner_required", False)
task.aqmm_blocks_final_release = getattr(template, "aqmm_blocks_final_release", False)
task.aqmm_reference = getattr(template, "aqmm_reference", None)
if update_assignment:
task.assigned_to_user_id = _default_assignee_for_template(subscription, template)
def sync_open_engagement_tasks_for_service(
db: Session,
*,
tenant_id: int,
catalogue_id: int,
user_id: int,
include_started_open_tasks: bool = False,
) -> dict[str, int]:
templates = db.execute(
select(FirmServiceTaskTemplate)
.where(
FirmServiceTaskTemplate.tenant_id == tenant_id,
FirmServiceTaskTemplate.service_catalogue_id == catalogue_id,
FirmServiceTaskTemplate.is_active.is_(True),
)
.order_by(FirmServiceTaskTemplate.sequence_no.asc(), FirmServiceTaskTemplate.id.asc())
).scalars().all()
engagements = db.execute(
select(ClientServiceSubscription)
.where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.service_catalogue_id == catalogue_id,
ClientServiceSubscription.is_active.is_(True),
ClientServiceSubscription.status == "active",
ClientServiceSubscription.is_locked.is_(False),
)
.order_by(ClientServiceSubscription.id.asc())
).scalars().all()
result = {
"engagements": len(engagements),
"created": 0,
"updated_pending": 0,
"updated_started": 0,
"preserved_history": 0,
}
for subscription in engagements:
existing = db.execute(
select(ClientServiceTaskInstance).where(
ClientServiceTaskInstance.subscription_id == subscription.id,
ClientServiceTaskInstance.financial_year == subscription.financial_year,
)
).scalars().all()
by_template = {
int(row.firm_task_template_id): row
for row in existing
if row.firm_task_template_id is not None
}
for template in templates:
task = by_template.get(int(template.id))
if task is None:
db.add(
ClientServiceTaskInstance(
tenant_id=subscription.tenant_id,
branch_id=subscription.branch_id,
subscription_id=subscription.id,
client_id=subscription.client_id,
service_catalogue_id=subscription.service_catalogue_id,
firm_task_template_id=template.id,
financial_year=subscription.financial_year,
assessment_year=subscription.assessment_year,
task_name=template.task_name,
description=template.description,
sequence_no=template.sequence_no,
default_role_name=template.default_role_name,
eligible_role_names=serialise_eligible_task_roles(
(getattr(template, "eligible_role_names", None) or "").split(","),
template.default_role_name,
),
assigned_to_user_id=_default_assignee_for_template(subscription, template),
internal_target_date=_default_internal_target_date(subscription, template),
status="pending",
priority="normal",
task_category=getattr(template, "task_category", None),
response_required=getattr(template, "response_required", False),
response_type=(getattr(template, "response_type", "NONE") or "NONE").upper(),
evidence_required=getattr(template, "evidence_required", False),
remarks_required_if_no=getattr(template, "remarks_required_if_no", False),
is_aqmm_task=getattr(template, "is_aqmm_task", False),
aqmm_mandatory=getattr(template, "aqmm_mandatory", False),
aqmm_evidence_required=getattr(template, "aqmm_evidence_required", False),
aqmm_manager_review_required=getattr(template, "aqmm_manager_review_required", False),
aqmm_partner_review_required=getattr(template, "aqmm_partner_review_required", False),
aqmm_review_partner_required=getattr(template, "aqmm_review_partner_required", False),
aqmm_blocks_final_release=getattr(template, "aqmm_blocks_final_release", False),
aqmm_reference=getattr(template, "aqmm_reference", None),
aqmm_status="pending" if getattr(template, "is_aqmm_task", False) else "not_required",
aqmm_review_status="pending_review" if (
getattr(template, "aqmm_manager_review_required", False)
or getattr(template, "aqmm_partner_review_required", False)
or getattr(template, "aqmm_review_partner_required", False)
) else "not_required",
is_active=True,
created_by_user_id=user_id,
updated_by_user_id=user_id,
)
)
result["created"] += 1
continue
status = (task.status or "pending").strip().lower()
never_started = (
status == "pending"
and task.started_at_utc is None
and task.completed_at_utc is None
and task.submitted_for_review_at_utc is None
and (task.rework_status or "none") == "none"
)
if never_started:
_copy_template_snapshot_to_task(
task,
template=template,
subscription=subscription,
update_assignment=True,
)
task.updated_by_user_id = user_id
result["updated_pending"] += 1
elif include_started_open_tasks and status in {"in_progress", "blocked"} and task.completed_at_utc is None:
task.default_role_name = template.default_role_name
task.eligible_role_names = serialise_eligible_task_roles(
(getattr(template, "eligible_role_names", None) or "").split(","),
template.default_role_name,
)
task.assigned_to_user_id = _default_assignee_for_template(subscription, template)
task.updated_by_user_id = user_id
result["updated_started"] += 1
else:
result["preserved_history"] += 1
return result
def _latest_task_documents(db: Session, task_id: int) -> list[EngagementDocument]:
return db.execute(