Add task role eligibility and safe open engagement sync
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -111,6 +111,7 @@ class ServiceDefaultTaskTemplate(CommonBase):
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
sequence_no: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
default_role_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
eligible_role_names: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
@@ -179,6 +180,7 @@ class FirmServiceTaskTemplate(CommonBase):
|
||||
sequence_no: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
|
||||
default_role_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
eligible_role_names: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
is_mandatory: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
requires_review: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
@@ -724,6 +726,7 @@ class ClientServiceTaskInstance(CommonBase):
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
sequence_no: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
default_role_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
eligible_role_names: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
|
||||
assigned_to_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||
execution_mode: Mapped[str] = mapped_column(String(30), nullable=False, default="internal", index=True)
|
||||
|
||||
@@ -18,8 +18,28 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-700">Default Role</label>
|
||||
<input type="text" name="default_role_name" value="{{ task.default_role_name or '' }}" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
||||
<label class="mb-2 block text-sm font-medium text-slate-700">Default Assignee Role</label>
|
||||
<select name="default_role_name" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
||||
<option value="">Automatic / existing fallback</option>
|
||||
{% for role in task_execution_roles %}
|
||||
<option value="{{ role }}" {% if (task.default_role_name or '') == role %}selected{% endif %}>{{ role }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-slate-500">This role receives the task first. If unavailable, another eligible role can be used.</p>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 rounded-xl border border-blue-200 bg-blue-50 p-4">
|
||||
<div class="text-sm font-semibold text-blue-950">Eligible Roles</div>
|
||||
<p class="mt-1 text-xs text-blue-800">Select every role allowed to perform this task. For Manager or Partner, select both.</p>
|
||||
{% set eligible_text = task.eligible_role_names or task.default_role_name or '' %}
|
||||
<div class="mt-3 flex flex-wrap gap-5 text-sm text-slate-700">
|
||||
{% for role in task_execution_roles %}
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" name="eligible_roles" value="{{ role }}" {% if role in eligible_text.split(',') or role == task.default_role_name %}checked{% endif %}>
|
||||
{{ role }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -18,8 +18,28 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-700">Default Role</label>
|
||||
<input type="text" name="default_role_name" value="{{ task.default_role_name or '' }}" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
||||
<label class="mb-2 block text-sm font-medium text-slate-700">Default Assignee Role</label>
|
||||
<select name="default_role_name" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
||||
<option value="">Automatic / existing fallback</option>
|
||||
{% for role in task_execution_roles %}
|
||||
<option value="{{ role }}" {% if (task.default_role_name or '') == role %}selected{% endif %}>{{ role }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-slate-500">This role receives the task first. If unavailable, another eligible role can be used.</p>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 rounded-xl border border-blue-200 bg-blue-50 p-4">
|
||||
<div class="text-sm font-semibold text-blue-950">Eligible Roles</div>
|
||||
<p class="mt-1 text-xs text-blue-800">Select every role allowed to perform this task. For Manager or Partner, select both.</p>
|
||||
{% set eligible_text = task.eligible_role_names or task.default_role_name or '' %}
|
||||
<div class="mt-3 flex flex-wrap gap-5 text-sm text-slate-700">
|
||||
{% for role in task_execution_roles %}
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="checkbox" name="eligible_roles" value="{{ role }}" {% if role in eligible_text.split(',') or role == task.default_role_name %}checked{% endif %}>
|
||||
{{ role }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -89,6 +109,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 rounded-xl border border-violet-200 bg-violet-50 p-4">
|
||||
<div class="text-sm font-semibold text-violet-950">Existing Open Engagements</div>
|
||||
<p class="mt-1 text-xs text-violet-800">Optional. Add missing tasks and refresh pending/not-started task role/assignee from the current firm template. Completed/history stays unchanged.</p>
|
||||
<div class="mt-3 space-y-2 text-sm text-slate-700">
|
||||
<label class="flex items-start gap-2"><input type="checkbox" name="sync_open_engagements" class="mt-1"><span><strong>Sync open engagements after Save</strong></span></label>
|
||||
<label class="flex items-start gap-2"><input type="checkbox" name="sync_started_open_tasks" class="mt-1"><span><strong>Also update assignee for In Progress / Blocked tasks</strong> without changing status, evidence, comments or history.</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
||||
To deactivate this task, untick <strong>Active</strong> and save. No hard delete is used.
|
||||
</div>
|
||||
|
||||
@@ -59,8 +59,19 @@
|
||||
<input name="task_name" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="e.g. Verify books with GSTR-2B">
|
||||
</div>
|
||||
<div class="lg:col-span-3">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Default Role</label>
|
||||
<input name="default_role_name" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Staff / Manager / Partner">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Default Assignee Role</label>
|
||||
<select name="default_role_name" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Automatic</option>
|
||||
{% for role in task_execution_roles %}<option value="{{ role }}">{{ role }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="lg:col-span-12 rounded-xl border border-blue-200 bg-blue-50 p-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-blue-800">Eligible Roles</div>
|
||||
<div class="mt-2 flex flex-wrap gap-4 text-xs text-slate-700">
|
||||
{% for role in task_execution_roles %}
|
||||
<label class="inline-flex items-center gap-1"><input type="checkbox" name="eligible_roles" value="{{ role }}"> {{ role }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Sequence</label>
|
||||
@@ -99,6 +110,32 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if request.query_params.get('synced') %}
|
||||
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
|
||||
Open engagement sync completed:
|
||||
{{ request.query_params.get('created', '0') }} missing task(s) added,
|
||||
{{ request.query_params.get('updated_pending', '0') }} pending task(s) refreshed,
|
||||
{{ request.query_params.get('updated_started', '0') }} started task assignee(s) refreshed,
|
||||
{{ request.query_params.get('preserved', '0') }} historical/unsafe task(s) preserved.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if can_manage_tasks %}
|
||||
<section class="rounded-2xl border border-violet-200 bg-violet-50 p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-violet-950">Sync Firm Tasks to Open Engagements</h3>
|
||||
<p class="mt-1 text-xs text-violet-800">Adds missing active tasks and refreshes pending/not-started role and assignee. Completed tasks, evidence, comments, review history and locked engagements are preserved.</p>
|
||||
</div>
|
||||
<form method="post" action="/services/templates/{{ service.id }}/sync-open-engagements" class="flex flex-wrap items-center gap-3" onsubmit="return confirm('Sync current firm task templates to open engagements?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label class="inline-flex items-center gap-2 text-xs text-violet-900"><input type="checkbox" name="include_started_open_tasks"> Also reassign In Progress / Blocked</label>
|
||||
<button type="submit" class="rounded-xl bg-violet-600 px-4 py-2 text-sm font-semibold text-white hover:bg-violet-700">Sync Open Engagements</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||
<div class="border-b border-slate-100 px-5 py-4">
|
||||
<h3 class="text-sm font-semibold text-slate-900">Firm Task List</h3>
|
||||
@@ -124,7 +161,7 @@
|
||||
<div class="font-medium text-slate-900">{{ task.task_name }}</div>
|
||||
{% if task.description %}<div class="mt-1 text-xs text-slate-500">{{ task.description }}</div>{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">{{ task.default_role_name or '-' }}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700"><div>{{ task.default_role_name or '-' }}</div><div class="mt-1 text-xs text-slate-500">Eligible: {{ task.eligible_role_names or task.default_role_name or '-' }}</div></td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% if task.is_mandatory %}<span class="rounded-full bg-slate-100 px-2 py-1">Mandatory</span>{% endif %}
|
||||
|
||||
@@ -36,6 +36,11 @@ from app.modules.services.due_dates import (
|
||||
parse_optional_date,
|
||||
)
|
||||
from app.modules.services.catalogue_merge import build_merge_preview, merge_service_catalogues
|
||||
from app.modules.services.execution import (
|
||||
TASK_EXECUTION_ROLES,
|
||||
serialise_eligible_task_roles,
|
||||
sync_open_engagement_tasks_for_service,
|
||||
)
|
||||
from app.modules.services.task_documents import (
|
||||
create_task_document_requirement,
|
||||
get_task_document_requirement,
|
||||
@@ -79,6 +84,7 @@ def _base_ctx(request: Request, user, db, **ctx):
|
||||
"engagement_type_choices": ENGAGEMENT_TYPE_CHOICES,
|
||||
"due_period_types": DUE_PERIOD_TYPES,
|
||||
"due_year_basis_choices": DUE_YEAR_BASIS_CHOICES,
|
||||
"task_execution_roles": TASK_EXECUTION_ROLES,
|
||||
}
|
||||
base.update(ctx)
|
||||
return base
|
||||
@@ -939,6 +945,7 @@ def _copy_system_defaults_if_firm_tasks_empty(
|
||||
description=default.description,
|
||||
sequence_no=default.sequence_no,
|
||||
default_role_name=default.default_role_name,
|
||||
eligible_role_names=getattr(default, "eligible_role_names", None) or default.default_role_name,
|
||||
is_mandatory=default.is_mandatory,
|
||||
requires_review=default.requires_review,
|
||||
is_aqmm_task=getattr(default, "is_aqmm_task", False),
|
||||
@@ -1234,7 +1241,7 @@ def task_templates_detail(request: Request, catalogue_id: int):
|
||||
|
||||
|
||||
@router.post('/templates/{catalogue_id}/tasks/new')
|
||||
def task_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), evidence_required: str | None = Form(None), remarks_required_if_no: str | None = Form(None), is_aqmm_task: str | None = Form(None), aqmm_mandatory: str | None = Form(None), aqmm_evidence_required: str | None = Form(None), aqmm_manager_review_required: str | None = Form(None), aqmm_partner_review_required: str | None = Form(None), aqmm_review_partner_required: str | None = Form(None), aqmm_blocks_final_release: str | None = Form(None), aqmm_reference: str = Form(''), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
def task_template_create_submit(request: Request, catalogue_id: int, task_name: str = Form(...), description: str = Form(''), default_role_name: str = Form(''), eligible_roles: list[str] = Form([]), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), evidence_required: str | None = Form(None), remarks_required_if_no: str | None = Form(None), is_aqmm_task: str | None = Form(None), aqmm_mandatory: str | None = Form(None), aqmm_evidence_required: str | None = Form(None), aqmm_manager_review_required: str | None = Form(None), aqmm_partner_review_required: str | None = Form(None), aqmm_review_partner_required: str | None = Form(None), aqmm_blocks_final_release: str | None = Form(None), aqmm_reference: str = Form(''), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
@@ -1256,6 +1263,7 @@ def task_template_create_submit(request: Request, catalogue_id: int, task_name:
|
||||
task_name=task_name.strip(),
|
||||
description=description.strip() or None,
|
||||
default_role_name=default_role_name.strip() or None,
|
||||
eligible_role_names=serialise_eligible_task_roles(eligible_roles, default_role_name),
|
||||
sequence_no=sequence_no or next_task_sequence(db, tenant_id=tenant_id, catalogue_id=catalogue_id),
|
||||
is_mandatory=is_mandatory is not None,
|
||||
requires_review=requires_review is not None,
|
||||
@@ -1602,6 +1610,48 @@ def firm_task_template_toggle_active(
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post('/templates/{catalogue_id}/sync-open-engagements')
|
||||
def sync_firm_tasks_to_open_engagements(
|
||||
request: Request,
|
||||
catalogue_id: int,
|
||||
include_started_open_tasks: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url='/login', status_code=303)
|
||||
if not _can_manage_firm_tasks(db, user):
|
||||
return _redirect_denied()
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
selection = get_firm_selection(db, tenant_id=tenant_id, catalogue_id=catalogue_id)
|
||||
if not selection or not selection.is_enabled:
|
||||
return RedirectResponse(url='/services', status_code=303)
|
||||
|
||||
result = sync_open_engagement_tasks_for_service(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
catalogue_id=catalogue_id,
|
||||
user_id=user.id,
|
||||
include_started_open_tasks=include_started_open_tasks is not None,
|
||||
)
|
||||
db.commit()
|
||||
return RedirectResponse(
|
||||
url=(
|
||||
f'/services/templates/{catalogue_id}'
|
||||
f'?synced=1&created={result["created"]}'
|
||||
f'&updated_pending={result["updated_pending"]}'
|
||||
f'&updated_started={result["updated_started"]}'
|
||||
f'&preserved={result["preserved_history"]}'
|
||||
),
|
||||
status_code=303,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get('/templates/{catalogue_id}/tasks/{task_id}/edit')
|
||||
def firm_task_template_edit_page(request: Request, catalogue_id: int, task_id: int):
|
||||
db = CommonSessionLocal()
|
||||
@@ -1653,6 +1703,7 @@ def firm_task_template_edit_submit(
|
||||
task_name: str = Form(...),
|
||||
description: str = Form(''),
|
||||
default_role_name: str = Form(''),
|
||||
eligible_roles: list[str] = Form([]),
|
||||
sequence_no: int = Form(1),
|
||||
is_mandatory: str | None = Form(None),
|
||||
requires_review: str | None = Form(None),
|
||||
@@ -1670,6 +1721,8 @@ def firm_task_template_edit_submit(
|
||||
aqmm_blocks_final_release: str | None = Form(None),
|
||||
aqmm_reference: str = Form(''),
|
||||
is_active: str | None = Form(None),
|
||||
sync_open_engagements: str | None = Form(None),
|
||||
sync_started_open_tasks: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
@@ -1696,6 +1749,7 @@ def firm_task_template_edit_submit(
|
||||
task.task_name = task_name.strip()
|
||||
task.description = description.strip() or None
|
||||
task.default_role_name = default_role_name.strip() or None
|
||||
task.eligible_role_names = serialise_eligible_task_roles(eligible_roles, default_role_name)
|
||||
task.sequence_no = sequence_no
|
||||
task.is_mandatory = is_mandatory is not None
|
||||
task.requires_review = requires_review is not None
|
||||
@@ -1715,7 +1769,28 @@ def firm_task_template_edit_submit(
|
||||
task.is_active = is_active is not None
|
||||
task.updated_by_user_id = user.id
|
||||
|
||||
sync_result = None
|
||||
if sync_open_engagements is not None:
|
||||
sync_result = sync_open_engagement_tasks_for_service(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
catalogue_id=catalogue_id,
|
||||
user_id=user.id,
|
||||
include_started_open_tasks=sync_started_open_tasks is not None,
|
||||
)
|
||||
|
||||
db.commit()
|
||||
if sync_result is not None:
|
||||
return RedirectResponse(
|
||||
url=(
|
||||
f'/services/templates/{catalogue_id}'
|
||||
f'?synced=1&created={sync_result["created"]}'
|
||||
f'&updated_pending={sync_result["updated_pending"]}'
|
||||
f'&updated_started={sync_result["updated_started"]}'
|
||||
f'&preserved={sync_result["preserved_history"]}'
|
||||
),
|
||||
status_code=303,
|
||||
)
|
||||
return RedirectResponse(url=f'/services/templates/{catalogue_id}', status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1770,6 +1845,7 @@ def default_task_template_edit_submit(
|
||||
task_name: str = Form(...),
|
||||
description: str = Form(''),
|
||||
default_role_name: str = Form(''),
|
||||
eligible_roles: list[str] = Form([]),
|
||||
sequence_no: int = Form(1),
|
||||
is_mandatory: str | None = Form(None),
|
||||
requires_review: str | None = Form(None),
|
||||
@@ -1815,6 +1891,7 @@ def default_task_template_edit_submit(
|
||||
task.task_name = task_name.strip()
|
||||
task.description = description.strip() or None
|
||||
task.default_role_name = default_role_name.strip() or None
|
||||
task.eligible_role_names = serialise_eligible_task_roles(eligible_roles, default_role_name)
|
||||
task.sequence_no = sequence_no
|
||||
task.is_mandatory = is_mandatory is not None
|
||||
task.requires_review = requires_review is not None
|
||||
|
||||
Reference in New Issue
Block a user