diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index 0cb7a3e..68a29af 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -5,33 +5,34 @@ from dataclasses import dataclass from enum import Enum from typing import List, Optional, Union +import tabulate as tabulate_module + from wireviz.wv_utils import html_line_breaks -BOM_HASH_FIELDS = "description unit partnumbers" +BOM_HASH_FIELDS = "description qty_unit amount partnumbers" + + +BomEntry = namedtuple("BomEntry", "category qty designators") BomHash = namedtuple("BomHash", BOM_HASH_FIELDS) BomHashList = namedtuple("BomHashList", BOM_HASH_FIELDS) +PartNumberInfo = namedtuple("PartNumberInfo", "pn manufacturer mpn supplier spn") + BomCategory = Enum( "BomEntry", "CONNECTOR CABLE WIRE ADDITIONAL_INSIDE ADDITIONAL_OUTSIDE" ) - -PartNumberInfo = namedtuple("PartNumberInfo", "pn manufacturer mpn supplier spn") +QtyMultiplierConnector = Enum( + "QtyMultiplierConnector", "ONE PINCOUNT POPULATED CONNECTIONS" +) +QtyMultiplierCable = Enum( + "QtyMultiplierCable", "ONE WIRECOUNT TERMINATION LENGTH TOTAL_LENGTH" +) PART_NUMBER_HEADERS = PartNumberInfo( pn="P/N", manufacturer=None, mpn="MPN", supplier=None, spn="SPN" ) -@dataclass -class BomEntry: - hash: BomHash # includes description, part number info, - description: str - qty: Union[int, float] - unit: str - designators: List[str] - _category: BomCategory # for sorting - - def partnumbers_to_list(partnumbers: PartNumberInfo) -> List[str]: cell_contents = [ pn_info_string(PART_NUMBER_HEADERS.pn, None, partnumbers.pn), @@ -55,3 +56,35 @@ def pn_info_string( return f'{name if name else header}{": " + number if number else ""}' else: return None + + +def print_bom_debug(bom): + headers = "# qty unit description amount unit designators category".split(" ") + rows = [] + rows.append(headers) + # fill rows + for hash, entry in bom.items(): + cells = [ + 0, + entry["qty"], + hash.qty_unit, + hash.description, + hash.amount.number if hash.amount else None, + hash.amount.unit if hash.amount else None, + ", ".join(sorted(entry["designators"])), + entry["category"], + ] + rows.append(cells) + # remove empty columns + transposed = list(map(list, zip(*rows))) + transposed = [ + column + for column in transposed + if any([cell is not None for cell in column[1:]]) + # ^ ignore header cell in check + ] + rows = list(map(list, zip(*transposed))) + # output + print() + print(tabulate_module.tabulate(rows, headers="firstrow")) + print() diff --git a/src/wireviz/wv_dataclasses.py b/src/wireviz/wv_dataclasses.py index 6a79e26..46bca0f 100644 --- a/src/wireviz/wv_dataclasses.py +++ b/src/wireviz/wv_dataclasses.py @@ -6,7 +6,13 @@ from enum import Enum from itertools import zip_longest from typing import Dict, List, Optional, Tuple, Union -from wireviz.wv_bom import BomHash, BomHashList, PartNumberInfo +from wireviz.wv_bom import ( + BomHash, + BomHashList, + PartNumberInfo, + QtyMultiplierCable, + QtyMultiplierConnector, +) from wireviz.wv_colors import ( COLOR_CODES, ColorOutputMode, @@ -27,10 +33,6 @@ MultilineHypertext = ( Designator = PlainText # Case insensitive unique name of connector or cable # Literal type aliases below are commented to avoid requiring python 3.8 -ConnectorMultiplier = PlainText # = Literal['pincount', 'populated'] -CableMultiplier = ( - PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length'] -) ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both'] # Type combinations @@ -52,6 +54,7 @@ MetadataKeys = PlainText # Literal['title', 'description', 'notes', ...] Side = Enum("Side", "LEFT RIGHT") ArrowDirection = Enum("ArrowDirection", "NONE BACK FORWARD BOTH") ArrowWeight = Enum("ArrowWeight", "SINGLE DOUBLE") +NumberAndUnit = namedtuple("NumberAndUnit", "number unit") AUTOGENERATED_PREFIX = "AUTOGENERATED_" @@ -154,6 +157,7 @@ class PinClass: label: str color: MultiColor parent: str # designator of parent connector + _num_connections = 0 # incremented in Connector.connect() _anonymous: bool = False # true for pins on autogenerated connectors _simple: bool = False # true for simple connector @@ -166,33 +170,6 @@ class PinClass: return ":".join([snip for snip in snippets if snip != ""]) -@dataclass -class WireClass: - index: int - id: str - label: str - color: MultiColor - parent: str # designator of parent cable/bundle - # gauge: Gauge - # pn: str - # manufacturer: str - # mpn: str - # supplier: str - # spn: str - - -@dataclass -class ShieldClass(WireClass): - pass # TODO, for wires with multiple shields more shield details, ... - - -@dataclass -class Connection: - from_: PinClass = None - via: Union[WireClass, ShieldClass] = None - to: PinClass = None - - @dataclass class Component: category: Optional[str] = None # currently only used by cables, to define bundles @@ -208,7 +185,10 @@ class Component: mpn: str = None supplier: str = None spn: str = None - + # BOM info + qty: NumberAndUnit = NumberAndUnit(1, None) + amount: Optional[NumberAndUnit] = None + sum_amounts_in_bom: bool = True ignore_in_bom: bool = False bom_id: Optional[str] = None # to be filled after harness is built @@ -218,42 +198,71 @@ class Component: partnos = tuple(partnos) self.partnumbers = PartNumberInfo(*partnos) + def parse_number_and_unit( + self, + inp: Optional[Union[NumberAndUnit, float, int, str]], + default_unit: Optional[str] = None, + ) -> Optional[NumberAndUnit]: + if inp is None: + return None + elif isinstance(inp, NumberAndUnit): + return inp + elif isinstance(inp, float) or isinstance(inp, int): + return NumberAndUnit(float(inp), default_unit) + elif isinstance(inp, str): + if " " in inp: + number, unit = inp.split(" ", 1) + else: + number, unit = inp, default_unit + try: + number = float(number) + except ValueError: + raise Exception( + f"{inp} is not a valid number and unit.\n" + "It must be a number, or a number and unit separated by a space." + ) + else: + return NumberAndUnit(number, unit) + @property def bom_hash(self) -> BomHash: - def _force_list(inp): - if isinstance(inp, list): - return inp - else: - return [inp for i in range(len(self.colors))] - - if self.category == "bundle": - # create a temporary single item that includes the necessary fields, - # which may or may not be lists - _hash_list = BomHashList( - self.description, - self.unit, - self.partnumbers, + if self.sum_amounts_in_bom: + _hash = BomHash( + description=self.description, + qty_unit=self.amount.unit if self.amount else None, + amount=None, + partnumbers=self.partnumbers, ) - # convert elements that are not lists, into lists - _hash_matrix = list(map(_force_list, [elem for elem in _hash_list])) - # transpose list of lists, convert to tuple for next step - _hash_matrix = list(map(tuple, zip(*_hash_matrix))) - # generate list of BomHashes - hash_list = [BomHash(*item) for item in _hash_matrix] - return hash_list else: - return BomHash( - self.description, - self.unit, - self.partnumbers, + _hash = BomHash( + description=self.description, + qty_unit=self.qty.unit, + amount=self.amount, + partnumbers=self.partnumbers, ) + return _hash + + @property + def bom_qty(self) -> float: + if self.sum_amounts_in_bom: + if self.amount: + return self.qty.number * self.amount.number + else: + return self.qty.number + else: + return self.qty.number + + def bom_amount(self) -> NumberAndUnit: + if self.sum_amounts_in_bom: + return NumberAndUnit(None, None) + else: + return self.amount @dataclass class AdditionalComponent(Component): - qty: float = 1 - unit: Optional[str] = None - qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = 1 + qty_multiplier: Union[QtyMultiplierConnector, QtyMultiplierCable, int] = 1 + qty_multipliers_computed: Dict = field(default_factory=list) designators: Optional[str] = None # used for components definedi in the # additional_bom_items section within another component bgcolor: SingleColor = None # ^ same here @@ -261,9 +270,22 @@ class AdditionalComponent(Component): def __post_init__(self): super().fill_partnumbers() self.bgcolor = SingleColor(self.bgcolor) + self.qty = self.parse_number_and_unit(self.qty, None) + self.amount = self.parse_number_and_unit(self.amount, None) + + if isinstance(self.qty_multiplier, float) or isinstance(self.qty_multiplier, int): + pass + else: + self.qty_multiplier = self.qty_multiplier.upper() + if self.qty_multiplier in QtyMultiplierConnector.__members__.keys(): + self.qty_multiplier = QtyMultiplierConnector[self.qty_multiplier] + elif self.qty_multiplier in QtyMultiplierCable.__members__.keys(): + self.qty_multiplier = QtyMultiplierCable[self.qty_multiplier] + else: + raise Exception(f"Unknown qty multiplier: {self.qty_multiplier}") @property - def qty_final(self): + def bom_qty(self): return 999 @property @@ -285,6 +307,8 @@ class TopLevelGraphicalComponent(GraphicalComponent): # abstract class image: Optional[Image] = None additional_components: List[AdditionalComponent] = field(default_factory=list) notes: Optional[MultilineHypertext] = None + # BOM options + add_up_in_bom: Optional[bool] = None # rendering options bgcolor_title: Optional[SingleColor] = None show_name: Optional[bool] = None @@ -338,6 +362,10 @@ class Connector(TopLevelGraphicalComponent): self.bgcolor_title = SingleColor(self.bgcolor_title) self.color = SingleColor(self.color) + # connectors do not support custom qty or amount + self.qty = NumberAndUnit(1, None) + self.amount = None + if isinstance(self.image, dict): self.image = Image(**self.image) @@ -431,27 +459,106 @@ class Connector(TopLevelGraphicalComponent): elif side == Side.RIGHT: self.ports_right = True - def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int: - # TODO!!! how and when to compute final qty for additional components??? - if not qty_multiplier: - return 1 - elif qty_multiplier == "pincount": - return self.pincount - elif qty_multiplier == "populated": - return sum(self.visible_pins.values()) - else: - raise ValueError( - f"invalid qty multiplier parameter for connector {qty_multiplier}" + def compute_qty_multipliers(self): + for subitem in self.additional_components: + populated_pins = [] + subitem.qty_multipliers_computed["ONE"] = 1 + subitem.qty_multipliers_computed["PINCOUNT"] = self.pincount + subitem.qty_multipliers_computed["POPULATED"] = 999 + subitem.qty_multipliers_computed["CONNECTIONS"] = 999 + + # QtyMultiplierConnector = Enum( + # "QtyMultiplierConnector", "ONE PINCOUNT POPULATED CONNECTIONS" + # ) + # QtyMultiplierCable = Enum( + # "QtyMultiplierCable", "ONE WIRECOUNT TERMINATION LENGTH TOTAL_LENGTH" + # ) + # def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int: + # # TODO!!! how and when to compute final qty for additional components??? + # if not qty_multiplier: + # return 1 + # elif qty_multiplier == "pincount": + # return self.pincount + # elif qty_multiplier == "populated": + # return sum(self.visible_pins.values()) + # else: + # raise ValueError( + # f"invalid qty multiplier parameter for connector {qty_multiplier}" + # ) + + +@dataclass +class WireClass: + parent: str # designator of parent cable/bundle + # wire-specific properties + index: int + id: str + label: str + color: MultiColor + # inheritable from parent cable + type: Union[MultilineHypertext, List[MultilineHypertext]] = None + subtype: Union[MultilineHypertext, List[MultilineHypertext]] = None + gauge: Optional[NumberAndUnit] = None + length: Optional[NumberAndUnit] = None + sum_amounts_in_bom: bool = True + partnumbers: PartNumberInfo = None + + @property + def bom_hash(self) -> BomHash: + if self.sum_amounts_in_bom: + _hash = BomHash( + description=self.description, + qty_unit=self.length.unit if self.length else None, + amount=None, + partnumbers=self.partnumbers, ) + else: + _hash = BomHash( + description=self.description, + qty_unit=1, + amount=self.length, + partnumbers=self.partnumbers, + ) + return _hash + + @property + def gauge_str(self): + if not self.gauge: + return None + actual_gauge = f"{self.gauge.number} {self.gauge.unit}" + actual_gauge = actual_gauge.replace("mm2", "mm\u00B2") + return actual_gauge + + @property + def description(self) -> str: + substrs = [ + "Wire", + self.type, + self.subtype, + self.gauge_str, + str(self.color) if self.color else None, + ] + desc = ", ".join([s for s in substrs if s is not None and s != ""]) + return desc + + +@dataclass +class ShieldClass(WireClass): + pass # TODO, for wires with multiple shields more shield details, ... + + +@dataclass +class Connection: + from_: PinClass = None + via: Union[WireClass, ShieldClass] = None + to: PinClass = None @dataclass class Cable(TopLevelGraphicalComponent): # cable-specific properties - gauge: Optional[float] = None - gauge_unit: Optional[str] = None - length: float = 0 - length_unit: Optional[str] = None + gauge: Optional[NumberAndUnit] = None + length: Optional[NumberAndUnit] = None color_code: Optional[str] = None # wire information in particular wirecount: Optional[int] = None @@ -460,7 +567,7 @@ class Cable(TopLevelGraphicalComponent): wirelabels: List[Wire] = field(default_factory=list) # legacy wire_objects: List[WireClass] = field( default_factory=list - ) # new, to replace the lists above + ) # to replace the lists above # internal _connections: List[Connection] = field(default_factory=list) # rendering options @@ -475,49 +582,57 @@ class Cable(TopLevelGraphicalComponent): @property def unit(self): # for compatibility with parent class - return self.length_unit + return self.length @property def gauge_str(self): if not self.gauge: return None - actual_gauge = f"{self.gauge} {self.gauge_unit}" + actual_gauge = f"{self.gauge.number} {self.gauge.unit}" + actual_gauge = actual_gauge.replace("mm2", "mm\u00B2") + return actual_gauge + + @property + def gauge_str_with_equiv(self): + if not self.gauge: + return None + actual_gauge = self.gauge_str equivalent_gauge = "" if self.show_equiv: - # Only convert units we actually know about, i.e. currently - # mm2 and awg --- other units _are_ technically allowed, - # and passed through as-is. - if self.gauge_unit == "mm\u00B2": + # convert unit if known + if self.gauge.unit == "mm2": equivalent_gauge = f" ({awg_equiv(self.gauge)} AWG)" - elif self.gauge_unit.upper() == "AWG": - equivalent_gauge = f" ({mm2_equiv(self.gauge)} mm\u00B2)" - return f"{actual_gauge}{equivalent_gauge}" + elif self.gauge.unit.upper() == "AWG": + equivalent_gauge = f" ({mm2_equiv(self.gauge)} mm2)" + out = f"{actual_gauge}{equivalent_gauge}" + out = out.replace("mm2", "mm\u00B2") + return out + + @property + def length_str(self): + if not self.length: + return None + out = f"{self.length.number} {self.length.unit}" + return out + + @property + def bom_hash(self): + if self.category == "bundle": + raise Exception("Do this at the wire level!") # TODO + else: + return super().bom_hash @property def description(self) -> str: if self.category == "bundle": - desc_list = [] - for index, color in enumerate(self.colors): - substrs = [ - "Wire", - self.type, - self.subtype, - f"{self.gauge} {self.gauge_unit}" if self.gauge else None, - str(self.color) - if self.color - else None, # translate_color(self.color, harness.options.color_mode)] <- get harness.color_mode! - ] - desc_list.append( - ", ".join([s for s in substrs if s is not None and s != ""]) - ) - return desc_list + raise Exception("Do this at the wire level!") # TODO else: substrs = [ ("", "Cable"), (", ", self.type), (", ", self.subtype), (", ", self.wirecount), - (" ", f"x {self.gauge} {self.gauge_unit}" if self.gauge else " wires"), + (" ", f"x {self.gauge_str}" if self.gauge else "wires"), (" ", "shielded" if self.shield else None), (", ", str(self.color) if self.color else None), ] @@ -537,52 +652,9 @@ class Cable(TopLevelGraphicalComponent): if isinstance(self.image, dict): self.image = Image(**self.image) - if isinstance(self.gauge, str): # gauge and unit specified - try: - g, u = self.gauge.split(" ") - except Exception: - raise Exception( - f"Cable {self.designator} gauge={self.gauge} - " - "Gauge must be a number, or number and unit separated by a space" - ) - self.gauge = g - - if self.gauge_unit is not None: - print( - f"Warning: Cable {self.designator} gauge_unit={self.gauge_unit} " - f"is ignored because its gauge contains {u}" - ) - if u.upper() == "AWG": - self.gauge_unit = u.upper() - else: - self.gauge_unit = u.replace("mm2", "mm\u00B2") - - elif self.gauge is not None: # gauge specified, assume mm2 - if self.gauge_unit is None: - self.gauge_unit = "mm\u00B2" - else: - pass # gauge not specified - - if isinstance(self.length, str): # length and unit specified - try: - L, u = self.length.split(" ") - L = float(L) - except Exception: - raise Exception( - f"Cable {self.designator} length={self.length} - " - "Length must be a number, or number and unit separated by a space" - ) - self.length = L - if self.length_unit is not None: - print( - f"Warning: Cable {self.designator} length_unit={self.length_unit} is ignored " - f"because its length contains {u}" - ) - self.length_unit = u - elif not any(isinstance(self.length, t) for t in [int, float]): - raise Exception(f"Cable {self.designator} length has a non-numeric value") - elif self.length_unit is None: - self.length_unit = "m" + self.gauge = self.parse_number_and_unit(self.gauge, "mm2") + self.length = self.parse_number_and_unit(self.length, "m") + self.amount = self.length # for BOM if self.wirecount: # number of wires explicitly defined if self.colors: # use custom color palette (partly or looped if needed) @@ -634,11 +706,19 @@ class Cable(TopLevelGraphicalComponent): for wire_index, (wire_color, wire_label) in enumerate(wire_tuples): self.wire_objects.append( WireClass( + parent=self.designator, + # wire-specific properties index=wire_index, # TODO: wire_id id=wire_index + 1, # TODO: wire_id label=wire_label, color=MultiColor(wire_color), - parent=self.designator, + # inheritable from parent cable + type=self.type, + subtype=self.subtype, + gauge=self.gauge, + length=self.length, + sum_amounts_in_bom=self.sum_amounts_in_bom, + # TODO partnumbers ) ) @@ -685,21 +765,22 @@ class Cable(TopLevelGraphicalComponent): via_wire_obj = self.get_wire_by_id(via_wire_id) self._connections.append(Connection(from_pin_obj, via_wire_obj, to_pin_obj)) - def get_qty_multiplier(self, qty_multiplier: Optional[CableMultiplier]) -> float: - if not qty_multiplier: - return 1 - elif qty_multiplier == "wirecount": - return self.wirecount - elif qty_multiplier == "terminations": - return len(self.connections) - elif qty_multiplier == "length": - return self.length - elif qty_multiplier == "total_length": - return self.length * self.wirecount - else: - raise ValueError( - f"invalid qty multiplier parameter for cable {qty_multiplier}" - ) + + # def get_qty_multiplier(self, qty_multiplier: Optional[CableMultiplier]) -> float: + # if not qty_multiplier: + # return 1 + # elif qty_multiplier == "wirecount": + # return self.wirecount + # elif qty_multiplier == "terminations": + # return len(self.connections) + # elif qty_multiplier == "length": + # return self.length + # elif qty_multiplier == "total_length": + # return self.length * self.wirecount + # else: + # raise ValueError( + # f"invalid qty multiplier parameter for cable {qty_multiplier}" + # ) @dataclass diff --git a/src/wireviz/wv_graphviz.py b/src/wireviz/wv_graphviz.py index 93ff237..2dcb716 100644 --- a/src/wireviz/wv_graphviz.py +++ b/src/wireviz/wv_graphviz.py @@ -54,11 +54,9 @@ def gv_node_component(component: Component) -> Table: line_info = [ html_line_breaks(component.type), f"{component.wirecount}x" if component.show_wirecount else None, - f"{component.gauge_str}" if component.gauge else None, + component.gauge_str_with_equiv, "+ S" if component.shield else None, - f"{component.length} {component.length_unit}" - if component.length > 0 - else None, + component.length_str, str(component.color) if component.color else None, colorbar_cell(component.color) if component.color else None, ] diff --git a/src/wireviz/wv_harness.py b/src/wireviz/wv_harness.py index 7c4143e..0ea732a 100644 --- a/src/wireviz/wv_harness.py +++ b/src/wireviz/wv_harness.py @@ -8,6 +8,7 @@ from typing import List from graphviz import Graph import wireviz.wv_colors +from wireviz.wv_bom import BomEntry, print_bom_debug from wireviz.wv_dataclasses import ( AUTOGENERATED_PREFIX, AdditionalComponent, @@ -21,6 +22,7 @@ from wireviz.wv_dataclasses import ( Metadata, Options, Side, + TopLevelGraphicalComponent, Tweak, ) from wireviz.wv_graphviz import ( @@ -48,23 +50,20 @@ class Harness: self.connectors = {} self.cables = {} self.mates = [] - self._bom = defaultdict(dict) + self.bom = defaultdict(dict) self.additional_bom_items = [] def add_connector(self, designator: str, *args, **kwargs) -> None: conn = Connector(designator=designator, *args, **kwargs) self.connectors[designator] = conn - # self._add_to_internal_bom(conn) def add_cable(self, designator: str, *args, **kwargs) -> None: cbl = Cable(designator=designator, *args, **kwargs) self.cables[designator] = cbl - # self._add_to_internal_bom(cbl) def add_additional_bom_item(self, item: dict) -> None: new_item = AdditionalComponent(**item) self.additional_bom_items.append(new_item) - # self._add_to_internal_bom(new_item) def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_str) -> None: from_con = self.connectors[from_name] @@ -81,13 +80,22 @@ class Harness: arrow = Arrow(direction=parse_arrow_str(arrow_str), weight=ArrowWeight.SINGLE) self.mates.append(MateComponent(from_name, to_name, arrow)) + def populate_bom(self): + for item in self.connectors.values(): + self._add_to_internal_bom(item) + for item in self.cables.values(): + self._add_to_internal_bom(item) + for item in self.additional_bom_items: + self._add_to_internal_bom(item) + + print_bom_debug(self.bom) + def _add_to_internal_bom(self, item: Component): if item.ignore_in_bom: return - def _add(hash, designator=None, qty=1, category=None): - # generate entry - bom_entry = self._bom[hash] + def _add(hash, qty, designator=None, category=None): + bom_entry = self.bom[hash] # initialize missing fields if not "qty" in bom_entry: bom_entry["qty"] = 0 @@ -106,63 +114,52 @@ class Harness: bom_entry["designators"].add(des) bom_entry["category"] = category - if isinstance(item, Connector): - _add(item.bom_hash, designator=item.designator, category="connector") - for comp in item.additional_components: - if comp.ignore_in_bom: - continue - _add( - comp.bom_hash, - designator=item.designator, - qty=comp.qty_final, - category="connector/additional", - ) - elif isinstance(item, Cable): - _bom_hash = item.bom_hash + if isinstance(item, TopLevelGraphicalComponent): + if isinstance(item, Connector): + cat = "connector" + elif isinstance(item, Cable): + if item.category == "bundle": + cat = "wire" + else: + cat = "cable" + else: + cat = "" + if item.category == "bundle": - _cat = "wire" - for wire in item.wire_objects: + for subitem in item.wire_objects: _add( - None, - qty=item.length + 0.001, - designator=item.designator, - category="wire DUMMY", + hash=subitem.bom_hash, + qty=item.bom_qty, # should be 1 + designator=item.designator, # inherit from parent item + category=cat, ) else: - _cat = "cable" _add( - item.bom_hash, - qty=item.length, + hash=item.bom_hash, + qty=item.bom_qty, designator=item.designator, - category="cable", + category=cat, ) for comp in item.additional_components: if comp.ignore_in_bom: continue _add( - comp.bom_hash, + hash=comp.bom_hash, designator=item.designator, - qty=comp.qty_final, - category=f"{_cat}/additional", + qty=comp.bom_qty, + category=f"{cat}_additional", ) - elif isinstance(item, AdditionalComponent): # additional component + elif isinstance(item, AdditionalComponent): + cat = "additional" _add( - item.bom_hash, - designator=item.designators, - qty=item.qty_final, - category="additional", + hash=item.bom_hash, + qty=item.bom_qty, + designator=None, + category=cat, ) else: raise Exception(f"Unknown type of item:\n{item}") - def populate_bom(self): - pass - # raise Exception( - # "Implement BOM population after all connections have been made, " - # " so that additional component qty's can be computed correctly " - # "(factoring in number of connected pins/wires/...)" - # ) - def connect( self, from_name: str,