feat(bom-export): attach a Supplier (JLCPCB by default) to PCB & SMT Stencil parts#33
Conversation
bom_export.py's create_pcb_part and create_stencil_part currently
create the Part only, with no SupplierPart linking it to the fab
(JLCPCB in our case). After v0.1 import we have 4 PCBs + 4 stencils
without supplier metadata — re-order workflow breaks down at scale.
Fix: attach a SupplierPart with a deterministic SKU
({full_name} rev {version}) on both create and reuse branches. CLI
flag --pcb-supplier (default JLCPCB) selects the fab; --no-pcb-supplier
opts out. Idempotent via SKU-existence check. Fab linkage is best-
effort: a failure logs warning but never blocks the release CI.
Assembly Module stays without supplier (built in-house).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three TDD-strict tasks:
Task 1 — _ensure_fab_supplier_part helper (idempotent SP linkage)
Task 2 — create_pcb_part / create_stencil_part attach fab supplier
(both create and reuse branches; assembly stays unchanged)
Task 3 — CLI flags --pcb-supplier (default JLCPCB) and
--no-pcb-supplier; main() resolves supplier via
get_or_create_supplier and threads it through
Each step has concrete code, exact pytest commands, expected output,
and commit message ready to use.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Idempotently link a Part to a fab Supplier (JLCPCB by default) with a derived SKU. Same defensive list-then-post-filter pattern as ensure_supplier_parts in client.py. Failures during list / create are logged and swallowed — fab linkage is best-effort metadata, never blocks the release CI run. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add caplog warning assertions to the two failure-path tests so future refactors can't silently drop the operator's only signal. - Add inline "Broad except is intentional" comments above the two except blocks to pre-empt Copilot review noise. - Replace Unicode arrow with ASCII -> in the success log message for consistency with the rest of the codebase. - Add two new tests: multi-SP early-return on match (after a non-matching item), and tolerance for SKU=None on existing SP. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…pplier
Both create functions gain a keyword-only fab_supplier=None argument.
When non-None, _ensure_fab_supplier_part is called after Part create OR
reuse — the reuse path enables retroactive linkage by re-running
bom_export.py on releases that predate this change.
SKU is derived as f'{full_name} rev {version}', stable per design
revision, idempotent across re-runs.
create_assembly_part is intentionally NOT modified: the assembly is
built in-house, no external supplier applies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
main() resolves the fab supplier Company via get_or_create_supplier when --pcb-supplier is set (default: JLCPCB). The resolved Company is threaded into create_pcb_part and create_stencil_part as a keyword argument. create_assembly_part is intentionally not touched (assembly is built in-house). --no-pcb-supplier opts out: PCB+stencil Parts get no SupplierPart attached, matching pre-PR behaviour for users who don't want this. Dry-run path doesn't model SupplierPart decisions — the linkage is best-effort metadata, real-run logs cover the outcome. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…be supplier Move the get_or_create_supplier call from main()'s pre-dry-run section into the non-dry-run code path. The function calls Company.create when the named Company is missing, which violated the dry-run contract: `--dry-run --pcb-supplier=NewFab` would create a real Company in InvenTree despite the flag. Same bug class as PR #31 (--dry-run actually dry on Part resolution). New test test_dry_run_does_not_resolve_fab_supplier locks the behaviour in. Also tightened the dry-run code comment to make the absence of SupplierPart linkage in the report explicit and intentional (not a side-effect bug). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances scripts/bom_export.py so newly created (or reused) PCB and SMT Stencil InvenTree Parts are automatically linked to a fab via a SupplierPart (default supplier: JLCPCB, configurable via CLI). This unblocks standard InvenTree purchasing/re-order flows for PCBs/stencils by ensuring they have supplier metadata.
Changes:
- Add
_ensure_fab_supplier_part(...)helper to idempotently create aSupplierPartfor(part, supplier, SKU)using a derived SKU format. - Thread an optional
fab_supplierthroughcreate_pcb_part/create_stencil_partand add CLI flags--pcb-supplier/--no-pcb-supplier(while preserving dry-run’s “no writes” contract). - Add a focused unit test suite covering helper edge cases, create/reuse paths, and dry-run non-resolution of the supplier.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| scripts/bom_export.py | Adds fab-supplier SupplierPart linkage helper, CLI options, and wiring into PCB/stencil part creation without impacting assembly behavior. |
| scripts/tests/test_bom_export_fab_supplier.py | Adds unit tests for supplier-part idempotency, error handling, create/reuse behavior, and dry-run guarantees. |
| docs/superpowers/specs/2026-06-11-pcb-supplier-attachment-design.md | Captures the design/specification for attaching a fab supplier to PCB/stencil parts. |
| docs/superpowers/plans/2026-06-11-pcb-supplier-attachment.md | Provides the implementation plan and test strategy for the change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Resolve fab supplier now (not before the dry-run gate): get_or_create_supplier | ||
| # calls Company.create when the named Company is missing, which would violate | ||
| # the dry-run contract — same bug class as PR #31. | ||
| fab_supplier: Optional[Company] = None | ||
| if args.pcb_supplier is not None: | ||
| fab_supplier = get_or_create_supplier(api, name=args.pcb_supplier) | ||
| if fab_supplier is None: | ||
| log.error( | ||
| "Could not get or create fab supplier %r — proceeding " | ||
| "without SupplierPart linkage.", args.pcb_supplier) | ||
|
|
There was a problem hiding this comment.
Fixed. main() now normalises args.pcb_supplier via (args.pcb_supplier or "").strip() or None — empty/whitespace values are treated as opt-out, equivalent to --no-pcb-supplier. New parametrized test test_blank_pcb_supplier_treated_as_opt_out covers "", " ", "\t", "\n".
| ) | ||
|
|
||
|
|
||
| def test_ensure_fab_supplier_part_returns_early_on_first_match(): |
There was a problem hiding this comment.
Fixed. Renamed to test_ensure_fab_supplier_part_skips_create_when_any_existing_sku_matches. The matching SKU is the second item in the list, so the old name _returns_early_on_first_match was actively misleading.
- 3430855521 (important): --pcb-supplier with empty/whitespace value would have POSTed a Company with a blank name. Normalise in main() via `(args.pcb_supplier or "").strip() or None` and treat as opt-out (equivalent to --no-pcb-supplier). New parametrized test covers "", " ", "\t", "\n". - 3430855563 (minor): rename test_ensure_fab_supplier_part_returns_ early_on_first_match → _skips_create_when_any_existing_sku_matches. The matching SKU was the second item in the list, not the first; the old name read as a contradiction. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
bom_export.py'screate_pcb_partandcreate_stencil_partcurrently create the InvenTree Part only — no SupplierPart linking them to the fab. Re-order workflows in InvenTree break down because you can't add the PCB/Stencil to a JLCPCB PurchaseOrder via the normal flow.This PR attaches a
SupplierPart(default supplier: JLCPCB, configurable via--pcb-supplier) to every newly-created PCB and SMT Stencil Part. SKU is derived asf\"{full_name} rev {version}\", idempotent across re-runs. The Assembly Module is intentionally not touched (built in-house).Trigger
After the recent v0.1 bulk-import we ended up with 4 PCBs (pks 1291/1294/1297/1301) and 4 Stencils (pks 1293/1296/1299/1303) with no SupplierPart. JLCPCB exists as Company pk=381 in InvenTree, just wasn't linked. Backfill of those 8 records will happen as a separate ad-hoc API run after merge.
What's in the diff
docs/superpowers/specs/2026-06-11-pcb-supplier-attachment-design.md)docs/superpowers/plans/2026-06-11-pcb-supplier-attachment.md)scripts/bom_export.py:_ensure_fab_supplier_part(api, part, supplier, sku)helper (idempotent, defensive list-then-post-filter on SKU)create_pcb_part/create_stencil_partaccept keyword-onlyfab_supplier=None; call the helper after Part create OR reuseDEFAULT_PCB_SUPPLIER_NAME = \"JLCPCB\"--pcb-supplier NAME(default JLCPCB) and--no-pcb-supplier(opt-out)main()resolves supplier viaget_or_create_supplieronly in the non-dry-run branch (dry-run contract preserved)scripts/tests/test_bom_export_fab_supplier.py(new): 13 unit tests covering the helper edge cases, create-function behavior on new+reuse+no-supplier paths, assembly-not-touched, and the dry-run no-probe guarantee.Idempotency
--pcb-supplier=PCBWay: existing Part reused, new SupplierPart for PCBWay (different supplier).create_pcb_partadds the SupplierPart on the reuse path.Process artefacts
subagent-driven-development(sonnet implementer, atlas spec reviewer, audit code reviewer per task)get_or_create_supplierwas called before the dry-run gate — same bug class as PR fix(import-orders): make --dry-run actually dry on Part resolution #31, fixed by moving the resolution into the non-dry-run branch and adding a test that locks it in)Test plan
pytest scripts/tests/)--helpshows the new--pcb-supplierand--no-pcb-supplierflagsBackward compatibility
--pcb-supplier=JLCPCBIS active out of the box. This is intentional — the current no-supplier behavior is the bug.get_or_create_supplierauto-creates it on first non-dry-run.--no-pcb-supplieropt-out preserves pre-PR behavior.create_pcb_part/create_stencil_partadd a keyword-onlyfab_supplierarg (default None) — no existing call site breaks.Non-goals (deferred to future PRs)
link/pricing/pack_quantityfields on the SupplierPart (no re-order planned near-term).🤖 Generated with Claude Code