Auto-sync system default tasks to firms and open engagements
This commit is contained in:
+133
-85
@@ -27,6 +27,14 @@ from app.modules.services.bulk_imports import (
|
||||
from app.modules.services.catalogue_export import build_service_catalogue_export
|
||||
from app.modules.services.default_tasks_export import build_default_tasks_export
|
||||
from app.modules.services.firm_tasks_export import build_firm_tasks_export
|
||||
from app.modules.services.default_task_sync import (
|
||||
accept_system_update_for_firm_task,
|
||||
keep_firm_customization_for_system_revision,
|
||||
mark_firm_task_customized,
|
||||
sync_firm_tasks_from_system_defaults,
|
||||
sync_system_defaults_to_all_firms,
|
||||
system_task_diff,
|
||||
)
|
||||
from app.modules.services.task_categories_excel import build_task_categories_export, import_task_categories
|
||||
from app.modules.services.task_tools import list_task_tools, normalize_task_tool_code
|
||||
from app.modules.services.task_duplicates import find_duplicate_pairs, merge_default_tasks, merge_firm_tasks
|
||||
@@ -944,51 +952,21 @@ def _copy_system_defaults_if_firm_tasks_empty(
|
||||
catalogue_id: int,
|
||||
user_id: int,
|
||||
) -> tuple[int, bool]:
|
||||
"""Initialise firm task templates once without overwriting custom firm tasks."""
|
||||
"""Initialise/synchronise inherited firm tasks from the current system defaults."""
|
||||
existing_tasks = get_firm_task_templates(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
catalogue_id=catalogue_id,
|
||||
)
|
||||
if existing_tasks:
|
||||
return 0, True
|
||||
|
||||
defaults = get_default_task_templates(db, catalogue_id=catalogue_id)
|
||||
copied_count = 0
|
||||
for default in defaults:
|
||||
db.add(FirmServiceTaskTemplate(
|
||||
tenant_id=tenant_id,
|
||||
service_catalogue_id=catalogue_id,
|
||||
task_name=default.task_name,
|
||||
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,
|
||||
normal_review_role=getattr(default, "normal_review_role", None),
|
||||
task_category_id=(firm_category.id if (firm_category := ensure_task_category(db, catalogue_id=catalogue_id, tenant_id=tenant_id, name=getattr(default, "task_category", None), user_id=user_id)) else None),
|
||||
task_category=(firm_category.name if firm_category else None),
|
||||
response_required=getattr(default, "response_required", False),
|
||||
response_type=getattr(default, "response_type", "NONE") or "NONE",
|
||||
evidence_required=getattr(default, "evidence_required", False),
|
||||
remarks_required_if_no=getattr(default, "remarks_required_if_no", False),
|
||||
task_tool_code=getattr(default, "task_tool_code", "NONE") or "NONE",
|
||||
is_aqmm_task=getattr(default, "is_aqmm_task", False),
|
||||
aqmm_mandatory=getattr(default, "aqmm_mandatory", False),
|
||||
aqmm_evidence_required=getattr(default, "aqmm_evidence_required", False),
|
||||
aqmm_manager_review_required=getattr(default, "aqmm_manager_review_required", False),
|
||||
aqmm_partner_review_required=getattr(default, "aqmm_partner_review_required", False),
|
||||
aqmm_review_partner_required=getattr(default, "aqmm_review_partner_required", False),
|
||||
aqmm_blocks_final_release=getattr(default, "aqmm_blocks_final_release", False),
|
||||
aqmm_reference=getattr(default, "aqmm_reference", None),
|
||||
is_active=default.is_active,
|
||||
created_by_user_id=user_id,
|
||||
updated_by_user_id=user_id,
|
||||
))
|
||||
copied_count += 1
|
||||
|
||||
return copied_count, False
|
||||
had_existing = bool(existing_tasks)
|
||||
result = sync_firm_tasks_from_system_defaults(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
service_catalogue_id=catalogue_id,
|
||||
updated_by_user_id=user_id,
|
||||
sync_open_engagements=False,
|
||||
)
|
||||
return result.created, had_existing
|
||||
|
||||
|
||||
@router.post('/catalogue/bulk-selection')
|
||||
@@ -1239,6 +1217,12 @@ def task_templates_detail(request: Request, catalogue_id: int):
|
||||
if not selection or not selection.is_enabled:
|
||||
return RedirectResponse(url='/services', status_code=303)
|
||||
tasks = get_firm_task_templates(db, tenant_id=tenant_id, catalogue_id=catalogue_id)
|
||||
system_update_diff_map = {}
|
||||
for firm_task in tasks:
|
||||
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))
|
||||
if system_task is not None:
|
||||
system_update_diff_map[firm_task.id] = system_task_diff(system_task, firm_task)
|
||||
task_requirement_map = {
|
||||
task.id: list_task_document_requirements(db, tenant_id=tenant_id, firm_task_template_id=task.id)
|
||||
for task in tasks
|
||||
@@ -1258,6 +1242,7 @@ def task_templates_detail(request: Request, catalogue_id: int):
|
||||
task_templates=tasks,
|
||||
task_requirement_map=task_requirement_map,
|
||||
task_template_file_map=task_template_file_map,
|
||||
system_update_diff_map=system_update_diff_map,
|
||||
can_manage_tasks=_can_manage_firm_tasks(db, user),
|
||||
default_tasks=get_default_task_templates(db, catalogue_id=catalogue_id),
|
||||
is_system_admin=_is_system_admin(db, user),
|
||||
@@ -1311,6 +1296,7 @@ def task_template_create_submit(request: Request, catalogue_id: int, task_name:
|
||||
aqmm_blocks_final_release=aqmm_blocks_final_release is not None,
|
||||
aqmm_reference=aqmm_reference.strip() or None,
|
||||
is_active=is_active is not None,
|
||||
is_customized=True,
|
||||
created_by_user_id=user.id,
|
||||
updated_by_user_id=user.id,
|
||||
)
|
||||
@@ -1395,7 +1381,15 @@ def default_template_create_submit(request: Request, catalogue_id: int, task_nam
|
||||
if not catalogue:
|
||||
return RedirectResponse(url='/services/defaults', status_code=303)
|
||||
row = ServiceDefaultTaskTemplate(service_catalogue_id=catalogue_id, task_name=task_name.strip(), description=description.strip() or None, default_role_name=default_role_name.strip() or None, sequence_no=sequence_no or next_default_task_sequence(db, catalogue_id=catalogue_id), is_mandatory=is_mandatory is not None, requires_review=requires_review is not None, normal_review_role=_normal_review_role_value(normal_review_role, requires_review=requires_review is not None), task_category_id=(category.id if (category := ensure_task_category(db, catalogue_id=catalogue_id, tenant_id=None, name=task_category, user_id=user.id)) else None), task_category=(category.name if category else None), response_required=response_required is not None, response_type=(response_type.strip().upper() or "NONE"), task_tool_code=normalize_task_tool_code(task_tool_code), evidence_required=evidence_required is not None, remarks_required_if_no=remarks_required_if_no is not None, is_aqmm_task=is_aqmm_task is not None, aqmm_mandatory=aqmm_mandatory is not None, aqmm_evidence_required=aqmm_evidence_required is not None, aqmm_manager_review_required=aqmm_manager_review_required is not None, aqmm_partner_review_required=aqmm_partner_review_required is not None, aqmm_review_partner_required=aqmm_review_partner_required is not None, aqmm_blocks_final_release=aqmm_blocks_final_release is not None, aqmm_reference=aqmm_reference.strip() or None, is_active=is_active is not None)
|
||||
db.add(row); db.commit()
|
||||
db.add(row)
|
||||
db.flush()
|
||||
sync_system_defaults_to_all_firms(
|
||||
db,
|
||||
service_catalogue_id=catalogue_id,
|
||||
updated_by_user_id=user.id,
|
||||
sync_open_engagements=True,
|
||||
)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults', status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1415,46 +1409,22 @@ def copy_defaults_to_firm(request: Request, catalogue_id: int, csrf_token: str =
|
||||
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)
|
||||
existing_sequences = {t.sequence_no for t in get_firm_task_templates(db, tenant_id=tenant_id, catalogue_id=catalogue_id)}
|
||||
defaults = get_default_task_templates(db, catalogue_id=catalogue_id)
|
||||
next_seq = next_task_sequence(db, tenant_id=tenant_id, catalogue_id=catalogue_id)
|
||||
for d in defaults:
|
||||
seq = d.sequence_no
|
||||
if seq in existing_sequences:
|
||||
seq = next_seq
|
||||
next_seq += 1
|
||||
existing_sequences.add(seq)
|
||||
db.add(FirmServiceTaskTemplate(
|
||||
tenant_id=tenant_id,
|
||||
service_catalogue_id=catalogue_id,
|
||||
task_name=d.task_name,
|
||||
description=d.description,
|
||||
sequence_no=seq,
|
||||
default_role_name=d.default_role_name,
|
||||
is_mandatory=d.is_mandatory,
|
||||
requires_review=d.requires_review,
|
||||
normal_review_role=getattr(d, "normal_review_role", None),
|
||||
task_category_id=(firm_category.id if (firm_category := ensure_task_category(db, catalogue_id=catalogue_id, tenant_id=tenant_id, name=getattr(d, "task_category", None), user_id=user.id)) else None),
|
||||
task_category=(firm_category.name if firm_category else None),
|
||||
response_required=getattr(d, "response_required", False),
|
||||
response_type=getattr(d, "response_type", "NONE") or "NONE",
|
||||
evidence_required=getattr(d, "evidence_required", False),
|
||||
remarks_required_if_no=getattr(d, "remarks_required_if_no", False),
|
||||
task_tool_code=getattr(d, "task_tool_code", "NONE") or "NONE",
|
||||
is_aqmm_task=getattr(d, "is_aqmm_task", False),
|
||||
aqmm_mandatory=getattr(d, "aqmm_mandatory", False),
|
||||
aqmm_evidence_required=getattr(d, "aqmm_evidence_required", False),
|
||||
aqmm_manager_review_required=getattr(d, "aqmm_manager_review_required", False),
|
||||
aqmm_partner_review_required=getattr(d, "aqmm_partner_review_required", False),
|
||||
aqmm_review_partner_required=getattr(d, "aqmm_review_partner_required", False),
|
||||
aqmm_blocks_final_release=getattr(d, "aqmm_blocks_final_release", False),
|
||||
aqmm_reference=getattr(d, "aqmm_reference", None),
|
||||
is_active=d.is_active,
|
||||
created_by_user_id=user.id,
|
||||
updated_by_user_id=user.id,
|
||||
))
|
||||
result = sync_firm_tasks_from_system_defaults(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
service_catalogue_id=catalogue_id,
|
||||
updated_by_user_id=user.id,
|
||||
sync_open_engagements=True,
|
||||
)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f'/services/templates/{catalogue_id}', status_code=303)
|
||||
return RedirectResponse(
|
||||
url=(
|
||||
f'/services/templates/{catalogue_id}?sync_success=1'
|
||||
f'&created={result.created}&updated={result.updated}'
|
||||
f'&custom_updates={result.custom_updates_available}'
|
||||
),
|
||||
status_code=303,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -1631,6 +1601,7 @@ def firm_task_template_toggle_active(
|
||||
)
|
||||
|
||||
task.is_active = not bool(task.is_active)
|
||||
mark_firm_task_customized(task)
|
||||
task.updated_by_user_id = user.id
|
||||
db.commit()
|
||||
|
||||
@@ -1643,6 +1614,70 @@ def firm_task_template_toggle_active(
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post('/templates/{catalogue_id}/tasks/{task_id}/system-update/upgrade')
|
||||
def firm_task_accept_system_update(
|
||||
request: Request,
|
||||
catalogue_id: int,
|
||||
task_id: int,
|
||||
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)
|
||||
task = get_firm_task_template(db, tenant_id=tenant_id, catalogue_id=catalogue_id, task_id=task_id)
|
||||
if not task or not getattr(task, 'system_update_available', False):
|
||||
return RedirectResponse(url=f'/services/templates/{catalogue_id}?error=system_update_missing', status_code=303)
|
||||
result = accept_system_update_for_firm_task(db, task=task, user_id=user.id, sync_open_engagements=True)
|
||||
db.commit()
|
||||
return RedirectResponse(
|
||||
url=(f'/services/templates/{catalogue_id}?system_upgraded=1'
|
||||
f'&engagement_created={result.engagement_created}'
|
||||
f'&engagement_updated={result.engagement_updated_pending}'
|
||||
f'&engagement_deactivated={result.engagement_deactivated_pending}'),
|
||||
status_code=303,
|
||||
)
|
||||
except ValueError:
|
||||
db.rollback()
|
||||
return RedirectResponse(url=f'/services/templates/{catalogue_id}?error=system_update_missing', status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post('/templates/{catalogue_id}/tasks/{task_id}/system-update/keep-firm')
|
||||
def firm_task_keep_customization(
|
||||
request: Request,
|
||||
catalogue_id: int,
|
||||
task_id: int,
|
||||
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)
|
||||
task = get_firm_task_template(db, tenant_id=tenant_id, catalogue_id=catalogue_id, task_id=task_id)
|
||||
if not task or not getattr(task, 'system_update_available', False):
|
||||
return RedirectResponse(url=f'/services/templates/{catalogue_id}?error=system_update_missing', status_code=303)
|
||||
keep_firm_customization_for_system_revision(db, task=task, user_id=user.id)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f'/services/templates/{catalogue_id}?system_kept=1', status_code=303)
|
||||
except ValueError:
|
||||
db.rollback()
|
||||
return RedirectResponse(url=f'/services/templates/{catalogue_id}?error=system_update_missing', status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post('/templates/{catalogue_id}/sync-open-engagements')
|
||||
def sync_firm_tasks_to_open_engagements(
|
||||
request: Request,
|
||||
@@ -1728,6 +1763,8 @@ def firm_task_template_move(
|
||||
|
||||
# Swap safely across the existing unique tenant/service/sequence constraint.
|
||||
min_seq = min(int(row.sequence_no) for row in tasks)
|
||||
mark_firm_task_customized(current)
|
||||
mark_firm_task_customized(other)
|
||||
current.sequence_no = min_seq - 1000000 - int(current.id)
|
||||
current.updated_by_user_id = user.id
|
||||
db.flush()
|
||||
@@ -1865,6 +1902,7 @@ def firm_task_template_edit_submit(
|
||||
task.aqmm_blocks_final_release = aqmm_blocks_final_release is not None
|
||||
task.aqmm_reference = aqmm_reference.strip() or None
|
||||
task.is_active = is_active is not None
|
||||
mark_firm_task_customized(task)
|
||||
task.updated_by_user_id = user.id
|
||||
|
||||
sync_result = None
|
||||
@@ -2015,6 +2053,13 @@ def default_task_template_edit_submit(
|
||||
task.aqmm_reference = aqmm_reference.strip() or None
|
||||
task.is_active = is_active is not None
|
||||
|
||||
db.flush()
|
||||
sync_system_defaults_to_all_firms(
|
||||
db,
|
||||
service_catalogue_id=catalogue_id,
|
||||
updated_by_user_id=user.id,
|
||||
sync_open_engagements=True,
|
||||
)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults', status_code=303)
|
||||
finally:
|
||||
@@ -2102,7 +2147,9 @@ def default_task_duplicates_merge(request: Request,catalogue_id:int,master_task_
|
||||
if not user: return RedirectResponse(url='/login',status_code=303)
|
||||
require_permission(db,user,'service_tasks.edit')
|
||||
if not _is_system_admin(db,user): return _redirect_denied()
|
||||
count=merge_default_tasks(db,catalogue_id=catalogue_id,master_task_id=master_task_id,source_task_ids=source_task_ids); db.commit()
|
||||
count=merge_default_tasks(db,catalogue_id=catalogue_id,master_task_id=master_task_id,source_task_ids=source_task_ids)
|
||||
sync_system_defaults_to_all_firms(db,service_catalogue_id=catalogue_id,updated_by_user_id=user.id,sync_open_engagements=True)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults/duplicates?merged={count}',status_code=303)
|
||||
except ValueError:
|
||||
db.rollback(); return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults/duplicates?merge_error=1',status_code=303)
|
||||
@@ -2237,9 +2284,11 @@ def firm_task_category_update(request: Request, catalogue_id: int, category_id:
|
||||
duplicate=db.execute(select(ServiceTaskCategory.id).where(ServiceTaskCategory.tenant_id==tenant_id, ServiceTaskCategory.service_catalogue_id==catalogue_id, ServiceTaskCategory.code==clean_code, ServiceTaskCategory.id!=row.id)).scalar_one_or_none()
|
||||
if duplicate is not None: return RedirectResponse(url=f'/services/templates/{catalogue_id}/categories?error=duplicate_code', status_code=303)
|
||||
old_name=row.name; row.name=clean_name; row.code=clean_code; row.sort_order=sort_order; row.is_active=is_active is not None; row.updated_by_user_id=user.id
|
||||
for task in db.execute(select(FirmServiceTaskTemplate).where(FirmServiceTaskTemplate.tenant_id==tenant_id, FirmServiceTaskTemplate.service_catalogue_id==catalogue_id, FirmServiceTaskTemplate.task_category_id==row.id)).scalars().all(): task.task_category=clean_name
|
||||
for task in db.execute(select(FirmServiceTaskTemplate).where(FirmServiceTaskTemplate.tenant_id==tenant_id, FirmServiceTaskTemplate.service_catalogue_id==catalogue_id, FirmServiceTaskTemplate.task_category_id==row.id)).scalars().all():
|
||||
task.task_category=clean_name; mark_firm_task_customized(task)
|
||||
# Backward-compatible rows created before category IDs are also renamed when their text exactly matches.
|
||||
for task in db.execute(select(FirmServiceTaskTemplate).where(FirmServiceTaskTemplate.tenant_id==tenant_id, FirmServiceTaskTemplate.service_catalogue_id==catalogue_id, FirmServiceTaskTemplate.task_category_id.is_(None), FirmServiceTaskTemplate.task_category==old_name)).scalars().all(): task.task_category_id=row.id; task.task_category=clean_name
|
||||
for task in db.execute(select(FirmServiceTaskTemplate).where(FirmServiceTaskTemplate.tenant_id==tenant_id, FirmServiceTaskTemplate.service_catalogue_id==catalogue_id, FirmServiceTaskTemplate.task_category_id.is_(None), FirmServiceTaskTemplate.task_category==old_name)).scalars().all():
|
||||
task.task_category_id=row.id; task.task_category=clean_name; mark_firm_task_customized(task)
|
||||
db.commit(); return RedirectResponse(url=f'/services/templates/{catalogue_id}/categories?updated=1', status_code=303)
|
||||
finally: db.close()
|
||||
|
||||
@@ -2334,6 +2383,7 @@ def system_task_category_update(request: Request, catalogue_id: int, category_id
|
||||
old_name=row.name; row.name=clean_name; row.code=clean_code; row.sort_order=sort_order; row.is_active=is_active is not None; row.updated_by_user_id=user.id
|
||||
for task in db.execute(select(ServiceDefaultTaskTemplate).where(ServiceDefaultTaskTemplate.service_catalogue_id==catalogue_id, ServiceDefaultTaskTemplate.task_category_id==row.id)).scalars().all(): task.task_category=row.name
|
||||
for task in db.execute(select(ServiceDefaultTaskTemplate).where(ServiceDefaultTaskTemplate.service_catalogue_id==catalogue_id, ServiceDefaultTaskTemplate.task_category_id.is_(None), ServiceDefaultTaskTemplate.task_category==old_name)).scalars().all(): task.task_category_id=row.id; task.task_category=row.name
|
||||
sync_system_defaults_to_all_firms(db,service_catalogue_id=catalogue_id,updated_by_user_id=user.id,sync_open_engagements=True)
|
||||
db.commit(); return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/task-categories?updated=1',status_code=303)
|
||||
finally: db.close()
|
||||
|
||||
@@ -2528,8 +2578,6 @@ def upload_firm_task_templates(request: Request, file: UploadFile = File(...), u
|
||||
@router.post("/catalogue/{catalogue_id}/firm-tasks/sync-system-defaults")
|
||||
def sync_system_default_tasks_for_firm(request: Request, catalogue_id: int):
|
||||
"""Idempotent replacement for the old insert-only 'Copy System Default Tasks' action."""
|
||||
from app.modules.services.default_task_sync import sync_firm_tasks_from_system_defaults
|
||||
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
|
||||
Reference in New Issue
Block a user