Fix firm default task sync sequence conflicts
This commit is contained in:
@@ -176,6 +176,36 @@ def sync_firm_tasks_from_system_defaults(
|
|||||||
firm_task_ids=[int(row.id) for row in firm_rows if getattr(row, "id", None)],
|
firm_task_ids=[int(row.id) for row in firm_rows if getattr(row, "id", None)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# IMPORTANT: two-phase sequence re-numbering
|
||||||
|
#
|
||||||
|
# The database enforces a UNIQUE constraint on:
|
||||||
|
# (tenant_id, service_catalogue_id, sequence_no)
|
||||||
|
#
|
||||||
|
# Existing duplicate imports may contain several generations of the same
|
||||||
|
# task at different sequence numbers. During a sync, moving a canonical
|
||||||
|
# row directly onto the latest system-default sequence can therefore
|
||||||
|
# collide with another historical row that still owns that sequence.
|
||||||
|
#
|
||||||
|
# Marking that historical row inactive does NOT release the unique key.
|
||||||
|
# Park every existing firm template on a guaranteed-unique temporary
|
||||||
|
# sequence first, flush, and only then apply final/default sequences.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
max_existing_sequence = max(
|
||||||
|
[int(getattr(row, "sequence_no", 0) or 0) for row in firm_rows] + [0]
|
||||||
|
)
|
||||||
|
max_default_sequence = max(
|
||||||
|
[int(getattr(row, "sequence_no", 0) or 0) for row in defaults] + [0]
|
||||||
|
)
|
||||||
|
parking_base = max(max_existing_sequence, max_default_sequence, 0) + 100000
|
||||||
|
|
||||||
|
for offset, row in enumerate(sorted(firm_rows, key=lambda item: int(item.id)), start=1):
|
||||||
|
row.sequence_no = parking_base + offset
|
||||||
|
|
||||||
|
# This flush is intentional and must happen before any canonical row is
|
||||||
|
# assigned a system-default sequence number.
|
||||||
|
db.flush()
|
||||||
|
|
||||||
# First clean pre-existing duplicate groups, regardless of whether the task
|
# First clean pre-existing duplicate groups, regardless of whether the task
|
||||||
# remains in the current system defaults. A unique firm-only task is untouched.
|
# remains in the current system defaults. A unique firm-only task is untouched.
|
||||||
for candidates in by_name.values():
|
for candidates in by_name.values():
|
||||||
@@ -188,6 +218,10 @@ def sync_firm_tasks_from_system_defaults(
|
|||||||
for duplicate in candidates:
|
for duplicate in candidates:
|
||||||
if duplicate.id == canonical.id:
|
if duplicate.id == canonical.id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Duplicate rows are retained for historical FK safety but remain
|
||||||
|
# on their unique parked sequence number. The UNIQUE sequence
|
||||||
|
# constraint therefore remains satisfied even after deactivation.
|
||||||
if getattr(duplicate, "is_active", True):
|
if getattr(duplicate, "is_active", True):
|
||||||
duplicate.is_active = False
|
duplicate.is_active = False
|
||||||
if (
|
if (
|
||||||
@@ -221,28 +255,13 @@ def sync_firm_tasks_from_system_defaults(
|
|||||||
|
|
||||||
# No name match. A unique sequence match is a conservative fallback for a
|
# No name match. A unique sequence match is a conservative fallback for a
|
||||||
# system-default rename while still avoiding arbitrary replacement.
|
# system-default rename while still avoiding arbitrary replacement.
|
||||||
sequence_no = getattr(default, "sequence_no", None)
|
# Do NOT use the pre-sync sequence number as an identity fallback here.
|
||||||
sequence_candidates = [
|
# All existing rows have deliberately been parked on temporary sequence
|
||||||
row
|
# numbers to satisfy the DB unique constraint. More importantly,
|
||||||
for row in firm_rows
|
# sequence number is ordering metadata, not a stable task identity.
|
||||||
if getattr(row, "sequence_no", None) == sequence_no
|
# A renamed system-default task without a name match is therefore
|
||||||
and getattr(row, "is_active", True)
|
# treated as a new task rather than risking replacement of the wrong
|
||||||
]
|
# firm-specific task.
|
||||||
if len(sequence_candidates) == 1:
|
|
||||||
target = sequence_candidates[0]
|
|
||||||
changed = _copy_default_columns(default, target)
|
|
||||||
if (
|
|
||||||
updated_by_user_id is not None
|
|
||||||
and hasattr(target, "updated_by_user_id")
|
|
||||||
):
|
|
||||||
target.updated_by_user_id = updated_by_user_id
|
|
||||||
if changed:
|
|
||||||
result.updated += 1
|
|
||||||
else:
|
|
||||||
result.unchanged += 1
|
|
||||||
# Make subsequent defaults see the new name.
|
|
||||||
by_name[_normalise_name(getattr(target, "task_name", None))].append(target)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Missing task: construct from the intersection of mapped columns.
|
# Missing task: construct from the intersection of mapped columns.
|
||||||
source_cols = _column_names(ServiceDefaultTaskTemplate)
|
source_cols = _column_names(ServiceDefaultTaskTemplate)
|
||||||
|
|||||||
Reference in New Issue
Block a user