Fix retirement propagation and task counts

This commit is contained in:
A R R R Associates
2026-09-19 15:11:56 +05:30
parent f565e8309d
commit cc8691d4cd
4 changed files with 107 additions and 22 deletions
+44 -1
View File
@@ -109,6 +109,29 @@ def firm_task_hash(target: FirmServiceTaskTemplate) -> str:
return hashlib.sha256(payload.encode("utf-8")).hexdigest() return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def _legacy_retirement_equivalent(
source: ServiceDefaultTaskTemplate,
target: FirmServiceTaskTemplate,
) -> bool:
"""Return True when a legacy firm row differs only because the system row was retired.
Full-sync retirement moves omitted system rows into a high temporary sequence range
before setting is_active=False. Legacy firm rows created before provenance tracking still
carry their old active flag and old sequence. Those two differences must not make an
otherwise inherited task look like a deliberate firm customization.
Any substantive field difference still protects the firm row as customized.
"""
source_snapshot = _snapshot_dict(source)
target_snapshot = _snapshot_dict(target)
source_snapshot.pop("is_active", None)
target_snapshot.pop("is_active", None)
if int(getattr(source, "sequence_no", 0) or 0) >= 100000:
source_snapshot.pop("sequence_no", None)
target_snapshot.pop("sequence_no", None)
return source_snapshot == target_snapshot
def system_task_diff(source: ServiceDefaultTaskTemplate, target: FirmServiceTaskTemplate) -> list[dict[str, Any]]: def system_task_diff(source: ServiceDefaultTaskTemplate, target: FirmServiceTaskTemplate) -> list[dict[str, Any]]:
labels = { labels = {
"task_name": "Task name", "task_name": "Task name",
@@ -367,11 +390,31 @@ def sync_firm_tasks_from_system_defaults(
# For legacy rows with no inheritance history, infer whether they were # For legacy rows with no inheritance history, infer whether they were
# already firm-customized by comparing the full portable snapshot. # already firm-customized by comparing the full portable snapshot.
if linked_from_legacy and target.last_synced_system_hash is None: if linked_from_legacy and target.last_synced_system_hash is None:
target.is_customized = firm_task_hash(target) != latest_hash if not source.is_active and _legacy_retirement_equivalent(source, target):
# This was an inherited legacy row whose system source has just been
# retired by Full Synchronization. Do not misclassify the active/old
# sequence difference as a firm customization; allow normal retirement
# propagation below.
target.is_customized = False
else:
target.is_customized = firm_task_hash(target) != latest_hash
if not target.is_customized: if not target.is_customized:
target.last_synced_system_hash = latest_hash target.last_synced_system_hash = latest_hash
target.last_reviewed_system_hash = latest_hash target.last_reviewed_system_hash = latest_hash
# Repair rows linked by an earlier rollout where retirement-only differences
# were incorrectly interpreted as customization. This is deliberately limited
# to rows that have never had a successful inherited snapshot recorded.
if (
not source.is_active
and target.is_customized
and target.last_synced_system_hash is None
and _legacy_retirement_equivalent(source, target)
):
target.is_customized = False
target.system_update_available = False
target.system_update_detected_at_utc = None
# Detect out-of-band edits to a linked inherited row as customization. # Detect out-of-band edits to a linked inherited row as customization.
if ( if (
not target.is_customized not target.is_customized
@@ -11,14 +11,21 @@
{% if request.query_params.get('imported') %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Excel import completed: {{ request.query_params.get('created','0') }} created, {{ request.query_params.get('updated','0') }} updated, {{ request.query_params.get('retired','0') }} retired.{% if request.query_params.get('full_sync') == '1' %} Full Synchronization was applied.{% endif %}</div>{% endif %} {% if request.query_params.get('imported') %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Excel import completed: {{ request.query_params.get('created','0') }} created, {{ request.query_params.get('updated','0') }} updated, {{ request.query_params.get('retired','0') }} retired.{% if request.query_params.get('full_sync') == '1' %} Full Synchronization was applied.{% endif %}</div>{% endif %}
{% if request.query_params.get('import_error') %}<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Excel import failed validation. No partial update was committed. Review the workbook and try again.</div>{% endif %} {% if request.query_params.get('import_error') %}<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Excel import failed validation. No partial update was committed. Review the workbook and try again.</div>{% endif %}
<section class="grid gap-4 md:grid-cols-2">
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Active System Defaults</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ active_default_task_count }}</div><p class="mt-1 text-sm text-slate-500">These tasks are available for synchronization to firms and new engagement work.</p></div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Retired System Defaults</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ retired_default_task_count }}</div><p class="mt-1 text-sm text-slate-500">Soft-retired tasks remain in the database so historical engagement and audit records are preserved.</p></div>
</section>
<section class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4"> <section class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"><div><h3 class="text-sm font-semibold text-emerald-950">Export · Edit · Re-import This Service</h3><p class="mt-1 text-xs text-emerald-800">Export this service's current tasks, edit the Import Ready sheet, then re-import. Existing exports now carry a stable system_task_id so renaming and resequencing update the correct task.</p></div> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"><div><h3 class="text-sm font-semibold text-emerald-950">Export · Edit · Re-import This Service</h3><p class="mt-1 text-xs text-emerald-800">Export this service's current tasks, edit the Import Ready sheet, then re-import. Existing exports now carry a stable system_task_id so renaming and resequencing update the correct task.</p></div>
<form method="post" action="/services/catalogue/{{ service.id }}/defaults/import" enctype="multipart/form-data" class="flex flex-wrap items-center gap-2" onsubmit="if (this.full_sync.checked) return confirm('Full Synchronization will retire existing system-default tasks for this service that are missing from the workbook. Historical work will be preserved. Continue?');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="file" name="file" accept=".xlsx" required class="max-w-xs rounded-xl border border-emerald-200 bg-white px-3 py-2 text-xs"><label class="inline-flex items-center gap-2 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-medium text-amber-900"><input type="checkbox" name="full_sync"> Full Synchronization (retire missing tasks)</label><button class="rounded-xl bg-emerald-700 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-800">Import Updated Excel</button></form></div> <form method="post" action="/services/catalogue/{{ service.id }}/defaults/import" enctype="multipart/form-data" class="flex flex-wrap items-center gap-2" onsubmit="if (this.full_sync.checked) return confirm('Full Synchronization will retire existing system-default tasks for this service that are missing from the workbook. Historical work will be preserved. Continue?');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="file" name="file" accept=".xlsx" required class="max-w-xs rounded-xl border border-emerald-200 bg-white px-3 py-2 text-xs"><label class="inline-flex items-center gap-2 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-medium text-amber-900"><input type="checkbox" name="full_sync"> Full Synchronization (retire missing tasks)</label><button class="rounded-xl bg-emerald-700 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-800">Import Updated Excel</button></form></div>
</section> </section>
<section class="rounded-2xl bg-white p-5 shadow-soft"> <section class="rounded-2xl bg-white p-5 shadow-soft">
<div class="mb-4 flex items-center justify-between gap-3">
<div><h3 class="text-sm font-semibold text-slate-900">System Default Task List</h3><p class="text-xs text-slate-500">Active tasks are shown by default. Retired tasks remain available for audit/history review.</p></div>
<a href="{% if show_retired %}/services/catalogue/{{ service.id }}/defaults{% else %}/services/catalogue/{{ service.id }}/defaults?show_retired=1{% endif %}" class="rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">{% if show_retired %}Hide Retired{% else %}Show Retired ({{ retired_default_task_count }}){% endif %}</a>
</div>
<div class="space-y-3"> <div class="space-y-3">
{% for task in default_tasks %} {% for task in display_default_tasks %}
{% if task.is_active or is_system_admin %}
<div class="rounded-xl border border-slate-200 p-4"> <div class="rounded-xl border border-slate-200 p-4">
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div> <div>
@@ -40,8 +47,7 @@
<p class="mt-2 text-sm text-slate-700">{{ task.description }}</p> <p class="mt-2 text-sm text-slate-700">{{ task.description }}</p>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% else %}<div class="rounded-xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No active default tasks configured. Use Show Retired to view inactive historical defaults.</div>{% endfor %}
{% else %}<div class="rounded-xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No default tasks configured yet.</div>{% endfor %}
</div> </div>
</section> </section>
<section class="rounded-2xl bg-white p-5 shadow-soft"> <section class="rounded-2xl bg-white p-5 shadow-soft">
@@ -43,13 +43,13 @@
</div> </div>
<div class="rounded-2xl bg-white p-5 shadow-soft"> <div class="rounded-2xl bg-white p-5 shadow-soft">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Firm Tasks</div> <div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Firm Tasks</div>
<div class="mt-2 text-lg font-semibold text-slate-900">{{ task_templates|length }}</div> <div class="mt-2 text-lg font-semibold text-slate-900">{{ active_task_count }}</div>
<p class="mt-1 text-sm text-slate-500">Tasks customised for the active firm.</p> <p class="mt-1 text-sm text-slate-500">Active tasks for the firm. {{ retired_task_count }} retired task(s) are preserved for history.</p>
</div> </div>
<div class="rounded-2xl bg-white p-5 shadow-soft"> <div class="rounded-2xl bg-white p-5 shadow-soft">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">System Defaults</div> <div class="text-xs font-semibold uppercase tracking-wide text-slate-500">System Defaults</div>
<div class="mt-2 text-lg font-semibold text-slate-900">{{ default_tasks|length }}</div> <div class="mt-2 text-lg font-semibold text-slate-900">{{ active_default_task_count }}</div>
<p class="mt-1 text-sm text-slate-500">Inherited defaults stay synchronized automatically; customized tasks require Firm Admin approval before a later system update is applied.</p> <p class="mt-1 text-sm text-slate-500">Active inherited system defaults. {{ retired_default_task_count }} retired default(s) are preserved for history; customized tasks require Firm Admin approval before a later system update is applied.</p>
</div> </div>
</section> </section>
@@ -60,7 +60,7 @@
<h3 class="text-sm font-semibold text-slate-900">Add Firm Task</h3> <h3 class="text-sm font-semibold text-slate-900">Add Firm Task</h3>
<p class="text-sm text-slate-500">Create a task template for this firm and service.</p> <p class="text-sm text-slate-500">Create a task template for this firm and service.</p>
</div> </div>
{% if default_tasks|length > 0 %} {% if active_default_task_count > 0 %}
<form method="post" action="/services/catalogue/{{ service.id }}/firm-tasks/sync-system-defaults"> <form method="post" action="/services/catalogue/{{ service.id }}/firm-tasks/sync-system-defaults">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="rounded-xl border border-brand-300 px-4 py-2 text-sm font-medium text-brand-700 hover:bg-brand-50" type="submit">Sync System Defaults</button> <button class="rounded-xl border border-brand-300 px-4 py-2 text-sm font-medium text-brand-700 hover:bg-brand-50" type="submit">Sync System Defaults</button>
@@ -164,9 +164,12 @@
{% endif %} {% endif %}
<section class="overflow-hidden rounded-2xl bg-white shadow-soft"> <section class="overflow-hidden rounded-2xl bg-white shadow-soft">
<div class="border-b border-slate-100 px-5 py-4"> <div class="flex items-center justify-between gap-3 border-b border-slate-100 px-5 py-4">
<h3 class="text-sm font-semibold text-slate-900">Firm Task List</h3> <div>
<p class="text-sm text-slate-500">These tasks will be used when work is generated for this firm.</p> <h3 class="text-sm font-semibold text-slate-900">Firm Task List</h3>
<p class="text-sm text-slate-500">Active tasks are used for new work. Retired tasks remain stored for historical references.</p>
</div>
<a href="{% if show_retired %}/services/templates/{{ service.id }}{% else %}/services/templates/{{ service.id }}?show_retired=1{% endif %}" class="rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">{% if show_retired %}Hide Retired{% else %}Show Retired ({{ retired_task_count }}){% endif %}</a>
</div> </div>
<table class="min-w-full divide-y divide-slate-200"> <table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"> <thead class="bg-slate-50">
@@ -181,7 +184,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-100"> <tbody class="divide-y divide-slate-100">
{% for task in task_templates %} {% for task in display_task_templates %}
<tr> <tr>
<td class="px-4 py-3 text-sm font-medium text-slate-900 align-top"> <td class="px-4 py-3 text-sm font-medium text-slate-900 align-top">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -219,7 +222,8 @@
{% if task.system_update_available %} {% if task.system_update_available %}
{% set changes = system_update_diff_map.get(task.id, []) if system_update_diff_map else [] %} {% set changes = system_update_diff_map.get(task.id, []) if system_update_diff_map else [] %}
<div class="mt-2 rounded-xl border border-violet-200 bg-violet-50 p-3 text-xs text-violet-950"> <div class="mt-2 rounded-xl border border-violet-200 bg-violet-50 p-3 text-xs text-violet-950">
<div class="font-semibold">A newer system default is available. Your firm customization has not been overwritten.</div> {% set retirement_update = system_update_retirement_map.get(task.id, false) if system_update_retirement_map else false %}
<div class="font-semibold">{% if retirement_update %}This system default has been retired. Your customized firm task has not been disabled automatically.{% else %}A newer system default is available. Your firm customization has not been overwritten.{% endif %}</div>
{% if changes %} {% if changes %}
<div class="mt-2 space-y-1"> <div class="mt-2 space-y-1">
{% for change in changes[:5] %} {% for change in changes[:5] %}
@@ -230,13 +234,13 @@
{% endif %} {% endif %}
{% if can_manage_tasks %} {% if can_manage_tasks %}
<div class="mt-3 flex flex-wrap gap-2"> <div class="mt-3 flex flex-wrap gap-2">
<form method="post" action="/services/templates/{{ service.id }}/tasks/{{ task.id }}/system-update/upgrade" onsubmit="return confirm('Upgrade this customized firm task to the latest system default? Safe unstarted tasks in open engagements will also be refreshed.');"> <form method="post" action="/services/templates/{{ service.id }}/tasks/{{ task.id }}/system-update/upgrade" onsubmit="return confirm('{% if retirement_update %}Retire this firm task in line with the system default? Safe pending/unstarted tasks in open engagements will also be deactivated; historical work is preserved.{% else %}Upgrade this customized firm task to the latest system default? Safe unstarted tasks in open engagements will also be refreshed.{% endif %}');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="rounded-lg bg-violet-700 px-3 py-1.5 text-[11px] font-semibold text-white hover:bg-violet-800">Upgrade to System</button> <button class="rounded-lg bg-violet-700 px-3 py-1.5 text-[11px] font-semibold text-white hover:bg-violet-800">{% if retirement_update %}Retire with System{% else %}Upgrade to System{% endif %}</button>
</form> </form>
<form method="post" action="/services/templates/{{ service.id }}/tasks/{{ task.id }}/system-update/keep-firm"> <form method="post" action="/services/templates/{{ service.id }}/tasks/{{ task.id }}/system-update/keep-firm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="rounded-lg border border-violet-300 bg-white px-3 py-1.5 text-[11px] font-semibold text-violet-800 hover:bg-violet-100">Keep Firm Version</button> <button class="rounded-lg border border-violet-300 bg-white px-3 py-1.5 text-[11px] font-semibold text-violet-800 hover:bg-violet-100">{% if retirement_update %}Keep Firm Task{% else %}Keep Firm Version{% endif %}</button>
</form> </form>
</div> </div>
{% endif %} {% endif %}
@@ -326,7 +330,7 @@
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="7" class="px-4 py-8 text-center text-sm text-slate-500">No firm task templates yet. Copy defaults or add tasks manually.</td></tr> <tr><td colspan="7" class="px-4 py-8 text-center text-sm text-slate-500">No active firm task templates yet. Use Show Retired to view historical inactive tasks, or sync/add tasks as needed.</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
+35 -3
View File
@@ -1194,7 +1194,7 @@ def task_templates_list(request: Request):
tenant_id = _active_tenant_id(request, user) tenant_id = _active_tenant_id(request, user)
enabled_rows = list_firm_services_payload(db, tenant_id=tenant_id) enabled_rows = list_firm_services_payload(db, tenant_id=tenant_id)
for row in enabled_rows: for row in enabled_rows:
row['task_count'] = len(get_firm_task_templates(db, tenant_id=tenant_id, catalogue_id=row['catalogue'].id)) row['task_count'] = sum(1 for task in get_firm_task_templates(db, tenant_id=tenant_id, catalogue_id=row['catalogue'].id) if task.is_active)
return _render(request, 'modules/services/templates/services/task_templates_list.html', db, user, title='Firm Service Task Templates', rows=enabled_rows, can_manage_tasks=_can_manage_firm_tasks(db, user)) return _render(request, 'modules/services/templates/services/task_templates_list.html', db, user, title='Firm Service Task Templates', rows=enabled_rows, can_manage_tasks=_can_manage_firm_tasks(db, user))
finally: finally:
db.close() db.close()
@@ -1218,11 +1218,13 @@ def task_templates_detail(request: Request, catalogue_id: int):
return RedirectResponse(url='/services', status_code=303) return RedirectResponse(url='/services', status_code=303)
tasks = get_firm_task_templates(db, tenant_id=tenant_id, catalogue_id=catalogue_id) tasks = get_firm_task_templates(db, tenant_id=tenant_id, catalogue_id=catalogue_id)
system_update_diff_map = {} system_update_diff_map = {}
system_update_retirement_map = {}
for firm_task in tasks: for firm_task in tasks:
if getattr(firm_task, "system_update_available", False) and getattr(firm_task, "source_system_task_id", None): if getattr(firm_task, "system_update_available", False) and getattr(firm_task, "source_system_task_id", None):
system_task = db.get(ServiceDefaultTaskTemplate, int(firm_task.source_system_task_id)) system_task = db.get(ServiceDefaultTaskTemplate, int(firm_task.source_system_task_id))
if system_task is not None: if system_task is not None:
system_update_diff_map[firm_task.id] = system_task_diff(system_task, firm_task) system_update_diff_map[firm_task.id] = system_task_diff(system_task, firm_task)
system_update_retirement_map[firm_task.id] = not bool(system_task.is_active)
task_requirement_map = { task_requirement_map = {
task.id: list_task_document_requirements(db, tenant_id=tenant_id, firm_task_template_id=task.id) task.id: list_task_document_requirements(db, tenant_id=tenant_id, firm_task_template_id=task.id)
for task in tasks for task in tasks
@@ -1231,6 +1233,9 @@ def task_templates_detail(request: Request, catalogue_id: int):
task.id: list_task_document_templates(db, tenant_id=tenant_id, firm_task_template_id=task.id) task.id: list_task_document_templates(db, tenant_id=tenant_id, firm_task_template_id=task.id)
for task in tasks for task in tasks
} }
default_tasks = get_default_task_templates(db, catalogue_id=catalogue_id)
show_retired = request.query_params.get("show_retired") == "1"
display_tasks = tasks if show_retired else [task for task in tasks if task.is_active]
return _render( return _render(
request, request,
'modules/services/templates/services/task_template_detail.html', 'modules/services/templates/services/task_template_detail.html',
@@ -1240,11 +1245,20 @@ def task_templates_detail(request: Request, catalogue_id: int):
service=catalogue, service=catalogue,
selection=selection, selection=selection,
task_templates=tasks, task_templates=tasks,
display_task_templates=display_tasks,
show_retired=show_retired,
active_task_templates=[task for task in tasks if task.is_active],
retired_task_templates=[task for task in tasks if not task.is_active],
active_task_count=sum(1 for task in tasks if task.is_active),
retired_task_count=sum(1 for task in tasks if not task.is_active),
task_requirement_map=task_requirement_map, task_requirement_map=task_requirement_map,
task_template_file_map=task_template_file_map, task_template_file_map=task_template_file_map,
system_update_diff_map=system_update_diff_map, system_update_diff_map=system_update_diff_map,
system_update_retirement_map=system_update_retirement_map,
can_manage_tasks=_can_manage_firm_tasks(db, user), can_manage_tasks=_can_manage_firm_tasks(db, user),
default_tasks=get_default_task_templates(db, catalogue_id=catalogue_id), default_tasks=default_tasks,
active_default_task_count=sum(1 for task in default_tasks if task.is_active),
retired_default_task_count=sum(1 for task in default_tasks if not task.is_active),
is_system_admin=_is_system_admin(db, user), is_system_admin=_is_system_admin(db, user),
task_categories=list_task_categories(db, catalogue_id=catalogue_id, tenant_id=tenant_id, include_inactive=True), task_categories=list_task_categories(db, catalogue_id=catalogue_id, tenant_id=tenant_id, include_inactive=True),
) )
@@ -1361,7 +1375,24 @@ def default_templates_detail(request: Request, catalogue_id: int):
if not catalogue: if not catalogue:
return RedirectResponse(url='/services/defaults', status_code=303) return RedirectResponse(url='/services/defaults', status_code=303)
defaults = get_default_task_templates(db, catalogue_id=catalogue_id) defaults = get_default_task_templates(db, catalogue_id=catalogue_id)
return _render(request, 'modules/services/templates/services/default_templates_detail.html', db, user, title=f'Default Tasks - {catalogue.service_name}', service=catalogue, default_tasks=defaults, task_categories=list_task_categories(db, catalogue_id=catalogue_id, tenant_id=None, include_inactive=True)) show_retired = request.query_params.get("show_retired") == "1"
display_defaults = defaults if show_retired else [task for task in defaults if task.is_active]
return _render(
request,
'modules/services/templates/services/default_templates_detail.html',
db,
user,
title=f'Default Tasks - {catalogue.service_name}',
service=catalogue,
default_tasks=defaults,
display_default_tasks=display_defaults,
show_retired=show_retired,
active_default_tasks=[task for task in defaults if task.is_active],
retired_default_tasks=[task for task in defaults if not task.is_active],
active_default_task_count=sum(1 for task in defaults if task.is_active),
retired_default_task_count=sum(1 for task in defaults if not task.is_active),
task_categories=list_task_categories(db, catalogue_id=catalogue_id, tenant_id=None, include_inactive=True),
)
finally: finally:
db.close() db.close()
@@ -2628,6 +2659,7 @@ def sync_system_default_tasks_for_firm(request: Request, catalogue_id: int):
"updated": str(result.updated), "updated": str(result.updated),
"unchanged": str(result.unchanged), "unchanged": str(result.unchanged),
"duplicates_disabled": str(result.duplicates_disabled), "duplicates_disabled": str(result.duplicates_disabled),
"custom_updates": str(result.custom_updates_available),
} }
) )