Add task administration upgrade with duplicate merge and task tools

This commit is contained in:
A R R R Associates
2026-08-30 22:12:27 +05:30
parent ad0815c807
commit af4f6e8b28
17 changed files with 2072 additions and 14 deletions
+126 -3
View File
@@ -26,6 +26,9 @@ 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.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
from app.modules.services.due_dates import (
DUE_PERIOD_TYPES,
DUE_YEAR_BASIS_CHOICES,
@@ -88,6 +91,7 @@ def _base_ctx(request: Request, user, db, **ctx):
"due_period_types": DUE_PERIOD_TYPES,
"due_year_basis_choices": DUE_YEAR_BASIS_CHOICES,
"task_execution_roles": TASK_EXECUTION_ROLES,
"task_tools": list_task_tools(),
}
base.update(ctx)
return base
@@ -968,6 +972,7 @@ def _copy_system_defaults_if_firm_tasks_empty(
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),
@@ -1262,7 +1267,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(''), eligible_roles: list[str] = Form([]), sequence_no: int | None = Form(None), is_mandatory: str | None = Form(None), requires_review: str | None = Form(None), normal_review_role: str = Form('manager'), 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), normal_review_role: str = Form('manager'), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), task_tool_code: 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:
@@ -1293,6 +1298,7 @@ def task_template_create_submit(request: Request, catalogue_id: int, task_name:
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,
@@ -1374,7 +1380,7 @@ def default_templates_detail(request: Request, catalogue_id: int):
@router.post('/catalogue/{catalogue_id}/defaults/new')
def default_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), normal_review_role: str = Form('manager'), 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 default_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), normal_review_role: str = Form('manager'), task_category: str = Form(''), response_required: str | None = Form(None), response_type: str = Form('NONE'), task_tool_code: 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:
@@ -1387,7 +1393,7 @@ def default_template_create_submit(request: Request, catalogue_id: int, task_nam
catalogue = get_catalogue(db, catalogue_id)
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"), 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)
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()
return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults', status_code=303)
finally:
@@ -1433,6 +1439,7 @@ def copy_defaults_to_firm(request: Request, catalogue_id: int, csrf_token: str =
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),
@@ -1795,6 +1802,7 @@ def firm_task_template_edit_submit(
task_category: str = Form(''),
response_required: str | None = Form(None),
response_type: str = Form('NONE'),
task_tool_code: str = Form('NONE'),
evidence_required: str | None = Form(None),
remarks_required_if_no: str | None = Form(None),
is_aqmm_task: str | None = Form(None),
@@ -1844,6 +1852,7 @@ def firm_task_template_edit_submit(
task.task_category = category.name if category else None
task.response_required = response_required is not None
task.response_type = response_type.strip().upper() or "NONE"
task.task_tool_code = normalize_task_tool_code(task_tool_code)
task.evidence_required = evidence_required is not None
task.remarks_required_if_no = remarks_required_if_no is not None
task.is_aqmm_task = is_aqmm_task is not None
@@ -1942,6 +1951,7 @@ def default_task_template_edit_submit(
task_category: str = Form(''),
response_required: str | None = Form(None),
response_type: str = Form('NONE'),
task_tool_code: str = Form('NONE'),
evidence_required: str | None = Form(None),
remarks_required_if_no: str | None = Form(None),
is_aqmm_task: str | None = Form(None),
@@ -1991,6 +2001,7 @@ def default_task_template_edit_submit(
task.task_category = category.name if category else None
task.response_required = response_required is not None
task.response_type = response_type.strip().upper() or "NONE"
task.task_tool_code = normalize_task_tool_code(task_tool_code)
task.evidence_required = evidence_required is not None
task.remarks_required_if_no = remarks_required_if_no is not None
task.is_aqmm_task = is_aqmm_task is not None
@@ -2008,6 +2019,118 @@ def default_task_template_edit_submit(
finally:
db.close()
# -----------------------------------------------------------------------------
# Task administration: service export/import and safe duplicate consolidation
# -----------------------------------------------------------------------------
@router.get('/catalogue/{catalogue_id}/defaults/export')
def default_tasks_service_export(request: Request, catalogue_id: int):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user: return RedirectResponse(url='/login', status_code=303)
require_permission(db, user, 'service_tasks.view')
if not _is_system_admin(db, user): return _redirect_denied()
catalogue = get_catalogue(db, catalogue_id)
if not catalogue: return RedirectResponse(url='/services/defaults', status_code=303)
payload = build_default_tasks_export(db, catalogue_id=catalogue_id)
filename = f"{catalogue.service_code}_default_tasks_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.xlsx"
return StreamingResponse(iter([payload]), media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', headers={'Content-Disposition': f'attachment; filename="{filename}"'})
finally: db.close()
@router.post('/catalogue/{catalogue_id}/defaults/import')
def default_tasks_service_import(request: Request, catalogue_id: int, file: UploadFile = File(...), 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)
require_permission(db,user,'service_tasks.edit')
if not _is_system_admin(db,user): return _redirect_denied()
catalogue=get_catalogue(db,catalogue_id)
if not catalogue: return RedirectResponse(url='/services/defaults',status_code=303)
result=import_system_default_tasks(db,current_user=user,file_bytes=file.file.read(),update_existing=True,expected_service_code=catalogue.service_code)
if result.get('errors'): return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults?import_error=1',status_code=303)
return RedirectResponse(url=f'/services/catalogue/{catalogue_id}/defaults?imported=1&created={result.get("created",0)}&updated={result.get("updated",0)}',status_code=303)
finally: db.close()
@router.get('/templates/{catalogue_id}/export')
def firm_tasks_service_export(request: Request, catalogue_id: int):
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); catalogue=get_catalogue(db,catalogue_id)
if not catalogue: return RedirectResponse(url='/services/templates',status_code=303)
payload=build_firm_tasks_export(db,tenant_id=tenant_id,catalogue_id=catalogue_id)
filename=f"{catalogue.service_code}_firm_tasks_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.xlsx"
return StreamingResponse(iter([payload]),media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',headers={'Content-Disposition':f'attachment; filename="{filename}"'})
finally: db.close()
@router.post('/templates/{catalogue_id}/import')
def firm_tasks_service_import(request: Request, catalogue_id: int, file: UploadFile = File(...), 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); catalogue=get_catalogue(db,catalogue_id)
if not catalogue: return RedirectResponse(url='/services/templates',status_code=303)
result=import_firm_task_templates(db,current_user=user,tenant_id=tenant_id,file_bytes=file.file.read(),update_existing=True,expected_service_code=catalogue.service_code)
if result.get('errors'): return RedirectResponse(url=f'/services/templates/{catalogue_id}?import_error=1',status_code=303)
return RedirectResponse(url=f'/services/templates/{catalogue_id}?imported=1&created={result.get("created",0)}&updated={result.get("updated",0)}',status_code=303)
finally: db.close()
@router.get('/catalogue/{catalogue_id}/defaults/duplicates')
def default_task_duplicates_page(request: Request, catalogue_id: int):
db=CommonSessionLocal()
try:
user=get_current_user(request,db=db)
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()
service=get_catalogue(db,catalogue_id); tasks=get_default_task_templates(db,catalogue_id=catalogue_id)
return _render(request,'modules/services/templates/services/task_duplicate_review.html',db,user,title=f'Duplicate Tasks - {service.service_name}',service=service,tasks=tasks,pairs=find_duplicate_pairs(tasks),scope='default')
finally: db.close()
@router.post('/catalogue/{catalogue_id}/defaults/duplicates/merge')
def default_task_duplicates_merge(request: Request,catalogue_id:int,master_task_id:int=Form(...),source_task_ids:list[int]=Form([]),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)
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()
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)
finally: db.close()
@router.get('/templates/{catalogue_id}/duplicates')
def firm_task_duplicates_page(request: Request,catalogue_id:int):
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); service=get_catalogue(db,catalogue_id); tasks=get_firm_task_templates(db,tenant_id=tenant_id,catalogue_id=catalogue_id)
return _render(request,'modules/services/templates/services/task_duplicate_review.html',db,user,title=f'Duplicate Firm Tasks - {service.service_name}',service=service,tasks=tasks,pairs=find_duplicate_pairs(tasks),scope='firm')
finally: db.close()
@router.post('/templates/{catalogue_id}/duplicates/merge')
def firm_task_duplicates_merge(request: Request,catalogue_id:int,master_task_id:int=Form(...),source_task_ids:list[int]=Form([]),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); count=merge_firm_tasks(db,tenant_id=tenant_id,catalogue_id=catalogue_id,master_task_id=master_task_id,source_task_ids=source_task_ids,user_id=user.id); db.commit()
return RedirectResponse(url=f'/services/templates/{catalogue_id}/duplicates?merged={count}',status_code=303)
except ValueError:
db.rollback(); return RedirectResponse(url=f'/services/templates/{catalogue_id}/duplicates?merge_error=1',status_code=303)
finally: db.close()
# -----------------------------------------------------------------------------
# Service Task Category Master
# -----------------------------------------------------------------------------