Adding Manual connection point to allow undeclared fits

This commit is contained in:
Ian Roddis committed 2026-08-18 16:57:28 -03:00
1 parent 7e246be39c
commit b4dcb68bea
15 files changed
+227 -23

No files matched your search

+6 -1
View File
@@ -76,7 +76,12 @@ def _static_clamp_match(ap_a: AttachmentPoint, ap_b: AttachmentPoint) -> bool:
def attachment_points_match(ap_a: AttachmentPoint, ap_b: AttachmentPoint) -> bool:
"""True if these two attachment points satisfy the fit-matching algorithm."""
"""True if these two attachment points satisfy the fit-matching algorithm.
A "Manual" attachment point (all four connections null) always returns
False here — it never participates in algorithmic matching, only in
fits a user asserts by hand.
"""
# Disabled for now, the attachment_point_names should not be considered for the purposes of matching
# if ap_a.attachment_point_name_id == ap_b.attachment_point_name_id:
# return False
+55 -10
View File
@@ -27,9 +27,13 @@ Fits are created with label POSSIBLE. The referenced part is looked up by
``product_number`` and linked to its first attachment point (chosen answer:
"sole/first AP"). If the referenced part has no attachment points at all, one
is created for it that mirrors the scope's attachment point's connection, so
the fit has something valid to attach to. Both directions are resolved in a
second pass so a fit between two scopes in the same spreadsheet resolves
regardless of row order.
the fit has something valid to attach to. If the scope itself has no Ocular/
Objective attachment point (no ocular_outer_diameter_mm/objective_thread, or
an unparseable one), a Manual attachment point — no connection details, never
picked up by the fit-matching algorithm — is created on the scope under the
appropriate label so the fit still has somewhere to attach. Both directions
are resolved in a second pass so a fit between two scopes in the same
spreadsheet resolves regardless of row order.
Bad rows (missing key, unknown fit target, unparseable thread) are skipped and
reported per-row; good rows are committed as they go.
@@ -207,6 +211,36 @@ async def _mirrored_ap(db: AsyncSession, *, augmented_part_id: int, scope_ap):
)
async def _ensure_manual_scope_ap(
db: AsyncSession,
*,
scope_ap,
augmented_part_id: int,
label: str,
messages: list[str],
):
"""Return ``scope_ap``, creating a Manual attachment point named ``label``
on the scope if it's None.
Used so an ocular_fit/objective_fit reference still has something to link
from even when the scope has no ocular_outer_diameter_mm/objective_thread
of its own.
"""
if scope_ap is not None:
return scope_ap
apn = await apn_crud.get_by_name(db, label)
if apn is None:
apn = await apn_crud.create(db, name=label)
messages.append(
f"scope has no {label.lower()} attachment point; created a manual one to record the fit"
)
return await attachment_point_crud.create(
db,
augmented_part_id=augmented_part_id,
attachment_point_name_id=apn.id,
)
async def _link_fit(
db: AsyncSession,
*,
@@ -218,14 +252,11 @@ async def _link_fit(
) -> None:
"""Create a POSSIBLE fit from the scope's AP to the referenced part's first AP.
If the referenced part has no attachment points at all, one is created
mirroring the scope's attachment point's connection.
``scope_ap`` must already exist — callers ensure this via
``_ensure_manual_scope_ap`` before calling. If the referenced part has no
attachment points at all, one is created mirroring the scope's attachment
point's connection.
"""
if scope_ap is None:
messages.append(
f"{scope_role}_fit '{target_sku}' skipped: scope has no {scope_role} attachment point"
)
return
target = await augmented_part_crud.get_by_sku(db, target_sku)
if target is None:
messages.append(f"{scope_role}_fit references unknown product '{target_sku}'")
@@ -406,6 +437,13 @@ async def import_scopes(db: AsyncSession, rows: list[dict]) -> ScopeImportResult
try:
ocular_fit = _text(row.get("ocular_fit"))
if ocular_fit:
ocular_ap = await _ensure_manual_scope_ap(
db,
scope_ap=ocular_ap,
augmented_part_id=part_id,
label=OCULAR_LABEL,
messages=result.messages,
)
fits = ocular_fit
if "/" in fits:
fits = [x.strip() for x in fits.split("/")]
@@ -422,6 +460,13 @@ async def import_scopes(db: AsyncSession, rows: list[dict]) -> ScopeImportResult
)
objective_fit = _text(row.get("objective_fit"))
if objective_fit:
objective_ap = await _ensure_manual_scope_ap(
db,
scope_ap=objective_ap,
augmented_part_id=part_id,
label=OBJECTIVE_LABEL,
messages=result.messages,
)
fits = objective_fit
if "/" in fits:
fits = [x.strip() for x in fits.split("/")]