diff --git a/app/modules/services/templates/services/task_template_detail.html b/app/modules/services/templates/services/task_template_detail.html index 1299b4e..d5e404f 100644 --- a/app/modules/services/templates/services/task_template_detail.html +++ b/app/modules/services/templates/services/task_template_detail.html @@ -26,6 +26,8 @@ {% if request.query_params.get('imported') %}
Excel import completed: {{ request.query_params.get('created','0') }} created, {{ request.query_params.get('updated','0') }} updated.
{% endif %} {% if request.query_params.get('import_error') %}
Excel import failed validation. No partial update was committed.
{% endif %} {% if request.query_params.get('system_upgraded') %}
System update applied to the firm task. Safe unstarted tasks in open engagements were refreshed automatically.
{% endif %} + {% if request.query_params.get('system_bulk_upgraded') %}
Full system upgrade completed. {{ request.query_params.get('bulk_count','0') }} firm task(s) were aligned to the latest system defaults{% if request.query_params.get('bulk_retired','0') != '0' %}, including {{ request.query_params.get('bulk_retired','0') }} retired system task(s){% endif %}. Open engagements: {{ request.query_params.get('engagement_created','0') }} task(s) added, {{ request.query_params.get('engagement_updated','0') }} pending task(s) refreshed, {{ request.query_params.get('engagement_deactivated','0') }} pending task(s) deactivated. Started/completed/reviewed history was preserved.
{% endif %} + {% if request.query_params.get('system_bulk_none') %}
All firm tasks for this service are already aligned with the latest system defaults. Firm-only tasks were left unchanged.
{% endif %} {% if request.query_params.get('system_kept') %}
Firm customization retained for this system revision. You will be prompted again when the system default changes in a later revision.
{% endif %} {% if request.query_params.get('sync_success') %}
System defaults synchronized: {{ request.query_params.get('created','0') }} created, {{ request.query_params.get('updated','0') }} updated, {{ request.query_params.get('custom_updates','0') }} customized task(s) awaiting review.
{% endif %} @@ -61,10 +63,18 @@

Create a task template for this firm and service.

{% if active_default_task_count > 0 %} -
- - -
+
+
+ + +
+ {% if system_update_available_count > 0 %} +
+ + +
+ {% endif %} +
{% endif %} @@ -163,6 +173,21 @@ {% endif %} + {% if can_manage_tasks and system_update_available_count > 0 %} +
+
+
+

System updates awaiting firm decision

+

{{ system_update_available_count }} customized task(s) have a newer system revision. Use Upgrade All to System for full alignment, or continue using the existing task-by-task Upgrade / Keep controls below.

+
+
+ + +
+
+
+ {% endif %} +
diff --git a/app/modules/services/ui.py b/app/modules/services/ui.py index 3884199..fef1357 100644 --- a/app/modules/services/ui.py +++ b/app/modules/services/ui.py @@ -1255,6 +1255,7 @@ def task_templates_detail(request: Request, catalogue_id: int): task_template_file_map=task_template_file_map, system_update_diff_map=system_update_diff_map, system_update_retirement_map=system_update_retirement_map, + system_update_available_count=sum(1 for task in tasks if getattr(task, "system_update_available", False) and getattr(task, "source_system_task_id", None)), can_manage_tasks=_can_manage_firm_tasks(db, user), default_tasks=default_tasks, active_default_task_count=sum(1 for task in default_tasks if task.is_active), @@ -1645,6 +1646,101 @@ def firm_task_template_toggle_active( db.close() +@router.post('/templates/{catalogue_id}/system-updates/upgrade-all') +def firm_tasks_accept_all_system_updates( + request: Request, + catalogue_id: int, + csrf_token: str = Form(...), +): + """Upgrade every pending firm-task system revision for the active firm/service. + + Firm-only tasks are intentionally untouched. Existing per-task upgrade/keep actions + remain available. Engagement synchronization runs once after all template updates so + pending/unstarted open-engagement tasks are refreshed safely without rewriting + started/completed/reviewed history. + """ + 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) + + pending_tasks = db.scalars( + select(FirmServiceTaskTemplate) + .where( + FirmServiceTaskTemplate.tenant_id == tenant_id, + FirmServiceTaskTemplate.service_catalogue_id == catalogue_id, + FirmServiceTaskTemplate.system_update_available.is_(True), + FirmServiceTaskTemplate.source_system_task_id.is_not(None), + ) + .order_by(FirmServiceTaskTemplate.sequence_no, FirmServiceTaskTemplate.id) + ).all() + + if not pending_tasks: + return RedirectResponse( + url=f'/services/templates/{catalogue_id}?system_bulk_none=1', + status_code=303, + ) + + upgraded = 0 + retired = 0 + for task in pending_tasks: + source = db.get(ServiceDefaultTaskTemplate, int(task.source_system_task_id)) + if source is None: + continue + if not bool(source.is_active): + retired += 1 + accept_system_update_for_firm_task( + db, + task=task, + user_id=user.id, + sync_open_engagements=False, + ) + upgraded += 1 + + engagement = sync_open_engagement_tasks_for_service( + db, + tenant_id=tenant_id, + catalogue_id=catalogue_id, + user_id=user.id, + include_started_open_tasks=False, + safe_system_rollout=True, + ) + db.commit() + + return RedirectResponse( + url=( + f'/services/templates/{catalogue_id}?system_bulk_upgraded=1' + f'&bulk_count={upgraded}' + f'&bulk_retired={retired}' + f'&engagement_created={engagement.get("created", 0)}' + f'&engagement_updated={engagement.get("updated_pending", 0)}' + f'&engagement_deactivated={engagement.get("deactivated_pending", 0)}' + f'&engagement_preserved={engagement.get("preserved_history", 0)}' + ), + status_code=303, + ) + except ValueError: + db.rollback() + return RedirectResponse( + url=f'/services/templates/{catalogue_id}?error=system_bulk_upgrade_failed', + status_code=303, + ) + except Exception: + db.rollback() + raise + finally: + db.close() + + @router.post('/templates/{catalogue_id}/tasks/{task_id}/system-update/upgrade') def firm_task_accept_system_update( request: Request,