Implement BOM population (missing: qty multipliers)

This commit is contained in:
Daniel Rojas 2021-10-24 16:30:22 +02:00 committed by KV
parent 4db8c165ca
commit 8b9d997054
4 changed files with 336 additions and 227 deletions

View File

@ -5,33 +5,34 @@ from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import List, Optional, Union from typing import List, Optional, Union
import tabulate as tabulate_module
from wireviz.wv_utils import html_line_breaks 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) BomHash = namedtuple("BomHash", BOM_HASH_FIELDS)
BomHashList = namedtuple("BomHashList", BOM_HASH_FIELDS) BomHashList = namedtuple("BomHashList", BOM_HASH_FIELDS)
PartNumberInfo = namedtuple("PartNumberInfo", "pn manufacturer mpn supplier spn")
BomCategory = Enum( BomCategory = Enum(
"BomEntry", "CONNECTOR CABLE WIRE ADDITIONAL_INSIDE ADDITIONAL_OUTSIDE" "BomEntry", "CONNECTOR CABLE WIRE ADDITIONAL_INSIDE ADDITIONAL_OUTSIDE"
) )
QtyMultiplierConnector = Enum(
PartNumberInfo = namedtuple("PartNumberInfo", "pn manufacturer mpn supplier spn") "QtyMultiplierConnector", "ONE PINCOUNT POPULATED CONNECTIONS"
)
QtyMultiplierCable = Enum(
"QtyMultiplierCable", "ONE WIRECOUNT TERMINATION LENGTH TOTAL_LENGTH"
)
PART_NUMBER_HEADERS = PartNumberInfo( PART_NUMBER_HEADERS = PartNumberInfo(
pn="P/N", manufacturer=None, mpn="MPN", supplier=None, spn="SPN" 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]: def partnumbers_to_list(partnumbers: PartNumberInfo) -> List[str]:
cell_contents = [ cell_contents = [
pn_info_string(PART_NUMBER_HEADERS.pn, None, partnumbers.pn), 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 ""}' return f'{name if name else header}{": " + number if number else ""}'
else: else:
return None 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()

View File

@ -6,7 +6,13 @@ from enum import Enum
from itertools import zip_longest from itertools import zip_longest
from typing import Dict, List, Optional, Tuple, Union 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 ( from wireviz.wv_colors import (
COLOR_CODES, COLOR_CODES,
ColorOutputMode, ColorOutputMode,
@ -27,10 +33,6 @@ MultilineHypertext = (
Designator = PlainText # Case insensitive unique name of connector or cable Designator = PlainText # Case insensitive unique name of connector or cable
# Literal type aliases below are commented to avoid requiring python 3.8 # 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'] ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both']
# Type combinations # Type combinations
@ -52,6 +54,7 @@ MetadataKeys = PlainText # Literal['title', 'description', 'notes', ...]
Side = Enum("Side", "LEFT RIGHT") Side = Enum("Side", "LEFT RIGHT")
ArrowDirection = Enum("ArrowDirection", "NONE BACK FORWARD BOTH") ArrowDirection = Enum("ArrowDirection", "NONE BACK FORWARD BOTH")
ArrowWeight = Enum("ArrowWeight", "SINGLE DOUBLE") ArrowWeight = Enum("ArrowWeight", "SINGLE DOUBLE")
NumberAndUnit = namedtuple("NumberAndUnit", "number unit")
AUTOGENERATED_PREFIX = "AUTOGENERATED_" AUTOGENERATED_PREFIX = "AUTOGENERATED_"
@ -154,6 +157,7 @@ class PinClass:
label: str label: str
color: MultiColor color: MultiColor
parent: str # designator of parent connector parent: str # designator of parent connector
_num_connections = 0 # incremented in Connector.connect()
_anonymous: bool = False # true for pins on autogenerated connectors _anonymous: bool = False # true for pins on autogenerated connectors
_simple: bool = False # true for simple connector _simple: bool = False # true for simple connector
@ -166,33 +170,6 @@ class PinClass:
return ":".join([snip for snip in snippets if snip != ""]) 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 @dataclass
class Component: class Component:
category: Optional[str] = None # currently only used by cables, to define bundles category: Optional[str] = None # currently only used by cables, to define bundles
@ -208,7 +185,10 @@ class Component:
mpn: str = None mpn: str = None
supplier: str = None supplier: str = None
spn: 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 ignore_in_bom: bool = False
bom_id: Optional[str] = None # to be filled after harness is built bom_id: Optional[str] = None # to be filled after harness is built
@ -218,42 +198,71 @@ class Component:
partnos = tuple(partnos) partnos = tuple(partnos)
self.partnumbers = PartNumberInfo(*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 @property
def bom_hash(self) -> BomHash: def bom_hash(self) -> BomHash:
def _force_list(inp): if self.sum_amounts_in_bom:
if isinstance(inp, list): _hash = BomHash(
return inp description=self.description,
else: qty_unit=self.amount.unit if self.amount else None,
return [inp for i in range(len(self.colors))] amount=None,
partnumbers=self.partnumbers,
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,
) )
# 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: else:
return BomHash( _hash = BomHash(
self.description, description=self.description,
self.unit, qty_unit=self.qty.unit,
self.partnumbers, 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 @dataclass
class AdditionalComponent(Component): class AdditionalComponent(Component):
qty: float = 1 qty_multiplier: Union[QtyMultiplierConnector, QtyMultiplierCable, int] = 1
unit: Optional[str] = None qty_multipliers_computed: Dict = field(default_factory=list)
qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = 1
designators: Optional[str] = None # used for components definedi in the designators: Optional[str] = None # used for components definedi in the
# additional_bom_items section within another component # additional_bom_items section within another component
bgcolor: SingleColor = None # ^ same here bgcolor: SingleColor = None # ^ same here
@ -261,9 +270,22 @@ class AdditionalComponent(Component):
def __post_init__(self): def __post_init__(self):
super().fill_partnumbers() super().fill_partnumbers()
self.bgcolor = SingleColor(self.bgcolor) 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 @property
def qty_final(self): def bom_qty(self):
return 999 return 999
@property @property
@ -285,6 +307,8 @@ class TopLevelGraphicalComponent(GraphicalComponent): # abstract class
image: Optional[Image] = None image: Optional[Image] = None
additional_components: List[AdditionalComponent] = field(default_factory=list) additional_components: List[AdditionalComponent] = field(default_factory=list)
notes: Optional[MultilineHypertext] = None notes: Optional[MultilineHypertext] = None
# BOM options
add_up_in_bom: Optional[bool] = None
# rendering options # rendering options
bgcolor_title: Optional[SingleColor] = None bgcolor_title: Optional[SingleColor] = None
show_name: Optional[bool] = None show_name: Optional[bool] = None
@ -338,6 +362,10 @@ class Connector(TopLevelGraphicalComponent):
self.bgcolor_title = SingleColor(self.bgcolor_title) self.bgcolor_title = SingleColor(self.bgcolor_title)
self.color = SingleColor(self.color) 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): if isinstance(self.image, dict):
self.image = Image(**self.image) self.image = Image(**self.image)
@ -431,27 +459,106 @@ class Connector(TopLevelGraphicalComponent):
elif side == Side.RIGHT: elif side == Side.RIGHT:
self.ports_right = True self.ports_right = True
def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int: def compute_qty_multipliers(self):
# TODO!!! how and when to compute final qty for additional components??? for subitem in self.additional_components:
if not qty_multiplier: populated_pins = []
return 1 subitem.qty_multipliers_computed["ONE"] = 1
elif qty_multiplier == "pincount": subitem.qty_multipliers_computed["PINCOUNT"] = self.pincount
return self.pincount subitem.qty_multipliers_computed["POPULATED"] = 999
elif qty_multiplier == "populated": subitem.qty_multipliers_computed["CONNECTIONS"] = 999
return sum(self.visible_pins.values())
else: # QtyMultiplierConnector = Enum(
raise ValueError( # "QtyMultiplierConnector", "ONE PINCOUNT POPULATED CONNECTIONS"
f"invalid qty multiplier parameter for connector {qty_multiplier}" # )
# 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 @dataclass
class Cable(TopLevelGraphicalComponent): class Cable(TopLevelGraphicalComponent):
# cable-specific properties # cable-specific properties
gauge: Optional[float] = None gauge: Optional[NumberAndUnit] = None
gauge_unit: Optional[str] = None length: Optional[NumberAndUnit] = None
length: float = 0
length_unit: Optional[str] = None
color_code: Optional[str] = None color_code: Optional[str] = None
# wire information in particular # wire information in particular
wirecount: Optional[int] = None wirecount: Optional[int] = None
@ -460,7 +567,7 @@ class Cable(TopLevelGraphicalComponent):
wirelabels: List[Wire] = field(default_factory=list) # legacy wirelabels: List[Wire] = field(default_factory=list) # legacy
wire_objects: List[WireClass] = field( wire_objects: List[WireClass] = field(
default_factory=list default_factory=list
) # new, to replace the lists above ) # to replace the lists above
# internal # internal
_connections: List[Connection] = field(default_factory=list) _connections: List[Connection] = field(default_factory=list)
# rendering options # rendering options
@ -475,49 +582,57 @@ class Cable(TopLevelGraphicalComponent):
@property @property
def unit(self): # for compatibility with parent class def unit(self): # for compatibility with parent class
return self.length_unit return self.length
@property @property
def gauge_str(self): def gauge_str(self):
if not self.gauge: if not self.gauge:
return None 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 = "" equivalent_gauge = ""
if self.show_equiv: if self.show_equiv:
# Only convert units we actually know about, i.e. currently # convert unit if known
# mm2 and awg --- other units _are_ technically allowed, if self.gauge.unit == "mm2":
# and passed through as-is.
if self.gauge_unit == "mm\u00B2":
equivalent_gauge = f" ({awg_equiv(self.gauge)} AWG)" equivalent_gauge = f" ({awg_equiv(self.gauge)} AWG)"
elif self.gauge_unit.upper() == "AWG": elif self.gauge.unit.upper() == "AWG":
equivalent_gauge = f" ({mm2_equiv(self.gauge)} mm\u00B2)" equivalent_gauge = f" ({mm2_equiv(self.gauge)} mm2)"
return f"{actual_gauge}{equivalent_gauge}" 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 @property
def description(self) -> str: def description(self) -> str:
if self.category == "bundle": if self.category == "bundle":
desc_list = [] raise Exception("Do this at the wire level!") # TODO
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
else: else:
substrs = [ substrs = [
("", "Cable"), ("", "Cable"),
(", ", self.type), (", ", self.type),
(", ", self.subtype), (", ", self.subtype),
(", ", self.wirecount), (", ", 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), (" ", "shielded" if self.shield else None),
(", ", str(self.color) if self.color else None), (", ", str(self.color) if self.color else None),
] ]
@ -537,52 +652,9 @@ class Cable(TopLevelGraphicalComponent):
if isinstance(self.image, dict): if isinstance(self.image, dict):
self.image = Image(**self.image) self.image = Image(**self.image)
if isinstance(self.gauge, str): # gauge and unit specified self.gauge = self.parse_number_and_unit(self.gauge, "mm2")
try: self.length = self.parse_number_and_unit(self.length, "m")
g, u = self.gauge.split(" ") self.amount = self.length # for BOM
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"
if self.wirecount: # number of wires explicitly defined if self.wirecount: # number of wires explicitly defined
if self.colors: # use custom color palette (partly or looped if needed) 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): for wire_index, (wire_color, wire_label) in enumerate(wire_tuples):
self.wire_objects.append( self.wire_objects.append(
WireClass( WireClass(
parent=self.designator,
# wire-specific properties
index=wire_index, # TODO: wire_id index=wire_index, # TODO: wire_id
id=wire_index + 1, # TODO: wire_id id=wire_index + 1, # TODO: wire_id
label=wire_label, label=wire_label,
color=MultiColor(wire_color), 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) via_wire_obj = self.get_wire_by_id(via_wire_id)
self._connections.append(Connection(from_pin_obj, via_wire_obj, to_pin_obj)) 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: # def get_qty_multiplier(self, qty_multiplier: Optional[CableMultiplier]) -> float:
return 1 # if not qty_multiplier:
elif qty_multiplier == "wirecount": # return 1
return self.wirecount # elif qty_multiplier == "wirecount":
elif qty_multiplier == "terminations": # return self.wirecount
return len(self.connections) # elif qty_multiplier == "terminations":
elif qty_multiplier == "length": # return len(self.connections)
return self.length # elif qty_multiplier == "length":
elif qty_multiplier == "total_length": # return self.length
return self.length * self.wirecount # elif qty_multiplier == "total_length":
else: # return self.length * self.wirecount
raise ValueError( # else:
f"invalid qty multiplier parameter for cable {qty_multiplier}" # raise ValueError(
) # f"invalid qty multiplier parameter for cable {qty_multiplier}"
# )
@dataclass @dataclass

View File

@ -54,11 +54,9 @@ def gv_node_component(component: Component) -> Table:
line_info = [ line_info = [
html_line_breaks(component.type), html_line_breaks(component.type),
f"{component.wirecount}x" if component.show_wirecount else None, 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, "+ S" if component.shield else None,
f"{component.length} {component.length_unit}" component.length_str,
if component.length > 0
else None,
str(component.color) if component.color else None, str(component.color) if component.color else None,
colorbar_cell(component.color) if component.color else None, colorbar_cell(component.color) if component.color else None,
] ]

View File

@ -8,6 +8,7 @@ from typing import List
from graphviz import Graph from graphviz import Graph
import wireviz.wv_colors import wireviz.wv_colors
from wireviz.wv_bom import BomEntry, print_bom_debug
from wireviz.wv_dataclasses import ( from wireviz.wv_dataclasses import (
AUTOGENERATED_PREFIX, AUTOGENERATED_PREFIX,
AdditionalComponent, AdditionalComponent,
@ -21,6 +22,7 @@ from wireviz.wv_dataclasses import (
Metadata, Metadata,
Options, Options,
Side, Side,
TopLevelGraphicalComponent,
Tweak, Tweak,
) )
from wireviz.wv_graphviz import ( from wireviz.wv_graphviz import (
@ -48,23 +50,20 @@ class Harness:
self.connectors = {} self.connectors = {}
self.cables = {} self.cables = {}
self.mates = [] self.mates = []
self._bom = defaultdict(dict) self.bom = defaultdict(dict)
self.additional_bom_items = [] self.additional_bom_items = []
def add_connector(self, designator: str, *args, **kwargs) -> None: def add_connector(self, designator: str, *args, **kwargs) -> None:
conn = Connector(designator=designator, *args, **kwargs) conn = Connector(designator=designator, *args, **kwargs)
self.connectors[designator] = conn self.connectors[designator] = conn
# self._add_to_internal_bom(conn)
def add_cable(self, designator: str, *args, **kwargs) -> None: def add_cable(self, designator: str, *args, **kwargs) -> None:
cbl = Cable(designator=designator, *args, **kwargs) cbl = Cable(designator=designator, *args, **kwargs)
self.cables[designator] = cbl self.cables[designator] = cbl
# self._add_to_internal_bom(cbl)
def add_additional_bom_item(self, item: dict) -> None: def add_additional_bom_item(self, item: dict) -> None:
new_item = AdditionalComponent(**item) new_item = AdditionalComponent(**item)
self.additional_bom_items.append(new_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: def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_str) -> None:
from_con = self.connectors[from_name] from_con = self.connectors[from_name]
@ -81,13 +80,22 @@ class Harness:
arrow = Arrow(direction=parse_arrow_str(arrow_str), weight=ArrowWeight.SINGLE) arrow = Arrow(direction=parse_arrow_str(arrow_str), weight=ArrowWeight.SINGLE)
self.mates.append(MateComponent(from_name, to_name, arrow)) 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): def _add_to_internal_bom(self, item: Component):
if item.ignore_in_bom: if item.ignore_in_bom:
return return
def _add(hash, designator=None, qty=1, category=None): def _add(hash, qty, designator=None, category=None):
# generate entry bom_entry = self.bom[hash]
bom_entry = self._bom[hash]
# initialize missing fields # initialize missing fields
if not "qty" in bom_entry: if not "qty" in bom_entry:
bom_entry["qty"] = 0 bom_entry["qty"] = 0
@ -106,63 +114,52 @@ class Harness:
bom_entry["designators"].add(des) bom_entry["designators"].add(des)
bom_entry["category"] = category bom_entry["category"] = category
if isinstance(item, Connector): if isinstance(item, TopLevelGraphicalComponent):
_add(item.bom_hash, designator=item.designator, category="connector") if isinstance(item, Connector):
for comp in item.additional_components: cat = "connector"
if comp.ignore_in_bom: elif isinstance(item, Cable):
continue if item.category == "bundle":
_add( cat = "wire"
comp.bom_hash, else:
designator=item.designator, cat = "cable"
qty=comp.qty_final, else:
category="connector/additional", cat = ""
)
elif isinstance(item, Cable):
_bom_hash = item.bom_hash
if item.category == "bundle": if item.category == "bundle":
_cat = "wire" for subitem in item.wire_objects:
for wire in item.wire_objects:
_add( _add(
None, hash=subitem.bom_hash,
qty=item.length + 0.001, qty=item.bom_qty, # should be 1
designator=item.designator, designator=item.designator, # inherit from parent item
category="wire DUMMY", category=cat,
) )
else: else:
_cat = "cable"
_add( _add(
item.bom_hash, hash=item.bom_hash,
qty=item.length, qty=item.bom_qty,
designator=item.designator, designator=item.designator,
category="cable", category=cat,
) )
for comp in item.additional_components: for comp in item.additional_components:
if comp.ignore_in_bom: if comp.ignore_in_bom:
continue continue
_add( _add(
comp.bom_hash, hash=comp.bom_hash,
designator=item.designator, designator=item.designator,
qty=comp.qty_final, qty=comp.bom_qty,
category=f"{_cat}/additional", category=f"{cat}_additional",
) )
elif isinstance(item, AdditionalComponent): # additional component elif isinstance(item, AdditionalComponent):
cat = "additional"
_add( _add(
item.bom_hash, hash=item.bom_hash,
designator=item.designators, qty=item.bom_qty,
qty=item.qty_final, designator=None,
category="additional", category=cat,
) )
else: else:
raise Exception(f"Unknown type of item:\n{item}") 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( def connect(
self, self,
from_name: str, from_name: str,