Improve subclassing of components, prepare for BOM refactoring

This commit is contained in:
Daniel Rojas 2021-10-21 17:15:02 +02:00 committed by KV
parent ef2b406c78
commit 36ffa969f4
6 changed files with 341 additions and 408 deletions

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
from collections import namedtuple
from dataclasses import dataclass, field
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_colors import (
COLOR_CODES,
ColorOutputMode,
@ -145,34 +147,6 @@ class Image:
self.height = self.width / aspect_ratio(self.src)
@dataclass
class AdditionalComponent:
type: MultilineHypertext
subtype: Optional[MultilineHypertext] = None
manufacturer: Optional[MultilineHypertext] = None
mpn: Optional[MultilineHypertext] = None
supplier: Optional[MultilineHypertext] = None
spn: Optional[MultilineHypertext] = None
pn: Optional[Hypertext] = None
qty: float = 1
unit: Optional[str] = None
qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None
bgcolor: SingleColor = None
def __post_init__(self):
self.bgcolor = SingleColor(self.bgcolor)
@property
def description(self) -> str:
s = self.type.rstrip() + f", {self.subtype.rstrip()}" if self.subtype else ""
return s
@dataclass
class Component:
pass
@dataclass
class PinClass:
index: int
@ -220,43 +194,144 @@ class Connection:
@dataclass
class Connector(Component):
name: Designator
bgcolor: SingleColor = None
bgcolor_title: SingleColor = None
manufacturer: Optional[MultilineHypertext] = None
mpn: Optional[MultilineHypertext] = None
supplier: Optional[MultilineHypertext] = None
spn: Optional[MultilineHypertext] = None
pn: Optional[Hypertext] = None
class Component:
category: Optional[str] = None # currently only used by cables, to define bundles
type: Union[MultilineHypertext, List[MultilineHypertext]] = None
subtype: Union[MultilineHypertext, List[MultilineHypertext]] = None
# part number
partnumbers: PartNumberInfo = None # filled by fill_partnumbers()
# the following are provided for user convenience and should not be accessed later.
# their contents are loaded into partnumbers during the child class __post_init__()
pn: str = None
manufacturer: str = None
mpn: str = None
supplier: str = None
spn: str = None
ignore_in_bom: bool = False
bom_id: Optional[str] = None # to be filled after harness is built
def fill_partnumbers(self):
self.partnumbers = PartNumberInfo(
self.pn, self.manufacturer, self.mpn, self.supplier, self.spn
)
@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,
)
# 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,
)
@dataclass
class AdditionalComponent(Component):
qty: float = 1
unit: Optional[str] = None
qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None
designators: Optional[str] = None # used for components definedi in the
# additional_bom_items section within another component
bgcolor: SingleColor = None # ^ same here
def __post_init__(self):
super().fill_partnumbers()
self.bgcolor = SingleColor(self.bgcolor)
@property
def description(self) -> str:
s = self.type.rstrip() + f", {self.subtype.rstrip()}" if self.subtype else ""
return s
@dataclass
class GraphicalComponent(Component): # abstract class, for future use
bgcolor: Optional[SingleColor] = None
@dataclass
class TopLevelGraphicalComponent(GraphicalComponent): # abstract class
# component properties
designator: Designator = None
color: Optional[SingleColor] = None
image: Optional[Image] = None
additional_components: List[AdditionalComponent] = field(default_factory=list)
notes: Optional[MultilineHypertext] = None
# rendering options
bgcolor_title: Optional[SingleColor] = None
show_name: Optional[bool] = None
@dataclass
class Connector(TopLevelGraphicalComponent):
# connector-specific properties
style: Optional[str] = None
category: Optional[str] = None
type: Optional[MultilineHypertext] = None
subtype: Optional[MultilineHypertext] = None
loops: List[List[Pin]] = field(default_factory=list)
# pin information in particular
pincount: Optional[int] = None
image: Optional[Image] = None
notes: Optional[MultilineHypertext] = None
pins: List[Pin] = field(default_factory=list)
pinlabels: List[Pin] = field(default_factory=list)
pincolors: List[str] = field(default_factory=list)
color: MultiColor = None
show_name: Optional[bool] = None
pins: List[Pin] = field(default_factory=list) # legacy
pinlabels: List[Pin] = field(default_factory=list) # legacy
pincolors: List[str] = field(default_factory=list) # legacy
pin_objects: List[PinClass] = field(
default_factory=list
) # new, to replace the lists above
# rendering option
show_pincount: Optional[bool] = None
hide_disconnected_pins: bool = False
loops: List[List[Pin]] = field(default_factory=list)
ignore_in_bom: bool = False
additional_components: List[AdditionalComponent] = field(default_factory=list)
pin_objects: List[PinClass] = field(default_factory=list)
@property
def is_autogenerated(self):
return self.name.startswith(AUTOGENERATED_PREFIX)
import pudb
pudb.set_trace()
return self.designator.startswith(AUTOGENERATED_PREFIX)
@property
def description(self) -> str:
substrs = [
"Connector",
self.type,
self.subtype,
self.pincount if self.show_pincount else None,
str(self.color) if self.color else None,
]
return ", ".join([str(s) for s in substrs if s is not None and s != ""])
def should_show_pin(self, pin_name):
return not self.hide_disconnected_pins or self.visible_pins.get(pin_name, False)
@property
def unit(self): # for compatibility with BOM hashing
return None # connectors do not support units.
def __post_init__(self) -> None:
super().fill_partnumbers()
self.bgcolor = SingleColor(self.bgcolor)
self.bgcolor_title = SingleColor(self.bgcolor_title)
self.color = SingleColor(self.color)
@ -305,7 +380,7 @@ class Connector(Component):
id=pin_id,
label=pin_label,
color=MultiColor(pin_color),
parent=self.name,
parent=self.designator,
_anonymous=self.is_autogenerated,
_simple=self.style == "simple",
)
@ -337,9 +412,9 @@ class Connector(Component):
def _check_if_unique_id(self, id):
results = [pin for pin in self.pin_objects if pin.id == id]
if len(results) == 0:
raise Exception(f"Pin ID {id} not found in {self.name}")
raise Exception(f"Pin ID {id} not found in {self.designator}")
if len(results) > 1:
raise Exception(f"Pin ID {id} found more than once in {self.name}")
raise Exception(f"Pin ID {id} found more than once in {self.designator}")
return True
def get_pin_by_id(self, id):
@ -368,41 +443,36 @@ class Connector(Component):
@dataclass
class Cable(Component):
name: Designator
bgcolor: SingleColor = None
bgcolor_title: SingleColor = None
manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None
mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None
spn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
pn: Union[Hypertext, List[Hypertext], None] = None
category: Optional[str] = None
type: Optional[MultilineHypertext] = None
class Cable(TopLevelGraphicalComponent):
# cable-specific properties
gauge: Optional[float] = None
gauge_unit: Optional[str] = None
show_equiv: bool = False
length: float = 0
length_unit: Optional[str] = None
color: MultiColor = None
color_code: Optional[str] = None
# wire information in particular
wirecount: Optional[int] = None
shield: Union[bool, MultiColor] = False
image: Optional[Image] = None
notes: Optional[MultilineHypertext] = None
colors: List[str] = field(default_factory=list)
wirelabels: List[Wire] = field(default_factory=list)
color_code: Optional[str] = None
colors: List[str] = field(default_factory=list) # legacy
wirelabels: List[Wire] = field(default_factory=list) # legacy
wire_objects: List[WireClass] = field(
default_factory=list
) # new, to replace the lists above
# internal
_connections: List[Connection] = field(default_factory=list)
# rendering options
show_name: Optional[bool] = None
show_equiv: bool = False
show_wirecount: bool = True
show_wirenumbers: Optional[bool] = None
ignore_in_bom: bool = False
additional_components: List[AdditionalComponent] = field(default_factory=list)
connections: List[Connection] = field(default_factory=list)
wire_objects: List[WireClass] = field(default_factory=list)
@property
def is_autogenerated(self):
return self.name.startswith(AUTOGENERATED_PREFIX)
return self.designator.startswith(AUTOGENERATED_PREFIX)
@property
def unit(self): # for compatibility with parent class
return self.length_unit
@property
def gauge_str(self):
@ -420,8 +490,43 @@ class Cable(Component):
equivalent_gauge = f" ({mm2_equiv(self.gauge)} mm\u00B2)"
return f"{actual_gauge}{equivalent_gauge}"
@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
else:
substrs = [
("", "Cable"),
(", ", self.type),
(", ", self.subtype),
(", ", self.wirecount),
(" ", f"x {self.gauge} {self.gauge_unit}" if self.gauge else " wires"),
(" ", "shielded" if self.shield else None),
(", ", str(self.color) if self.color else None),
]
desc = "".join(
[f"{s[0]}{s[1]}" for s in substrs if s[1] is not None and s[1] != ""]
)
return desc
def __post_init__(self) -> None:
super().fill_partnumbers()
self.bgcolor = SingleColor(self.bgcolor)
self.bgcolor_title = SingleColor(self.bgcolor_title)
self.color = SingleColor(self.color)
@ -434,14 +539,14 @@ class Cable(Component):
g, u = self.gauge.split(" ")
except Exception:
raise Exception(
f"Cable {self.name} gauge={self.gauge} - "
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.name} gauge_unit={self.gauge_unit} "
f"Warning: Cable {self.designator} gauge_unit={self.gauge_unit} "
f"is ignored because its gauge contains {u}"
)
if u.upper() == "AWG":
@ -461,18 +566,18 @@ class Cable(Component):
L = float(L)
except Exception:
raise Exception(
f"Cable {self.name} length={self.length} - "
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.name} length_unit={self.length_unit} is ignored "
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.name} length has a non-numeric value")
raise Exception(f"Cable {self.designator} length has a non-numeric value")
elif self.length_unit is None:
self.length_unit = "m"
@ -530,7 +635,7 @@ class Cable(Component):
id=wire_index + 1, # TODO: wire_id
label=wire_label,
color=MultiColor(wire_color),
parent=self.name,
parent=self.designator,
)
)
@ -545,7 +650,7 @@ class Cable(Component):
color=MultiColor(self.shield)
if isinstance(self.shield, str)
else MultiColor(None),
parent=self.name,
parent=self.designator,
)
)
@ -563,16 +668,19 @@ class Cable(Component):
def get_wire_by_id(self, id):
wire = [wire for wire in self.wire_objects if wire.id == id]
if len(wire) == 0:
raise Exception(f"Wire ID {id} not found in {self.name}")
raise Exception(f"Wire ID {id} not found in {self.designator}")
if len(wire) > 1:
raise Exception(f"Wire ID {id} found more than once in {self.name}")
raise Exception(f"Wire ID {id} found more than once in {self.designator}")
return wire[0]
def connect(
self, from_pin_obj: [PinClass], via_wire_id: str, to_pin_obj: [PinClass]
def _connect(
self,
from_pin_obj: [PinClass],
via_wire_id: str,
to_pin_obj: [PinClass],
) -> None:
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:

View File

@ -1,12 +1,15 @@
# -*- coding: utf-8 -*-
from dataclasses import dataclass
from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path
from typing import List
from graphviz import Graph
import wireviz.wv_colors
from wireviz.DataClasses import (
AdditionalComponent,
Arrow,
ArrowWeight,
Cable,
@ -19,7 +22,6 @@ from wireviz.DataClasses import (
Tweak,
)
from wireviz.svgembed import embed_svg_images_file
from wireviz.wv_bom import bom_list, generate_bom
from wireviz.wv_gv_html import (
apply_dot_tweaks,
calculate_node_bgcolor,
@ -39,19 +41,29 @@ class Harness:
metadata: Metadata
options: Options
tweak: Tweak
additional_bom_items: List[AdditionalComponent] = field(default_factory=list)
def __post_init__(self):
self.connectors = {}
self.cables = {}
self.mates = []
self._bom = [] # Internal Cache for generated bom
self._bom = defaultdict(dict)
self.additional_bom_items = []
def add_connector(self, name: str, *args, **kwargs) -> None:
self.connectors[name] = Connector(name, *args, **kwargs)
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, name: str, *args, **kwargs) -> None:
self.cables[name] = Cable(name, *args, **kwargs)
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]
@ -68,8 +80,65 @@ class Harness:
arrow = Arrow(direction=parse_arrow_str(arrow_str), weight=ArrowWeight.SINGLE)
self.mates.append(MateComponent(from_name, to_name, arrow))
def add_bom_item(self, item: dict) -> None:
self.additional_bom_items.append(item)
def _add_to_internal_bom(self, item):
if item.ignore_in_bom:
return
def _add(hash, designator=None, qty=1, category=None):
# generate entry
bom_entry = self._bom[hash]
# initialize missing fields
if not "qty" in bom_entry:
bom_entry["qty"] = 0
if not "designators" in bom_entry:
bom_entry["designators"] = set()
# update fields
bom_entry["qty"] += qty
if designator:
if isinstance(designator, str):
bom_entry["designators"].add(designator)
else:
bom_entry["designators"].update(designator)
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,
category="connector/additional",
)
elif isinstance(item, Cable):
_bom_hash = item.bom_hash
if isinstance(_bom_hash, list):
_cat = "bundle"
for subhash in _bom_hash:
_add(subhash, designator=item.designator, category=_cat)
else:
_cat = "cable"
_add(item.bom_hash, designator=item.designator, category=_cat)
for comp in item.additional_components:
if comp.ignore_in_bom:
continue
_add(
comp.bom_hash,
designator=item.designator,
qty=comp.qty,
category=f"{_cat}/additional",
)
elif isinstance(item, AdditionalComponent): # additional component
_add(
item.bom_hash,
designator=item.designators,
qty=item.qty,
category="additional",
)
else:
raise Exception(f"Unknown type of item:\n{item}")
def connect(
self,
@ -145,7 +214,7 @@ class Harness:
else:
to_pin_obj = None
self.cables[via_name].connect(from_pin_obj, via_wire, to_pin_obj)
self.cables[via_name]._connect(from_pin_obj, via_wire, to_pin_obj)
if from_name in self.connectors:
self.connectors[from_name].activate_pin(from_pin, Side.RIGHT)
if to_name in self.connectors:
@ -160,7 +229,7 @@ class Harness:
gv_html = gv_node_component(connector)
bgcolor = calculate_node_bgcolor(connector, self.options)
dot.node(
connector.name,
connector.designator,
label=f"<\n{gv_html}\n>",
bgcolor=bgcolor,
shape="box",
@ -192,7 +261,7 @@ class Harness:
bgcolor = calculate_node_bgcolor(cable, self.options)
style = "filled,dashed" if cable.category == "bundle" else "filled"
dot.node(
cable.name,
cable.designator,
label=f"<\n{gv_html}\n>",
bgcolor=bgcolor,
shape="box",
@ -200,7 +269,7 @@ class Harness:
)
# generate wire edges between component nodes and cable nodes
for connection in cable.connections:
for connection in cable._connections:
color, l1, l2, r1, r2 = gv_edge_wire(self, cable, connection)
dot.attr("edge", color=color)
if not (l1, l2) == (None, None):
@ -268,7 +337,8 @@ class Harness:
if "gv" in fmt:
graph.save(filename=f"{filename}.gv")
# BOM output
bomlist = bom_list(self.bom())
# bomlist = bom_list(self.bom())
bomlist = [[]]
if "tsv" in fmt:
open_file_write(f"{filename}.bom.tsv").write(tuplelist2tsv(bomlist))
if "csv" in fmt:
@ -288,7 +358,7 @@ class Harness:
elif "svg" in fmt:
Path(f"{filename}.tmp.svg").replace(f"{filename}.svg")
def bom(self):
if not self._bom:
self._bom = generate_bom(self)
return self._bom
# def bom(self):
# if not self._bom:
# self._bom = generate_bom(self)
# return self._bom

View File

@ -168,7 +168,7 @@ def parse(
)
designator = (
f"{AUTOGENERATED_PREFIX}"
"{template}_{autogenerated_designators[template]}"
f"{template}_{autogenerated_designators[template]}"
)
# check if redefining existing component to different template
if designator in designators_and_templates:
@ -288,7 +288,7 @@ def parse(
# generate new connector instance from template
check_type(designator, template, "connector")
harness.add_connector(
name=designator, **template_connectors[template]
designator=designator, **template_connectors[template]
)
elif designator in harness.cables: # existing cable instance
@ -296,7 +296,9 @@ def parse(
elif template in template_cables.keys():
# generate new cable instance from template
check_type(designator, template, "cable/arrow")
harness.add_cable(name=designator, **template_cables[template])
harness.add_cable(
designator=designator, **template_cables[template]
)
elif is_arrow(designator):
check_type(designator, template, "cable/arrow")
@ -366,7 +368,7 @@ def parse(
if "additional_bom_items" in yaml_data:
for line in yaml_data["additional_bom_items"]:
harness.add_bom_item(line)
harness.add_additional_bom_item(line)
if output_formats:
harness.output(filename=output_file, fmt=output_formats, view=False)

View File

@ -1,277 +1,55 @@
# -*- coding: utf-8 -*-
from dataclasses import asdict
from itertools import groupby
from typing import Any, Dict, List, Optional, Tuple, Union
from collections import namedtuple
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional, Union
from wireviz.DataClasses import AdditionalComponent, Cable, Connector
from wireviz.wv_gv_html import html_line_breaks
from wireviz.wv_helper import clean_whitespace, pn_info_string
BOM_HASH_FIELDS = "description unit partnumbers"
BomHash = namedtuple("BomHash", BOM_HASH_FIELDS)
BomHashList = namedtuple("BomHashList", BOM_HASH_FIELDS)
BOM_COLUMNS_ALWAYS = ("id", "description", "qty", "unit", "designators")
BOM_COLUMNS_OPTIONAL = ("pn", "manufacturer", "mpn", "supplier", "spn")
BOM_COLUMNS_IN_KEY = ("description", "unit") + BOM_COLUMNS_OPTIONAL
HEADER_PN = "P/N"
HEADER_MPN = "MPN"
HEADER_SPN = "SPN"
BOMKey = Tuple[str, ...]
BOMColumn = str # = Literal[*BOM_COLUMNS_ALWAYS, *BOM_COLUMNS_OPTIONAL]
BOMEntry = Dict[BOMColumn, Union[str, int, float, List[str], None]]
def optional_fields(part: Union[Connector, Cable, AdditionalComponent]) -> BOMEntry:
"""Return part field values for the optional BOM columns as a dict."""
part = asdict(part)
return {field: part.get(field) for field in BOM_COLUMNS_OPTIONAL}
def get_additional_component_table(
harness: "Harness", component: Union[Connector, Cable]
) -> List[str]:
"""Return a list of diagram node table row strings with additional components."""
rows = []
if component.additional_components:
rows.append(["Additional components"])
for part in component.additional_components:
common_args = {
"qty": part.qty * component.get_qty_multiplier(part.qty_multiplier),
"unit": part.unit,
"bgcolor": part.bgcolor,
}
if harness.options.mini_bom_mode:
id = get_bom_index(
harness.bom(),
bom_entry_key({**asdict(part), "description": part.description}),
)
rows.append(
component_table_entry(
f"#{id} ({part.type.rstrip()})", **common_args
)
)
else:
rows.append(
component_table_entry(
part.description, **common_args, **optional_fields(part)
)
)
return rows
def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOMEntry]:
"""Return a list of BOM entries with additional components."""
bom_entries = []
for part in component.additional_components:
bom_entries.append(
{
"description": part.description,
"qty": part.qty * component.get_qty_multiplier(part.qty_multiplier),
"unit": part.unit,
"designators": component.name if component.show_name else None,
**optional_fields(part),
}
)
return bom_entries
def bom_entry_key(entry: BOMEntry) -> BOMKey:
"""Return a tuple of string values from the dict that must be equal to join BOM entries."""
if "key" not in entry:
entry["key"] = tuple(
clean_whitespace(make_str(entry.get(c))) for c in BOM_COLUMNS_IN_KEY
)
return entry["key"]
def generate_bom(harness: "Harness") -> List[BOMEntry]:
"""Return a list of BOM entries generated from the harness."""
from wireviz.Harness import Harness # Local import to avoid circular imports
bom_entries = []
# connectors
for connector in harness.connectors.values():
if not connector.ignore_in_bom:
description = (
"Connector"
+ (f", {connector.type}" if connector.type else "")
+ (f", {connector.subtype}" if connector.subtype else "")
+ (f", {connector.pincount} pins" if connector.show_pincount else "")
+ (
f", xxx" # {translate_color(connector.color, harness.options.color_mode)}
if connector.color
else ""
)
)
bom_entries.append(
{
"description": description,
"designators": connector.name if connector.show_name else None,
**optional_fields(connector),
}
BomCategory = Enum(
"BomEntry", "CONNECTOR CABLE WIRE ADDITIONAL_INSIDE ADDITIONAL_OUTSIDE"
)
# add connectors aditional components to bom
bom_entries.extend(get_additional_component_bom(connector))
PartNumberInfo = namedtuple("PartNumberInfo", "pn manufacturer mpn supplier spn")
# cables
# TODO: If category can have other non-empty values than 'bundle', maybe it should be part of description?
for cable in harness.cables.values():
if not cable.ignore_in_bom:
if cable.category != "bundle":
# process cable as a single entity
description = (
"Cable"
+ (f", {cable.type}" if cable.type else "")
+ (f", {cable.wirecount}")
+ (
f" x {cable.gauge} {cable.gauge_unit}"
if cable.gauge
else " wires"
)
+ (" shielded" if cable.shield else "")
+ (
f", xxx" # {translate_color(cable.color, harness.options.color_mode)}
if cable.color
else ""
)
)
bom_entries.append(
{
"description": description,
"qty": cable.length,
"unit": cable.length_unit,
"designators": cable.name if cable.show_name else None,
**optional_fields(cable),
}
)
else:
# add each wire from the bundle to the bom
for index, color in enumerate(cable.colors):
description = (
"Wire"
+ (f", {cable.type}" if cable.type else "")
+ (f", {cable.gauge} {cable.gauge_unit}" if cable.gauge else "")
+ (
f", xxx" # {translate_color(color, harness.options.color_mode)}
if color
else ""
)
)
bom_entries.append(
{
"description": description,
"qty": cable.length,
"unit": cable.length_unit,
"designators": cable.name if cable.show_name else None,
**{
k: index_if_list(v, index)
for k, v in optional_fields(cable).items()
},
}
PART_NUMBER_HEADERS = PartNumberInfo(
pn="P/N", manufacturer=None, mpn="MPN", supplier=None, spn="SPN"
)
# add cable/bundles aditional components to bom
bom_entries.extend(get_additional_component_bom(cable))
# add harness aditional components to bom directly, as they both are List[BOMEntry]
bom_entries.extend(harness.additional_bom_items)
@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
# remove line breaks if present and cleanup any resulting whitespace issues
bom_entries = [
{k: clean_whitespace(v) for k, v in entry.items()} for entry in bom_entries
def partnumbers_to_list(partnumbers: PartNumberInfo) -> List[str]:
cell_contents = [
pn_info_string(PART_NUMBER_HEADERS.pn, None, partnumbers.pn),
pn_info_string(
PART_NUMBER_HEADERS.mpn, partnumbers.manufacturer, partnumbers.mpn
),
pn_info_string(PART_NUMBER_HEADERS.spn, partnumbers.supplier, partnumbers.spn),
]
# deduplicate bom
bom = []
for _, group in groupby(sorted(bom_entries, key=bom_entry_key), key=bom_entry_key):
group_entries = list(group)
designators = sum(
(make_list(entry.get("designators")) for entry in group_entries), []
)
total_qty = sum(entry.get("qty", 1) for entry in group_entries)
bom.append(
{
**group_entries[0],
"qty": round(total_qty, 3),
"designators": sorted(set(designators)),
}
)
# add an incrementing id to each bom entry
return [{**entry, "id": index} for index, entry in enumerate(bom, 1)]
if any(cell_contents):
return [html_line_breaks(cell) for cell in cell_contents]
else:
return None
def get_bom_index(bom: List[BOMEntry], target: BOMKey) -> int:
"""Return id of BOM entry or raise exception if not found."""
for entry in bom:
if bom_entry_key(entry) == target:
return entry["id"]
raise Exception("Internal error: No BOM entry found matching: " + "|".join(target))
def bom_list(bom: List[BOMEntry]) -> List[List[str]]:
"""Return list of BOM rows as lists of column strings with headings in top row."""
keys = list(BOM_COLUMNS_ALWAYS) # Always include this fixed set of BOM columns.
for fieldname in BOM_COLUMNS_OPTIONAL:
# Include only those optional BOM columns that are in use.
if any(entry.get(fieldname) for entry in bom):
keys.append(fieldname)
# Custom mapping from internal name to BOM column headers.
# Headers not specified here are generated by capitilising the internal name.
bom_headings = {
"pn": HEADER_PN,
"mpn": HEADER_MPN,
"spn": HEADER_SPN,
}
return [
[bom_headings.get(k, k.capitalize()) for k in keys]
] + [ # Create header row with key names
[make_str(entry.get(k)) for k in keys] for entry in bom
] # Create string list for each entry row
def component_table_entry(
type: str,
qty: Union[int, float],
unit: Optional[str] = None,
bgcolor: Optional[str] = None,
pn: Optional[str] = None,
manufacturer: Optional[str] = None,
mpn: Optional[str] = None,
supplier: Optional[str] = None,
spn: Optional[str] = None,
) -> str:
"""Return a diagram node table row string with an additional component."""
part_number_list = [
pn_info_string(HEADER_PN, None, pn),
pn_info_string(HEADER_MPN, manufacturer, mpn),
pn_info_string(HEADER_SPN, supplier, spn),
]
output = (
f"{qty}"
+ (f" {unit}" if unit else "")
+ f" x {type}"
+ ("<br/>" if any(part_number_list) else "")
+ (", ".join([pn for pn in part_number_list if pn]))
)
# format the above output as left aligned text in a single visible cell
# indent is set to two to match the indent in the generated html table
return f"""<table border="0" cellspacing="0" cellpadding="3" cellborder="1"{html_bgcolor_attr(bgcolor)}><tr>
<td align="left" balign="left">{html_line_breaks(output)}</td>
</tr></table>"""
def index_if_list(value: Any, index: int) -> Any:
"""Return the value indexed if it is a list, or simply the value otherwise."""
return value[index] if isinstance(value, list) else value
def make_list(value: Any) -> list:
"""Return value if a list, empty list if None, or single element list otherwise."""
return value if isinstance(value, list) else [] if value is None else [value]
def make_str(value: Any) -> str:
"""Return comma separated elements if a list, empty string if None, or value as a string otherwise."""
return ", ".join(str(element) for element in make_list(value))
def pn_info_string(
header: str, name: Optional[str], number: Optional[str]
) -> Optional[str]:
"""Return the company name and/or the part number in one single string or None otherwise."""
number = str(number).strip() if number is not None else ""
if name or number:
return f'{name if name else header}{": " + number if number else ""}'
else:
return None

View File

@ -14,16 +14,14 @@ from wireviz.DataClasses import (
MateComponent,
MatePin,
Options,
PartNumberInfo,
ShieldClass,
WireClass,
)
from wireviz.wv_helper import pn_info_string, remove_links
from wireviz.wv_bom import partnumbers_to_list
from wireviz.wv_helper import remove_links
from wireviz.wv_table_util import * # TODO: explicitly import each needed tag later
HEADER_PN = "P/N"
HEADER_MPN = "MPN"
HEADER_SPN = "SPN"
def gv_node_component(component: Component) -> Table:
# If no wires connected (except maybe loop wires)?
@ -33,12 +31,12 @@ def gv_node_component(component: Component) -> Table:
# generate all rows to be shown in the node
if component.show_name:
str_name = f"{remove_links(component.name)}"
str_name = f"{remove_links(component.designator)}"
line_name = colored_cell(str_name, component.bgcolor_title)
else:
line_name = None
line_pn = part_number_str_list(component)
line_pn = partnumbers_to_list(component.partnumbers)
is_simple_connector = (
isinstance(component, Connector) and component.style == "simple"
@ -204,8 +202,8 @@ def gv_connector_loops(connector: Connector) -> List:
else:
raise Exception("No side for loops")
for loop in connector.loops:
head = f"{connector.name}:p{loop[0]}{loop_side}:{loop_dir}"
tail = f"{connector.name}:p{loop[1]}{loop_side}:{loop_dir}"
head = f"{connector.designator}:p{loop[0]}{loop_side}:{loop_dir}"
tail = f"{connector.designator}:p{loop[1]}{loop_side}:{loop_dir}"
loop_edges.append((head, tail))
return loop_edges
@ -230,7 +228,7 @@ def gv_conductor_table(cable) -> Table:
wireinfo.append(wire.label)
ins, outs = [], []
for conn in cable.connections:
for conn in cable._connections:
if conn.via.id == wire.id:
if conn.from_ is not None:
ins.append(str(conn.from_))
@ -407,18 +405,6 @@ def colored_cell(contents, bgcolor) -> Td:
return Td(contents, bgcolor=bgcolor.html)
def part_number_str_list(component: Component) -> List[str]:
cell_contents = [
pn_info_string(HEADER_PN, None, component.pn),
pn_info_string(HEADER_MPN, component.manufacturer, component.mpn),
pn_info_string(HEADER_SPN, component.supplier, component.spn),
]
if any(cell_contents):
return [html_line_breaks(cell) for cell in cell_contents]
else:
return None
def colorbar_cell(color) -> Td:
return Td("", bgcolor=color.html, width=4)

View File

@ -176,14 +176,3 @@ def smart_file_resolve(filename: str, possible_paths: (str, List[str])) -> Path:
f"{filename} was not found in any of the following locations: \n"
+ "\n".join([str(x) for x in possible_paths])
)
def pn_info_string(
header: str, name: Optional[str], number: Optional[str]
) -> Optional[str]:
"""Return the company name and/or the part number in one single string or None otherwise."""
number = str(number).strip() if number is not None else ""
if name or number:
return f'{name if name else header}{": " + number if number else ""}'
else:
return None