Skip to content

feat(bom-export): attach a Supplier (JLCPCB by default) to PCB & SMT Stencil parts#33

Merged
peterus merged 8 commits into
mainfrom
feat/pcb-stencil-supplier-attachment
Jun 17, 2026
Merged

feat(bom-export): attach a Supplier (JLCPCB by default) to PCB & SMT Stencil parts#33
peterus merged 8 commits into
mainfrom
feat/pcb-stencil-supplier-attachment

Conversation

@peterus

@peterus peterus commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

bom_export.py's create_pcb_part and create_stencil_part currently 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 as f\"{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

  • Spec (docs/superpowers/specs/2026-06-11-pcb-supplier-attachment-design.md)
  • Plan (docs/superpowers/plans/2026-06-11-pcb-supplier-attachment.md)
  • Codescripts/bom_export.py:
    • New _ensure_fab_supplier_part(api, part, supplier, sku) helper (idempotent, defensive list-then-post-filter on SKU)
    • create_pcb_part / create_stencil_part accept keyword-only fab_supplier=None; call the helper after Part create OR reuse
    • New constant DEFAULT_PCB_SUPPLIER_NAME = \"JLCPCB\"
    • CLI flags --pcb-supplier NAME (default JLCPCB) and --no-pcb-supplier (opt-out)
    • main() resolves supplier via get_or_create_supplier only in the non-dry-run branch (dry-run contract preserved)
  • Testsscripts/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

  • Re-running the same release: existing Part found → existing SupplierPart found by SKU → no new write.
  • Re-running with --pcb-supplier=PCBWay: existing Part reused, new SupplierPart for PCBWay (different supplier).
  • Backfill old Parts (created before this PR): first call to modified create_pcb_part adds the SupplierPart on the reuse path.

Process artefacts

  • Spec brainstormed → plan written → 3 tasks executed via subagent-driven-development (sonnet implementer, atlas spec reviewer, audit code reviewer per task)
  • 1 per-task review fix on Task 1 (caplog assertions, broad-except comments, ASCII arrow, 2 edge tests)
  • Final cross-branch review caught and fixed the dry-run side-effect leak (get_or_create_supplier was 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

  • 225/225 unit tests pass (pytest scripts/tests/)
  • --help shows the new --pcb-supplier and --no-pcb-supplier flags
  • CLI tests unchanged (no regression in dry-run path)
  • Operator-side (post-merge): ad-hoc API run to backfill SupplierPart on the 8 existing v0.1 PCBs/Stencils (pks 1291/1293/1294/1296/1297/1299/1301/1303)

Backward compatibility

  • New default behavior: --pcb-supplier=JLCPCB IS active out of the box. This is intentional — the current no-supplier behavior is the bug.
  • Users without JLCPCB Company in their InvenTree: get_or_create_supplier auto-creates it on first non-dry-run.
  • Users who explicitly don't want fab linkage: --no-pcb-supplier opt-out preserves pre-PR behavior.
  • create_pcb_part / create_stencil_part add a keyword-only fab_supplier arg (default None) — no existing call site breaks.

Non-goals (deferred to future PRs)

  • ManufacturerPart linkage (bare-PCB substrate has no MPN worth tracking).
  • link / pricing / pack_quantity fields on the SupplierPart (no re-order planned near-term).
  • Multi-fab support (PCB from one fab, stencil from another).
  • Assembly-side supplier (JLCPCB-SMT-Assembly).

🤖 Generated with Claude Code

peterus and others added 7 commits June 17, 2026 20:58
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>
Copilot AI review requested due to automatic review settings June 17, 2026 19:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a SupplierPart for (part, supplier, SKU) using a derived SKU format.
  • Thread an optional fab_supplier through create_pcb_part / create_stencil_part and 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.

Comment thread scripts/bom_export.py
Comment on lines +646 to +656
# 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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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():

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@peterus peterus requested a review from Copilot June 17, 2026 19:24
- 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@peterus peterus merged commit 0ebcd04 into main Jun 17, 2026
2 checks passed
@peterus peterus deleted the feat/pcb-stencil-supplier-attachment branch June 17, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants