mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
fix(#918): RFID auto-match handles Quick-Add and rejects non-Bambu brands
`find_matching_untagged_spool` is supposed to attach an incoming Bambu
RFID UUID to a pre-existing manually-logged spool of the same
material/color so users who log inventory before scanning don't end up
with duplicate rows. Two bugs meant it almost never worked for the
actual reporting workflow:
1. Subtype filter was strict. AMS reports `tray_sub_brands="PLA Basic"`
→ matcher required `Spool.subtype = 'Basic'` exactly. The form's
Quick-Add mode only requires `material`, so bulk-logged rows have
`subtype=NULL` and were always excluded → duplicate on first AMS
read.
2. Brand wasn't filtered. The docstring claimed brand was matched but
the WHERE clause didn't include it, so a same-color Polymaker (or
any non-Bambu) untagged row could acquire a Bambu UUID — silent
data corruption.
Fix in the same query: subtype prefers exact match but accepts NULL as
fallback (CASE in ORDER BY ensures exact wins when both exist); brand
restricted to NULL or LOWER(brand) LIKE '%bambu%' (covers 'Bambu',
'Bambu Lab', 'BambuLab', 'bambu lab' — the spellings users actually
type).
6 regression tests added in test_spool_tag_matcher.py.
This commit is contained in:
@@ -690,6 +690,174 @@ async def test_find_matching_untagged_spool_relationships_loaded(db_session):
|
||||
assert _relationship_is_loaded(found, "assignments")
|
||||
|
||||
|
||||
# -- find_matching_untagged_spool: #918 regressions ------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_matching_untagged_spool_null_subtype_fallback(db_session):
|
||||
"""#918: Quick-Add spool (subtype=NULL) matches when AMS reports a subtype.
|
||||
|
||||
The form's Quick-Add mode only requires `material`, so bulk-logged spools
|
||||
have subtype=NULL. Before the fix, the strict `subtype = 'Basic'` filter
|
||||
excluded these rows and the system created duplicates on first AMS read.
|
||||
"""
|
||||
spool = Spool(
|
||||
material="PLA",
|
||||
subtype=None, # Quick-Add bulk entry
|
||||
rgba="FFFFFFFF",
|
||||
brand="Bambu Lab",
|
||||
label_weight=1000,
|
||||
core_weight=250,
|
||||
)
|
||||
db_session.add(spool)
|
||||
await db_session.commit()
|
||||
|
||||
# Tray reports "PLA Basic" → subtype parsed as "Basic"
|
||||
found = await find_matching_untagged_spool(db_session, SAMPLE_TRAY)
|
||||
assert found is not None
|
||||
assert found.id == spool.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_matching_untagged_spool_prefers_exact_subtype_over_null(db_session):
|
||||
"""#918: When both an exact-subtype and a NULL-subtype row match, exact wins.
|
||||
|
||||
The NULL fallback exists only as a backstop for Quick-Add bulk-logged
|
||||
spools — if the user did the work to record subtype="Basic", it must
|
||||
take precedence over a vague "PLA" record, even if the latter is older.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
null_spool = Spool(
|
||||
material="PLA",
|
||||
subtype=None, # Older but vague — should NOT win
|
||||
rgba="FFFFFFFF",
|
||||
brand="Bambu Lab",
|
||||
label_weight=1000,
|
||||
core_weight=250,
|
||||
)
|
||||
db_session.add(null_spool)
|
||||
await db_session.flush()
|
||||
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
exact_spool = Spool(
|
||||
material="PLA",
|
||||
subtype="Basic", # Newer but specific — should win
|
||||
rgba="FFFFFFFF",
|
||||
brand="Bambu Lab",
|
||||
label_weight=1000,
|
||||
core_weight=250,
|
||||
)
|
||||
db_session.add(exact_spool)
|
||||
await db_session.commit()
|
||||
|
||||
found = await find_matching_untagged_spool(db_session, SAMPLE_TRAY)
|
||||
assert found is not None
|
||||
assert found.id == exact_spool.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_matching_untagged_spool_rejects_non_bambu_brand(db_session):
|
||||
"""#918: A same-color non-Bambu spool must NOT attract a Bambu UUID.
|
||||
|
||||
Without the brand filter, a Polymaker untagged spool of matching
|
||||
material/color would silently acquire a Bambu RFID UUID, leaving the
|
||||
user with brand="Polymaker" but a Bambu Lab tray UUID — corrupt data.
|
||||
"""
|
||||
spool = Spool(
|
||||
material="PLA",
|
||||
subtype="Basic",
|
||||
rgba="FFFFFFFF",
|
||||
brand="Polymaker", # NOT Bambu — must be rejected
|
||||
label_weight=1000,
|
||||
core_weight=250,
|
||||
)
|
||||
db_session.add(spool)
|
||||
await db_session.commit()
|
||||
|
||||
found = await find_matching_untagged_spool(db_session, SAMPLE_TRAY)
|
||||
assert found is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_matching_untagged_spool_accepts_null_brand(db_session):
|
||||
"""#918: Quick-Add spools with brand=NULL still match a Bambu RFID read.
|
||||
|
||||
Quick-Add doesn't require brand, so a user bulk-logging Bambu spools may
|
||||
leave it empty. The matcher allows NULL brand because the alternative
|
||||
(forcing every Quick-Add spool to be tagged "Bambu") is the exact
|
||||
friction the auto-matcher exists to remove.
|
||||
"""
|
||||
spool = Spool(
|
||||
material="PLA",
|
||||
subtype="Basic",
|
||||
rgba="FFFFFFFF",
|
||||
brand=None, # Quick-Add left brand blank
|
||||
label_weight=1000,
|
||||
core_weight=250,
|
||||
)
|
||||
db_session.add(spool)
|
||||
await db_session.commit()
|
||||
|
||||
found = await find_matching_untagged_spool(db_session, SAMPLE_TRAY)
|
||||
assert found is not None
|
||||
assert found.id == spool.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_matching_untagged_spool_accepts_bambu_brand_variants(db_session):
|
||||
"""#918: Both 'Bambu' (form dropdown) and 'Bambu Lab' (catalog) match.
|
||||
|
||||
DEFAULT_BRANDS in the form lists 'Bambu'; the catalog uses 'Bambu Lab'.
|
||||
Users can pick either. The fuzzy %bambu% LIKE handles both, plus
|
||||
'BambuLab', 'bambu lab', etc.
|
||||
"""
|
||||
for brand_value in ("Bambu", "Bambu Lab", "BambuLab", "bambu lab"):
|
||||
spool = Spool(
|
||||
material="PLA",
|
||||
subtype="Basic",
|
||||
rgba="FFFFFFFF",
|
||||
brand=brand_value,
|
||||
label_weight=1000,
|
||||
core_weight=250,
|
||||
)
|
||||
db_session.add(spool)
|
||||
await db_session.commit()
|
||||
|
||||
found = await find_matching_untagged_spool(db_session, SAMPLE_TRAY)
|
||||
assert found is not None, f"brand={brand_value!r} should match"
|
||||
assert found.id == spool.id
|
||||
|
||||
# Clean up so the next iteration starts fresh.
|
||||
await db_session.delete(spool)
|
||||
await db_session.commit()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_matching_untagged_spool_null_subtype_with_null_brand(db_session):
|
||||
"""#918: Pure Quick-Add row (brand=NULL, subtype=NULL) matches.
|
||||
|
||||
This is the exact scenario from Arn0uDz's report: 20 spools logged via
|
||||
Quick Add, then placed in the AMS one at a time. Before the fix every
|
||||
insertion duplicated; after the fix the first matching row is reused.
|
||||
"""
|
||||
spool = Spool(
|
||||
material="PLA",
|
||||
subtype=None,
|
||||
rgba="FFFFFFFF",
|
||||
brand=None,
|
||||
label_weight=1000,
|
||||
core_weight=250,
|
||||
)
|
||||
db_session.add(spool)
|
||||
await db_session.commit()
|
||||
|
||||
found = await find_matching_untagged_spool(db_session, SAMPLE_TRAY)
|
||||
assert found is not None
|
||||
assert found.id == spool.id
|
||||
|
||||
|
||||
# -- link_tag_to_inventory_spool -------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user