Sync with imported parts now properly updates augmented parts and creates any new attachment points
This commit is contained in:
1 parent
d23f5b9e82
commit
13b64a2d17
6 files changed
+272
-8
No files matched your search
@@ -9,7 +9,24 @@ Phase 1 – archive stale entries
|
||||
remaining associations (i.e. it was created purely from now-deleted
|
||||
ImportedParts), preserving its fit/attachment-point history.
|
||||
|
||||
Phase 2 – create entries for new ImportedParts
|
||||
Phase 2 – refresh entries for already-linked ImportedParts
|
||||
• For each ImportedPart already linked to an AugmentedPart (from a prior
|
||||
sync run), refresh the fields sync itself owns — description, colour,
|
||||
and uac_diameter_range_mm (see field mapping below) — to match the
|
||||
ImportedPart's current values.
|
||||
• Never touches "extra"/admin-owned AugmentedPart fields: part_type_id,
|
||||
sub_part_type_id, scope_model_id, manufacturer_id, sku, archived. Those
|
||||
are only ever set once, at creation (phase 3) or via the admin API/UI.
|
||||
• Adds an "Ocular" and/or "Objective" attachment point if the part doesn't
|
||||
already have one of that name and the current ImportedPart data now
|
||||
supports deriving one. Never deletes or modifies an existing attachment
|
||||
point, even if the source data that produced it has since changed.
|
||||
• If an AugmentedPart has more than one backing ImportedPart, the one with
|
||||
the lexicographically smallest InventoryID is used as the source (this
|
||||
doesn't happen via any current creation path, but the data model and
|
||||
phase 1's archiving logic both allow for it).
|
||||
|
||||
Phase 3 – create entries for new ImportedParts
|
||||
• Any ImportedPart not yet linked to an AugmentedPart gets one created.
|
||||
• Description ← ImportedPart.Description (fallback: InventoryID)
|
||||
• Colour ← ImportedPart.TBXColour (fallback: "Unknown"; created if needed)
|
||||
@@ -40,6 +57,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.crud import attachment_point as attachment_point_crud
|
||||
from app.crud import attachment_point_name as attachment_point_name_crud
|
||||
from app.crud import augmented_part as augmented_part_crud
|
||||
from app.crud import clamp_connection as clamp_connection_crud
|
||||
from app.crud import iso_thread as iso_thread_crud
|
||||
from app.crud import manufacturer as manufacturer_crud
|
||||
@@ -408,6 +426,52 @@ async def delete_imported_part_and_archive_orphans(
|
||||
return archived
|
||||
|
||||
|
||||
async def _refresh_augmented_part(
|
||||
db: AsyncSession, ap: AugmentedPart, ip: ImportedPart
|
||||
) -> bool:
|
||||
"""Refresh the AugmentedPart fields sync owns (description, colour, UAC
|
||||
diameter range) from the current state of `ip`, and add an "Ocular"
|
||||
and/or "Objective" attachment point if the part doesn't already have one
|
||||
and the current data now supports deriving one.
|
||||
|
||||
Never touches "extra" AugmentedPart fields (part_type_id, sub_part_type_id,
|
||||
scope_model_id, manufacturer_id, sku, archived) and never deletes or
|
||||
replaces an attachment point that already exists. Returns True if
|
||||
anything changed.
|
||||
"""
|
||||
description = (ip.Description or "").strip() or ip.InventoryID
|
||||
|
||||
colour_name = (ip.TBXColour or "").strip() or "Unknown"
|
||||
colour = await part_colour_crud.get_by_name(db, colour_name)
|
||||
if colour is None:
|
||||
colour = await part_colour_crud.create(db, name=colour_name)
|
||||
|
||||
uac_range = _parse_uac_diameter_range(ip.UACDiameterRangemm)
|
||||
uac_value = uac_range[0] if uac_range else None
|
||||
|
||||
changes = {}
|
||||
if ap.description != description:
|
||||
changes["description"] = description
|
||||
if ap.colour_id != colour.id:
|
||||
changes["colour_id"] = colour.id
|
||||
if ap.uac_diameter_range_mm != uac_value:
|
||||
changes["uac_diameter_range_mm"] = uac_value
|
||||
if changes:
|
||||
await augmented_part_crud.update(db, ap, changes)
|
||||
|
||||
existing_points = await attachment_point_crud.list_for_augmented_part(db, ap.id)
|
||||
existing_names = {p.attachment_point_name.name for p in existing_points}
|
||||
points_before = len(existing_points)
|
||||
|
||||
if ATTACHMENT_POINT_NAME not in existing_names:
|
||||
await _add_attachment_point(db, ap, ip)
|
||||
if UAC_ATTACHMENT_POINT_NAME not in existing_names:
|
||||
await _add_uac_clamp_attachment_point(db, ap, ip)
|
||||
|
||||
points_after = await attachment_point_crud.list_for_augmented_part(db, ap.id)
|
||||
return bool(changes) or len(points_after) > points_before
|
||||
|
||||
|
||||
async def sync(db: AsyncSession) -> AugmentedPartSyncResult:
|
||||
# ------------------------------------------------------------------ phase 1
|
||||
|
||||
@@ -430,19 +494,45 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult:
|
||||
|
||||
# ------------------------------------------------------------------ phase 2
|
||||
|
||||
# Re-query linked IDs after phase 1 cleanup
|
||||
# Re-query linked IDs after phase 1 cleanup — these are the ImportedParts
|
||||
# already linked to an AugmentedPart from a prior run (not created below).
|
||||
result = await db.execute(
|
||||
select(augmented_part_imported_parts.c.imported_part_id).distinct()
|
||||
)
|
||||
now_linked_ids: set[str] = {row[0] for row in result.all()}
|
||||
unlinked_ids = current_ids - now_linked_ids
|
||||
|
||||
updated = 0
|
||||
|
||||
if now_linked_ids:
|
||||
result = await db.execute(
|
||||
select(
|
||||
augmented_part_imported_parts.c.augmented_part_id,
|
||||
augmented_part_imported_parts.c.imported_part_id,
|
||||
).where(
|
||||
augmented_part_imported_parts.c.imported_part_id.in_(now_linked_ids)
|
||||
)
|
||||
)
|
||||
ip_ids_by_ap_id: dict[int, list[str]] = {}
|
||||
for ap_id, ip_id in result.all():
|
||||
ip_ids_by_ap_id.setdefault(ap_id, []).append(ip_id)
|
||||
|
||||
for ap_id, ip_ids in ip_ids_by_ap_id.items():
|
||||
ap = await db.get(AugmentedPart, ap_id)
|
||||
ip = await db.get(ImportedPart, min(ip_ids))
|
||||
if ap is None or ip is None:
|
||||
continue
|
||||
if await _refresh_augmented_part(db, ap, ip):
|
||||
updated += 1
|
||||
|
||||
# ------------------------------------------------------------------ phase 3
|
||||
|
||||
created = 0
|
||||
skipped = 0
|
||||
|
||||
if not unlinked_ids:
|
||||
return AugmentedPartSyncResult(
|
||||
created=created, archived=archived, skipped=skipped
|
||||
created=created, updated=updated, archived=archived, skipped=skipped
|
||||
)
|
||||
|
||||
result = await db.execute(
|
||||
@@ -497,4 +587,6 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult:
|
||||
|
||||
await db.commit()
|
||||
|
||||
return AugmentedPartSyncResult(created=created, archived=archived, skipped=skipped)
|
||||
return AugmentedPartSyncResult(
|
||||
created=created, updated=updated, archived=archived, skipped=skipped
|
||||
)
|
||||
Reference in new issue
Block a user