Add Excel import export and code linking for task categories

This commit is contained in:
A R R R Associates
2026-08-31 12:52:50 +05:30
parent af4f6e8b28
commit fbb8607842
6 changed files with 551 additions and 14 deletions
+60 -4
View File
@@ -19,6 +19,7 @@ from app.modules.services.models import (
ServiceCatalogue,
ServiceCategory,
ServiceDefaultTaskTemplate,
ServiceTaskCategory,
ServiceDueDateExtension,
ServiceDueDateRule,
)
@@ -84,6 +85,13 @@ DEFAULT_TASK_COLUMNS = [
"default_role_name",
"is_mandatory",
"requires_review",
"task_category_code",
"task_category",
"response_required",
"response_type",
"evidence_required",
"remarks_required_if_no",
"task_tool_code",
"is_aqmm_task",
"aqmm_mandatory",
"aqmm_evidence_required",
@@ -103,6 +111,13 @@ FIRM_TASK_COLUMNS = [
"default_role_name",
"is_mandatory",
"requires_review",
"task_category_code",
"task_category",
"response_required",
"response_type",
"evidence_required",
"remarks_required_if_no",
"task_tool_code",
"is_aqmm_task",
"aqmm_mandatory",
"aqmm_evidence_required",
@@ -1015,6 +1030,35 @@ def import_due_date_extensions(
return {"created": created if not errors else 0, "updated": updated if not errors else 0, "skipped": skipped, "errors": errors}
def _resolve_import_task_category(db: Session, *, catalogue_id: int, tenant_id: int | None, category_code: str, category_name: str, user_id: int | None):
"""Resolve category by stable code first; preserve legacy name-based imports.
When task_category_code is supplied it must already exist in the category
master for the same service/scope. This prevents spelling mistakes from
silently creating unintended category records. Older workbooks that only
contain task_category continue to work exactly as before.
"""
code = normalize_code(category_code) if category_code else ""
name = _clean(category_name)
scope_filter = ServiceTaskCategory.tenant_id.is_(None) if tenant_id is None else ServiceTaskCategory.tenant_id == tenant_id
if code:
category = db.execute(
select(ServiceTaskCategory).where(
ServiceTaskCategory.service_catalogue_id == catalogue_id,
scope_filter,
ServiceTaskCategory.code == code,
)
).scalar_one_or_none()
if category is None:
raise ValueError(f"Task category code {code} was not found for this service. Import/create the Task Category master first.")
if name and category.name.strip().lower() != name.strip().lower():
# Code is the stable key. The master name wins so renamed categories
# do not break task imports.
pass
return category
return ensure_task_category(db, catalogue_id=catalogue_id, tenant_id=tenant_id, name=name or None, user_id=user_id)
def import_system_default_tasks(db: Session, *, current_user, file_bytes: bytes, update_existing: bool = True, expected_service_code: str | None = None) -> dict:
ws, headers = _load_sheet(file_bytes, "system_default_tasks")
missing = _validate_headers(headers, ["service_code", "sequence_no", "task_name"])
@@ -1056,8 +1100,14 @@ def import_system_default_tasks(db: Session, *, current_user, file_bytes: bytes,
task.default_role_name = _clean(_cell(row, headers, "default_role_name")) or None
task.is_mandatory = _bool(_cell(row, headers, "is_mandatory"), True)
task.requires_review = _bool(_cell(row, headers, "requires_review"), False)
task.task_category = _clean(_cell(row, headers, "task_category")) or None
category = ensure_task_category(db, catalogue_id=catalogue.id, tenant_id=None, name=task.task_category, user_id=current_user.id)
category = _resolve_import_task_category(
db,
catalogue_id=catalogue.id,
tenant_id=None,
category_code=_clean(_cell(row, headers, "task_category_code")),
category_name=_clean(_cell(row, headers, "task_category")),
user_id=current_user.id,
)
task.task_category_id = category.id if category else None
task.task_category = category.name if category else None
task.response_required = _bool(_cell(row, headers, "response_required"), False)
@@ -1134,8 +1184,14 @@ def import_firm_task_templates(db: Session, *, current_user, tenant_id: int, fil
task.default_role_name = _clean(_cell(row, headers, "default_role_name")) or None
task.is_mandatory = _bool(_cell(row, headers, "is_mandatory"), True)
task.requires_review = _bool(_cell(row, headers, "requires_review"), False)
task.task_category = _clean(_cell(row, headers, "task_category")) or None
category = ensure_task_category(db, catalogue_id=catalogue.id, tenant_id=tenant_id, name=task.task_category, user_id=current_user.id)
category = _resolve_import_task_category(
db,
catalogue_id=catalogue.id,
tenant_id=tenant_id,
category_code=_clean(_cell(row, headers, "task_category_code")),
category_name=_clean(_cell(row, headers, "task_category")),
user_id=current_user.id,
)
task.task_category_id = category.id if category else None
task.task_category = category.name if category else None
task.response_required = _bool(_cell(row, headers, "response_required"), False)