diff --git a/app/modules/services/default_task_sync.py b/app/modules/services/default_task_sync.py index 1975862..27bfec1 100644 --- a/app/modules/services/default_task_sync.py +++ b/app/modules/services/default_task_sync.py @@ -109,6 +109,29 @@ def firm_task_hash(target: FirmServiceTaskTemplate) -> str: 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]]: labels = { "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 # already firm-customized by comparing the full portable snapshot. 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: target.last_synced_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. if ( not target.is_customized diff --git a/app/modules/services/templates/services/default_templates_detail.html b/app/modules/services/templates/services/default_templates_detail.html index 6f366dd..e80dbc0 100644 --- a/app/modules/services/templates/services/default_templates_detail.html +++ b/app/modules/services/templates/services/default_templates_detail.html @@ -11,14 +11,21 @@ {% if request.query_params.get('imported') %}
These tasks are available for synchronization to firms and new engagement work.
Soft-retired tasks remain in the database so historical engagement and audit records are preserved.
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.
Active tasks are shown by default. Retired tasks remain available for audit/history review.
{{ task.description }}
{% endif %}Tasks customised for the active firm.
+Active tasks for the firm. {{ retired_task_count }} retired task(s) are preserved for history.
Inherited defaults stay synchronized automatically; customized tasks require Firm Admin approval before a later system update is applied.
+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.
Create a task template for this firm and service.