Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
"""BOM eligibility policy — refdes classes shared by every BOM surface.
A schematic's components are not one population: test points, fiducials,
mounting holes and logos are STRUCTURAL — they exist on the board but no
part is ever bought for them. Treating them like orderable lines produced
both noise (on a real board, most id-less "problems" were test points
and bare pads) and a
real incident (the suggestion engine offering an SMPS IC for a test point).
This module owns the classification; ``checks/bom.py`` (offline) and
``parts/bom_jlc.py`` (catalog) both consume it, and the prefix set is
project-configurable via ``akcli.toml`` ``[bom.classes].no_part``.
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .model import Component
__all__ = ["DEFAULT_NO_PART_PREFIXES", "CLASSES", "refdes_prefix",
"is_no_part_ref", "sourcing_of", "classify", "order_id_of"]
# The four BOM populations (SPEC BOM P1-2). Every surface — check --bom,
# jlc bom, the order CSV, totals — speaks this vocabulary.
CLASSES: tuple[str, ...] = ("fitted", "dnp", "external", "no-part")
# Refdes prefixes that never carry an orderable part (full alpha-prefix
# equality, so "H3" matches but a heatsink "HS1" does not).
DEFAULT_NO_PART_PREFIXES: tuple[str, ...] = ("TP", "FID", "MH", "H", "LOGO")
# The ONE refdes-prefix implementation (strips synthesized ``$`` — the copy
# that used to live here returned "" for ``$U7``, silently unclassifying
# every Altium part whose designator record was missing).
from .classify import refdes_prefix # noqa: E402
def is_no_part_ref(ref: str | None,
prefixes: tuple[str, ...] = DEFAULT_NO_PART_PREFIXES) -> bool:
"""True when ``ref``'s class is structural (no part expected)."""
p = refdes_prefix(ref)
return bool(p) and p in {x.upper() for x in prefixes}
# Recognized sourcing-channel parameter keys (case-insensitive).
_SOURCING_KEYS: tuple[str, ...] = ("Sourcing", "BOM_Sourcing")
# Explicit order-id parameter keys: an LCSC C-number or a manufacturer part
# number that an assembler can actually order by. (The offline coverage
# heuristic additionally accepts digit-bearing library refs — see
# checks/bom._part_identity — but a MISSING order id is judged on these.)
_ORDER_LCSC_KEYS: tuple[str, ...] = (
"LCSC Part", "LCSC Part Name", "LCSC", "LCSC#",
"JLCPCB Part", "JLCPCB", "JLC", "JLC#",
)
_ORDER_MPN_KEYS: tuple[str, ...] = (
"MPN", "Manufacturer Part", "Manufacturer Part Number", "Mfr. Part",
"Mfr Part", "Part Number", "Supplier Part",
)
def sourcing_of(comp: "Component") -> tuple[str, str]:
"""``(channel, note)`` from a ``Sourcing``/``BOM_Sourcing`` parameter.
Channels: ``lcsc`` (default), ``external`` (bought elsewhere; the value
may carry a note — ``external:vendor official store``), ``consigned``
(customer-supplied to the fab) and ``no-part``. Unknown values fall back
to ``lcsc`` so a typo can never silently drop a part from the order.
"""
params = comp.parameters or {}
raw = ""
for k, v in params.items():
if k.casefold() in {x.casefold() for x in _SOURCING_KEYS}:
raw = str(v or "").strip()
break
if not raw:
return "lcsc", ""
channel, _, note = raw.partition(":")
channel = channel.strip().casefold()
note = note.strip()
if channel in ("external", "consigned"):
return channel, note
if channel in ("no-part", "nopart", "none"):
return "no-part", note
return "lcsc", ""
def classify(comp: "Component",
prefixes: tuple[str, ...] = DEFAULT_NO_PART_PREFIXES,
) -> tuple[str, str]:
"""``(bom_class, note)`` — one of :data:`CLASSES` per component.
Precedence: structural facts first (``in_bom=no``, a structural refdes
class, or an explicit ``no-part`` sourcing), then ``dnp``, then the
sourcing channel, else ``fitted``. A DNP'd test point is still a test
point — no-part wins over dnp.
"""
channel, note = sourcing_of(comp)
if not comp.in_bom or is_no_part_ref(comp.designator, prefixes) \
or channel == "no-part":
return "no-part", note
if comp.dnp:
return "dnp", note
if channel in ("external", "consigned"):
return "external", (note or channel)
return "fitted", ""
_CNUM_RE = re.compile(r"^[Cc]?\d{2,}$")
def order_id_of(comp: "Component") -> str | None:
"""The explicit order id a fitted line needs (LCSC C-number or MPN)."""
params = comp.parameters or {}
lowered = {k.casefold(): str(v or "").strip() for k, v in params.items()}
for key in _ORDER_LCSC_KEYS:
v = lowered.get(key.casefold())
if v and _CNUM_RE.match(v):
return v if v.upper().startswith("C") else "C" + v
for key in _ORDER_MPN_KEYS:
v = lowered.get(key.casefold())
if v:
return v
return None
2126c3284This file is pinned to the selected release. Download the original to open it in your design tool.