From 1da0b0fc56c71f0dbb56bee3b9e14f0f0fdabacd Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Fri, 26 Apr 2024 09:38:55 +0200 Subject: [PATCH] Continue work on BOM handling (WIP) --- src/wireviz/wv_dataclasses.py | 101 ++++++++++++++++++++-------------- src/wireviz/wv_graphviz.py | 31 +++++++++-- src/wireviz/wv_harness.py | 46 ++++++++++------ tests/bom/bomqty.yml | 52 +++++++---------- 4 files changed, 135 insertions(+), 95 deletions(-) diff --git a/src/wireviz/wv_dataclasses.py b/src/wireviz/wv_dataclasses.py index 181a3503..ded141a5 100644 --- a/src/wireviz/wv_dataclasses.py +++ b/src/wireviz/wv_dataclasses.py @@ -190,7 +190,7 @@ class Component: supplier: str = None spn: str = None # BOM info - qty: NumberAndUnit = NumberAndUnit(1, None) + qty: Optional[Union[None, int, float]] = None amount: Optional[NumberAndUnit] = None sum_amounts_in_bom: bool = True ignore_in_bom: bool = False @@ -201,44 +201,31 @@ def __post_init__(self): partnos = [remove_links(entry) for entry in partnos] partnos = tuple(partnos) self.partnumbers = PartNumberInfo(*partnos) - - self.qty = parse_number_and_unit(self.qty, None) self.amount = parse_number_and_unit(self.amount, None) @property def bom_hash(self) -> BomHash: + if isinstance(self, AdditionalComponent): + _amount = self.amount_computed + else: + _amount = self.amount + if self.sum_amounts_in_bom: _hash = BomHash( description=self.description, - qty_unit=self.amount.unit if self.amount else None, + qty_unit=_amount.unit if _amount else None, amount=None, partnumbers=self.partnumbers, ) else: _hash = BomHash( description=self.description, - qty_unit=self.qty.unit, - amount=self.amount, + qty_unit=None, + amount=_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 - @property def has_pn_info(self) -> bool: return any([self.pn, self.manufacturer, self.mpn, self.supplier, self.spn]) @@ -272,7 +259,9 @@ def __post_init__(self): @dataclass class AdditionalComponent(GraphicalComponent): qty_multiplier: Union[QtyMultiplierConnector, QtyMultiplierCable, int] = 1 - _qty_multiplier_computed: Union[int, float] = 1 + qty_computed: Optional[int] = None + explicit_qty: bool = True + amount_computed: Optional[NumberAndUnit] = None note: str = None def __post_init__(self): @@ -291,9 +280,13 @@ def __post_init__(self): else: raise Exception(f"Unknown qty multiplier: {self.qty_multiplier}") - @property - def bom_qty(self): - return self.qty.number * self._qty_multiplier_computed + if self.qty is None and self.qty_multiplier in [ + QtyMultiplierCable.TOTAL_LENGTH, + QtyMultiplierCable.LENGTH, + 1, + ]: # simplify add.comp. table in parent node for implicit qty 1 + self.qty = 1 + self.explicit_qty = False @dataclass @@ -357,13 +350,12 @@ def __post_init__(self) -> None: self.color = MultiColor(self.color) # connectors do not support custom qty or amount - if self.qty != NumberAndUnit(1, None): + if self.qty is None: + self.qty = 1 + if self.qty != 1: raise Exception("Connector qty != 1 not supported") if self.amount is not None: raise Exception("Connector amount not supported") - # TODO: Delete next two assignments if tests above is sufficient. Please verify! - self.qty = NumberAndUnit(1, None) - self.amount = None if isinstance(self.image, dict): self.image = Image(**self.image) @@ -467,7 +459,12 @@ def compute_qty_multipliers(self): raise Exception("Used a cable multiplier in a connector!") else: # int or float computed_factor = subitem.qty_multiplier - subitem._qty_multiplier_computed = computed_factor + + if subitem.qty is not None: + subitem.qty_computed = subitem.qty * computed_factor + else: + subitem.qty_computed = computed_factor + subitem.amount_computed = subitem.amount @dataclass @@ -602,14 +599,17 @@ def length_str(self): @property def bom_hash(self): if self.category == "bundle": - raise Exception("Do this at the wire level!") # TODO + # This line should never be reached, since caller checks + # whether item is a bundle and if so, calls bom_hash + # for each individual wire instead + raise Exception("Do this at the wire level!") else: return super().bom_hash @property def description(self) -> str: if self.category == "bundle": - raise Exception("Do this at the wire level!") # TODO + raise Exception("Do this at the wire level!") else: substrs = [ ("", "Cable"), @@ -647,6 +647,12 @@ def __post_init__(self) -> None: self.bgcolor_title = SingleColor(self.bgcolor_title) self.color = MultiColor(self.color) + # cables do not support custom qty or amount + if self.qty is None: + self.qty = 1 + if self.qty != 1: + raise Exception("Cable qty != 1 not supported") + if isinstance(self.image, dict): self.image = Image(**self.image) @@ -770,27 +776,40 @@ def compute_qty_multipliers(self): ) qty_multipliers_computed = { "WIRECOUNT": len(self.wire_objects), - "TERMINATIONS": 999, # TODO + # "TERMINATIONS": ___, # TODO "LENGTH": self.length.number if self.length else 0, "TOTAL_LENGTH": total_length, } for subitem in self.additional_components: if isinstance(subitem.qty_multiplier, QtyMultiplierCable): computed_factor = qty_multipliers_computed[subitem.qty_multiplier.name] - # inherit component's length unit if appropriate if subitem.qty_multiplier.name in ["LENGTH", "TOTAL_LENGTH"]: - if subitem.qty.unit is not None: + # since length can have a unit, use amount fields to hold + if subitem.amount is not None: raise Exception( - f"No unit may be specified when using" - f"{subitem.qty_multiplier} as a multiplier" + f"No amount may be specified when using " + f"{subitem.qty_multiplier.name} as a multiplier." ) - subitem.qty = NumberAndUnit(subitem.qty.number, self.length.unit) + subitem.qty_computed = subitem.qty if subitem.qty else 1 + subitem.amount_computed = NumberAndUnit( + computed_factor, self.length.unit + ) + else: + # multiplier unrelated to length, therefore no unit + if subitem.qty is not None: + subitem.qty_computed = subitem.qty * computed_factor + else: + subitem.qty_computed = computed_factor + subitem.amount_computed = subitem.amount elif isinstance(subitem.qty_multiplier, QtyMultiplierConnector): raise Exception("Used a connector multiplier in a cable!") else: # int or float - computed_factor = subitem.qty_multiplier - subitem._qty_multiplier_computed = computed_factor + if subitem.qty is not None: + subitem.qty_computed = subitem.qty * subitem.qty_multiplier + else: + subitem.qty_computed = subitem.qty_multiplier + subitem.amount_computed = subitem.amount @dataclass diff --git a/src/wireviz/wv_graphviz.py b/src/wireviz/wv_graphviz.py index 7f5cad6a..d42dcacc 100644 --- a/src/wireviz/wv_graphviz.py +++ b/src/wireviz/wv_graphviz.py @@ -114,11 +114,28 @@ def gv_additional_component_table(component): rows = [] for subitem in component.additional_components: + + if subitem.explicit_qty: + text_qty, unit_qty = subitem.qty_computed, "x" + if subitem.amount_computed is not None: + text_desc = f"{subitem.amount_computed.number} {subitem.amount_computed.unit} {subitem.description}" + else: + text_desc = f"{subitem.description}" + else: + if subitem.amount_computed is not None: + text_qty, unit_qty = ( + subitem.amount_computed.number, + subitem.amount_computed.unit, + ) + else: + text_qty, unit_qty = "1", "x" + text_desc = subitem.description + firstline = [ Td(bom_bubble(subitem.bom_id)), - Td(f"{subitem.bom_qty}", align="right"), - Td(f"{subitem.qty.unit if subitem.qty.unit else 'x'}", align="left"), - Td(f"{subitem.description}", align="left"), + Td(text_qty, align="right"), + Td(unit_qty, align="left"), + Td(text_desc, align="left"), Td(f"{subitem.note if subitem.note else ''}", align="left"), ] rows.append(Tr(firstline)) @@ -584,7 +601,9 @@ def typecheck(name: str, value: Any, expect: type) -> None: if n_subs < 1: warnings.warn(f"tweak: {attr} not found in {keyword}!") elif n_subs > 1: - warnings.warn(f"tweak: {attr} removed {n_subs} times in {keyword}!") + warnings.warn( + f"tweak: {attr} removed {n_subs} times in {keyword}!" + ) continue if len(value) == 0 or " " in value: @@ -597,7 +616,9 @@ def typecheck(name: str, value: Any, expect: type) -> None: # If attr not found, then append it entry = re.sub(r"\]$", f" {attr}={value}]", entry) elif n_subs > 1: - warnings.warn(f"tweak: {attr} overridden {n_subs} times in {keyword}!") + warnings.warn( + f"tweak: {attr} overridden {n_subs} times in {keyword}!" + ) dot.body[i] = entry diff --git a/src/wireviz/wv_harness.py b/src/wireviz/wv_harness.py index fa6a01f0..0c730acb 100644 --- a/src/wireviz/wv_harness.py +++ b/src/wireviz/wv_harness.py @@ -86,7 +86,7 @@ def add_mate_component(self, from_name, to_name, arrow_str) -> None: arrow = Arrow(direction=parse_arrow_str(arrow_str), weight=ArrowWeight.SINGLE) self.mates.append(MateComponent(from_name, to_name, arrow)) - def populate_bom(self): + def populate_bom(self): # called once harness creation is complete # helper lists all_toplevel_items = ( list(self.connectors.values()) @@ -131,12 +131,10 @@ def populate_bom(self): if item.ignore_in_bom: continue if not item.bom_hash in self.bom: - print(f"{item}'s hash' not found in BOM dict.") + print(f"{item}'s hash' not found in BOM dict.") # Should not happen continue item.bom_id = self.bom[item.bom_hash]["id"] - # print_bom_table(self.bom) # for debugging - def _add_to_internal_bom(self, item: Component): if item.ignore_in_bom: return @@ -173,38 +171,50 @@ def _add(hash, qty, designator=None, category=None): cat = "" if item.category == "bundle": + # wires of a bundle are added as individual BOM entries for subitem in item.wire_objects.values(): _add( hash=subitem.bom_hash, - qty=item.bom_qty, # should be 1 + qty=item.qty, # should be 1 designator=item.designator, # inherit from parent item category=cat, ) else: _add( hash=item.bom_hash, - qty=item.bom_qty, + qty=item.qty, # should be 1 designator=item.designator, category=cat, ) + if item.additional_components: - if item.category == "bundle": - pass # TODO item.compute_qty_multipliers() - for comp in item.additional_components: - if comp.ignore_in_bom: - continue - _add( - hash=comp.bom_hash, - designator=item.designator, - qty=comp.bom_qty, - category=BomCategory.ADDITIONAL_INSIDE, - ) + + for comp in item.additional_components: + if comp.ignore_in_bom: + continue + + if comp.sum_amounts_in_bom: + if comp.amount_computed: + total_qty = comp.qty_computed * comp.amount_computed.number + else: + total_qty = comp.qty_computed + else: + total_qty = comp.qty_computed + _add( + hash=comp.bom_hash, + designator=item.designator, + qty=total_qty, + # no explicit qty specified; assume qty = 1 + # used to simplify add.comp. table within parent node + # e.g. show "10 mm Heatshrink" instead of "1x 10 mm Heatshrink" + category=BomCategory.ADDITIONAL_INSIDE, + ) elif isinstance(item, AdditionalBomItem): cat = BomCategory.ADDITIONAL_OUTSIDE _add( hash=item.bom_hash, - qty=item.bom_qty, + qty=item.qty, designator=None, category=cat, ) diff --git a/tests/bom/bomqty.yml b/tests/bom/bomqty.yml index 9302139c..0cb3e156 100644 --- a/tests/bom/bomqty.yml +++ b/tests/bom/bomqty.yml @@ -7,21 +7,16 @@ connectors: type: Contains additional components pincount: 6 additional_components: - - - type: One, no unit - - - type: Two kilometers - qty: 2 km - - - type: Takes pincount times seven + - type: One, no unit + - type: Two kilometers + amount: 2 km + - type: Takes pincount times seven qty: 7 qty_multiplier: pincount - - - type: Takes 10 mm per populated pin - qty: 10 mm + - type: Takes 10 mm per populated pin + amount: 10 mm qty_multiplier: populated - - - type: Takes number of connections + - type: Takes number of connections qty_multiplier: connections cables: @@ -31,22 +26,19 @@ cables: length: 1.5 color_code: DIN additional_components: - - - type: One - - - type: Three centimeters - qty: 3 cm - - - type: Takes wirecount times two + - type: One + - type: Three centimeters + amount: 3 cm + - type: Takes wirecount times two qty: 2 qty_multiplier: wirecount - - - type: Takes length times three - qty: 3 # adding unit here should cause error because the length already has a unit + - type: Takes length times three + qty: 3 + # adding amount here should cause error because the length already has a unit qty_multiplier: length - - - type: Takes total length times three - qty: 2 # adding unit here should cause error because the length already has a unit + - type: Takes total length times three + qty: 2 + # adding amount here should cause error because the length already has a unit qty_multiplier: total_length W2: @@ -55,11 +47,9 @@ cables: colors: [tomato, skyblue] connections: - - - - X1: [1-3] + - - X1: [1-3] - C1: [1-3] - X2: [1-3] - - - - X1: [3,4] - - W2: [1,2] - - X2: [3,4] + - - X1: [3, 4] + - W2: [1, 2] + - X2: [3, 4]