Compare commits

...

38 Commits

Author SHA1 Message Date
Daniel Rojas
9821ca31f7 Update demo02 to showcase more features
zzz
2021-03-23 18:09:16 +01:00
Daniel Rojas
37bf5309f0 Allow line breaks in additional parameters 2021-03-23 18:09:16 +01:00
Daniel Rojas
b4a0ae50b8 Implement additional parameter table
as proposed in #222
2021-03-23 15:57:38 +01:00
Daniel Rojas
1f86c97c67 Fix rendering bug for empty add.comp. table 2021-03-23 15:15:20 +01:00
Daniel Rojas
09d5496ca4 Add new bom_item_number property to components 2021-03-23 13:04:31 +01:00
Daniel Rojas
6aa53f6ce4 Remove unused columns in BOM table 2021-03-23 12:40:56 +01:00
Daniel Rojas
aaf88d7601 Remove vertical separators in BOM table 2021-03-23 12:15:03 +01:00
Daniel Rojas
f928bc73ca Add BOM bubble cell to cables 2021-03-23 11:50:02 +01:00
Daniel Rojas
7e168ea775 Improve GraphViz output, remove debug print 2021-03-23 11:26:13 +01:00
Daniel Rojas
4338b7a1c5 Remove header row for additional components 2021-03-23 11:22:09 +01:00
Daniel Rojas
9a93d86058 remove component_table_entry() as separate func.
to simplyfy code
2021-03-23 11:19:20 +01:00
Daniel Rojas
0dc02fe8b5 Correctly determine qty 2021-03-23 11:14:49 +01:00
Daniel Rojas
53fefa8f3c Add note attribute to additional components 2021-03-23 11:04:33 +01:00
Daniel Rojas
e61d14ba43 Create bom_bubble() function 2021-03-23 11:00:26 +01:00
Daniel Rojas
8c26c8fbbd New addit. compo. BOM table proof of concept 2021-03-23 10:49:56 +01:00
KV
523d0c659e Group common function arguments into a dict 2021-03-14 05:49:33 +01:00
KV
183a9432c3 Move repeated code into new optional_fields() function 2021-02-20 21:30:10 +01:00
KV
30dbd9573b Rename the 'item' key to 'description' in all BOMEntry dicts
This way, both BOM and harness.additional_bom_items uses the same
set of keys in their dict entries. This was originally suggested
in a #115 review, but had too many issues to be implemented then.
2021-01-06 22:53:33 +01:00
KV
8e7c48d42e Eliminate local variable 2021-01-05 04:20:44 +01:00
KV
2e244981fe Move default qty value=1 to BOM deduplication 2021-01-05 04:12:05 +01:00
KV
d15eeb1f9f Build output string in one big expression
Build output string in component_table_entry() as the similar strings
in generate_bom(). Repeating a couple of minor if-expressions is small
cost to obtain a more compact and readable main expression.
2021-01-03 06:13:09 +01:00
KV
d6d0d2a486 Rename extra variable to part for consistency 2021-01-03 06:13:09 +01:00
KV
c22c42e722 Add BOMEntry type alias
This type alias describes the possible types of keys and values in
the dict representing a BOM entry.
2021-01-03 06:13:09 +01:00
KV
12e570fdad Add function type hints and doc strings 2021-01-03 06:13:09 +01:00
KV
1d653c44ed Replace accumulation loop with sum expressions
Make a list from the group iterator for reusage in sum expressions
and to pick first group entry. The expected group sizes are very small,
so performance loss by creating a temporary list should be neglectable.

Alternativly, itertools.tee(group, 3) could be called to triplicate
the iterator, but it was not chosen for readability reasons.
2020-12-30 08:46:01 +01:00
KV
96d393dfb7 Use a generator expressions and raise exception if failing
Seems to be the most popular search alternative:
 https://stackoverflow.com/questions/8653516/python-list-of-dictionaries-search

Raising StopIteration if not found is better than returning None
to detect such an internal error more easily.
2020-12-30 08:46:01 +01:00
KV
f13f8a7dd7 Make the BOM grouping function return string tuple for sorting 2020-12-30 08:46:01 +01:00
KV
cdca708da9 Move BOM sorting above grouping to use groupby()
- Use one common entry loop to consume iterator only once.
- Use same key function for sort() and groupby(),
  except replace None with empty string when sorting.
2020-12-30 08:46:01 +01:00
KV
10b1198b77 Move out code from inner loop into helper functions 2020-12-30 08:46:00 +01:00
KV
74462cd225 Remove parentheses around return expressions
https://stackoverflow.com/questions/4978567/should-a-return-statement-have-parentheses
2020-12-30 08:46:00 +01:00
KV
e1d7babf63 Simplify deduplication and sorting of collected designators 2020-12-30 08:46:00 +01:00
KV
d2f8034961 Simplify collecting designators for a joined BOM entry
Assign input designators once to a temporary variable for easy reusage.
2020-12-30 08:46:00 +01:00
KV
6378b96541 Simplify BOM header row logic 2020-12-30 08:46:00 +01:00
KV
347f1e3031 Redefine the common lambda to an ordinary function 2020-12-30 08:46:00 +01:00
KV
da453db9f0 Convert dataclass object to dict to use the same lambda 2020-12-30 08:46:00 +01:00
KV
45b13ef797 Use the same lambda in get_bom_index() as for deduplicating BOM
Move the lambda declaration out of the function scope for common
access from two different functions.
2020-12-30 08:46:00 +01:00
KV
6525537312 Simplify get_bom_index() parameters
- Use the actual BOM as first parameter instead of the whole harness.
- Use a whole AdditionalComponent as second parameter instead of each
  attribute separately.
2020-12-30 08:46:00 +01:00
KV
0f3b5e9edf Skip assignment and return expression directly 2020-12-30 08:46:00 +01:00
6 changed files with 263 additions and 160 deletions

View File

@ -1,69 +1,126 @@
templates: # defining templates to be used later on
- &molex_f
type: Molex KK 254
subtype: female
- &con_i2c
pinlabels: [GND, +5V, SCL, SDA]
- &wire_i2c
category: bundle
gauge: 0.14 mm2
colors: [BK, RD, YE, GN]
connectors: connectors:
X1: X1:
<<: *molex_f # copying items from the template type: KK 254
pinlabels: [GND, +5V, SCL, SDA, MISO, MOSI, SCK, N/C] pn: CON-245-8
manufacturer: Molex
mpn: '0022013087'
subtype: female
pincount: 8
pinlabels: [Audio L, Audio R, Audio GND, N/C, I2C GND, I2C +5V, SCL, SDA]
additional_parameters:
Sleeving removal: 20 mm
Insulation removal: 2 mm
additional_components:
-
type: Crimp
pn: CRI-254
manufacturer: Molex
mpn: '008500032'
qty_multiplier: populated
-
type: Label
pn: LAB-444
note: '"C745-X1"'
notes: |
- Attach to main PCB
- Ensure proper contact
- Clamp down cables after attaching
X2: X2:
<<: *molex_f type: 3.5 mm
<<: *con_i2c # it is possible to copy from more than one template subtype: jack
color: BK
pins: [T,R,S]
show_pincount: false
pinlabels: [L, R, GND]
image:
src: resources/stereo-phone-plug-TRS.png
caption: Tip, Ring, and Sleeve
X3: X3:
<<: *molex_f type: KK 254
<<: *con_i2c subtype: female
pinlabels: [VCC, GND, SCL, SDA]
pincolors: [RD, BK, GN, BU]
X4: X4:
<<: *molex_f type: D-Sub
pinlabels: [GND, +12V, MISO, MOSI, SCK] subtype: female
ferrule_crimp: pincount: 9
pn: CON-D9-F
pinlabels: [GND, +5V, SCL, SDA, N/C, +12V IN, GND, +12V OUT, GND]
additional_components:
-
type: Casing, plastic
pn: CAS-D9
-
type: Mounting screws, M3 x 8
qty: 2
F:
style: simple style: simple
autogenerate: true autogenerate: true
type: Crimp ferrule type: Crimp ferrule
subtype: 0.25 mm² subtype: 0.5 mm²
color: YE color: OG
cables: cables:
W1: W1:
<<: *wire_i2c wirecount: 3
length: 0.2 shield: true
show_equiv: true length: 0.5
W2:
<<: *wire_i2c
length: 0.4
show_equiv: true
W3:
category: bundle
gauge: 0.14 mm2
length: 0.3
colors: [BK, BU, OG, VT]
show_equiv: true
W4:
gauge: 0.25 mm2 gauge: 0.25 mm2
length: 0.3
colors: [BK, RD]
show_equiv: true show_equiv: true
colors: [WH, RD, BK]
color: GY
additional_components:
-
type: Heatshrink D=5mm
qty: 15
unit: mm
note: left
-
type: Heatshrink D=5mm
qty: 25
unit: mm
note: right
W2: &wire_i2c
wirecount: 4
length: 0.2
gauge: 0.25 mm2
show_equiv: true
color_code: IEC
W3:
<<: *wire_i2c
color_code: DIN
W4: &wire_power
category: bundle
show_name: false
wirecount: 2
colors: [RD, BK]
gauge: 0.5 mm2
show_equiv: true
length: 1.0
additional_parameters:
Twist rate: 10/m
Twist direction: CCW
W5:
<<: *wire_power
connections: connections:
- -
- X1: [1-4] - X1: [Audio L, Audio R, Audio GND, Audio GND]
- W1: [1-4] - W1: [1-3,s]
- X2: [1-4] - X2: [T,R,S,S]
- -
- X1: [1-4] - X1: [I2C GND, I2C +5V, SCL, SDA]
- W2: [1-4] - W2: [1-4]
- X3: [1-4] - X3: [GND, VCC, SCL, SDA]
- -
- X1: [1,5-7] - X1: [I2C GND, I2C +5V, SCL, SDA]
- W3: [1-4] - W3: [1-4]
- X4: [1,3-5] - X4: [1,2,3,4]
- -
- ferrule_crimp - F
- W4: [1,2] - W4: [1,2]
- X4: [1,2] - X4: [6,7]
-
- X4: [8,9]
- W5: [1,2]
- F

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional, List, Tuple, Union from typing import Optional, List, Dict, Tuple, Union
from dataclasses import dataclass, field, InitVar from dataclasses import dataclass, field, InitVar
from pathlib import Path from pathlib import Path
@ -76,6 +76,7 @@ class AdditionalComponent:
qty: float = 1 qty: float = 1
unit: Optional[str] = None unit: Optional[str] = None
qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None
note: Optional[str] = None
@property @property
def description(self) -> str: def description(self) -> str:
@ -93,6 +94,7 @@ class Connector:
type: Optional[MultilineHypertext] = None type: Optional[MultilineHypertext] = None
subtype: Optional[MultilineHypertext] = None subtype: Optional[MultilineHypertext] = None
pincount: Optional[int] = None pincount: Optional[int] = None
additional_parameters: Optional[Dict] = None
image: Optional[Image] = None image: Optional[Image] = None
notes: Optional[MultilineHypertext] = None notes: Optional[MultilineHypertext] = None
pinlabels: List[Pin] = field(default_factory=list) pinlabels: List[Pin] = field(default_factory=list)
@ -104,6 +106,7 @@ class Connector:
hide_disconnected_pins: bool = False hide_disconnected_pins: bool = False
autogenerate: bool = False autogenerate: bool = False
loops: List[List[Pin]] = field(default_factory=list) loops: List[List[Pin]] = field(default_factory=list)
bom_item_number: Optional[int] = None
ignore_in_bom: bool = False ignore_in_bom: bool = False
additional_components: List[AdditionalComponent] = field(default_factory=list) additional_components: List[AdditionalComponent] = field(default_factory=list)
@ -180,6 +183,7 @@ class Cable:
color: Optional[Color] = None color: Optional[Color] = None
wirecount: Optional[int] = None wirecount: Optional[int] = None
shield: Union[bool, Color] = False shield: Union[bool, Color] = False
additional_parameters: Optional[Dict] = None
image: Optional[Image] = None image: Optional[Image] = None
notes: Optional[MultilineHypertext] = None notes: Optional[MultilineHypertext] = None
colors: List[Colors] = field(default_factory=list) colors: List[Colors] = field(default_factory=list)
@ -188,6 +192,7 @@ class Cable:
show_name: bool = True show_name: bool = True
show_wirecount: bool = True show_wirecount: bool = True
show_wirenumbers: Optional[bool] = None show_wirenumbers: Optional[bool] = None
bom_item_number: Optional[int] = None
ignore_in_bom: bool = False ignore_in_bom: bool = False
additional_components: List[AdditionalComponent] = field(default_factory=list) additional_components: List[AdditionalComponent] = field(default_factory=list)

View File

@ -12,8 +12,8 @@ from wireviz import wv_colors, __version__, APP_NAME, APP_URL
from wireviz.DataClasses import Connector, Cable from wireviz.DataClasses import Connector, Cable
from wireviz.wv_colors import get_color_hex from wireviz.wv_colors import get_color_hex
from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \ from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \
html_caption, remove_links, html_line_breaks html_caption, remove_links, html_line_breaks, bom_bubble, nested_html_table_dict
from wireviz.wv_bom import manufacturer_info_field, component_table_entry, \ from wireviz.wv_bom import manufacturer_info_field, \
get_additional_component_table, bom_list, generate_bom get_additional_component_table, bom_list, generate_bom
from wireviz.wv_html import generate_html_output from wireviz.wv_html import generate_html_output
from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \ from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \
@ -24,7 +24,8 @@ class Harness:
def __init__(self): def __init__(self):
self.color_mode = 'SHORT' self.color_mode = 'SHORT'
self.mini_bom_mode = True self.show_part_numbers = True # TODO: Make configurable via YAML
self.show_bom_item_numbers = True # TODO: Make configurable via YAML
self.connectors = {} self.connectors = {}
self.cables = {} self.cables = {}
self._bom = [] # Internal Cache for generated bom self._bom = [] # Internal Cache for generated bom
@ -114,17 +115,20 @@ class Harness:
html = [] html = []
rows = [[remove_links(connector.name) if connector.show_name else None], rows = [[remove_links(connector.name) if connector.show_name else None],
[f'P/N: {remove_links(connector.pn)}' if connector.pn else None, [bom_bubble(connector.bom_item_number) if self.show_bom_item_numbers else None, # TODO: Show actual BOM number
html_line_breaks(manufacturer_info_field(connector.manufacturer, connector.mpn))], html_line_breaks(connector.type),
[html_line_breaks(connector.type),
html_line_breaks(connector.subtype), html_line_breaks(connector.subtype),
f'{connector.pincount}-pin' if connector.show_pincount else None, f'{connector.pincount}-pin' if connector.show_pincount else None,
connector.color, html_colorbar(connector.color)], connector.color, html_colorbar(connector.color)],
[f'P/N: {remove_links(connector.pn)}' if connector.pn else None,
html_line_breaks(manufacturer_info_field(connector.manufacturer, connector.mpn))] if self.show_part_numbers else None,
nested_html_table_dict(connector.additional_parameters),
'<!-- connector table -->' if connector.style != 'simple' else None, '<!-- connector table -->' if connector.style != 'simple' else None,
[html_image(connector.image)], [html_image(connector.image)],
[html_caption(connector.image)]] [html_caption(connector.image)]]
rows.extend(get_additional_component_table(self, connector)) rows.append(get_additional_component_table(self, connector))
rows.append([html_line_breaks(connector.notes)]) rows.append([html_line_breaks(connector.notes)])
html.extend(nested_html_table(rows)) html.extend(nested_html_table(rows))
if connector.style != 'simple': if connector.style != 'simple':
@ -195,21 +199,23 @@ class Harness:
awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)' awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)'
rows = [[remove_links(cable.name) if cable.show_name else None], rows = [[remove_links(cable.name) if cable.show_name else None],
[f'P/N: {remove_links(cable.pn)}' if (cable.pn and not isinstance(cable.pn, list)) else None, [bom_bubble(cable.bom_item_number) if self.show_bom_item_numbers else None, # TODO: Show actual BOM number
html_line_breaks(manufacturer_info_field( html_line_breaks(cable.type),
cable.manufacturer if not isinstance(cable.manufacturer, list) else None,
cable.mpn if not isinstance(cable.mpn, list) else None))],
[html_line_breaks(cable.type),
f'{cable.wirecount}x' if cable.show_wirecount else None, f'{cable.wirecount}x' if cable.show_wirecount else None,
f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else None, f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else None,
'+ S' if cable.shield else None, '+ S' if cable.shield else None,
f'{cable.length} {cable.length_unit}' if cable.length > 0 else None, f'{cable.length} {cable.length_unit}' if cable.length > 0 else None,
cable.color, html_colorbar(cable.color)], cable.color, html_colorbar(cable.color)],
[f'P/N: {remove_links(cable.pn)}' if (cable.pn and not isinstance(cable.pn, list)) else None,
html_line_breaks(manufacturer_info_field(
cable.manufacturer if not isinstance(cable.manufacturer, list) else None,
cable.mpn if not isinstance(cable.mpn, list) else None))],
nested_html_table_dict(cable.additional_parameters),
'<!-- wire table -->', '<!-- wire table -->',
[html_image(cable.image)], [html_image(cable.image)],
[html_caption(cable.image)]] [html_caption(cable.image)]]
rows.extend(get_additional_component_table(self, cable)) rows.append(get_additional_component_table(self, cable)) # TODO: reimplement
rows.append([html_line_breaks(cable.notes)]) rows.append([html_line_breaks(cable.notes)])
html.extend(nested_html_table(rows)) html.extend(nested_html_table(rows))

View File

@ -1,42 +1,87 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import List, Union from dataclasses import asdict
from collections import Counter from itertools import groupby
from typing import Any, Dict, List, Optional, Tuple, Union
from wireviz.DataClasses import Connector, Cable from wireviz.DataClasses import AdditionalComponent, Connector, Cable
from wireviz.wv_gv_html import html_line_breaks from wireviz.wv_gv_html import html_line_breaks, bom_bubble
from wireviz.wv_helper import clean_whitespace from wireviz.wv_helper import clean_whitespace
def get_additional_component_table(harness, component: Union[Connector, Cable]) -> List[str]: BOMColumn = str # = Literal['id', 'description', 'qty', 'unit', 'designators', 'pn', 'manufacturer', 'mpn']
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."""
return {'pn': part.pn, 'manufacturer': part.manufacturer, 'mpn': part.mpn}
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 = [] rows = []
if component.additional_components: if component.additional_components:
rows.append(["Additional components"]) parts = []
for extra in component.additional_components: for part in component.additional_components:
qty = extra.qty * component.get_qty_multiplier(extra.qty_multiplier) # if True:
if harness.mini_bom_mode: # id = get_bom_index(harness.bom(), part)
id = get_bom_index(harness, extra.description, extra.unit, extra.manufacturer, extra.mpn, extra.pn) # rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', **common_args))
rows.append(component_table_entry(f'#{id} ({extra.type.rstrip()})', qty, extra.unit)) # else:
else: # rows.append(component_table_entry(part.description, **common_args, **optional_fields(part)))
rows.append(component_table_entry(extra.description, qty, extra.unit, extra.pn, extra.manufacturer, extra.mpn)) id = get_bom_index(harness.bom(), part)
return(rows) manufacturer_str = manufacturer_info_field(part.manufacturer, part.mpn)
columns = []
if harness.show_bom_item_numbers:
columns.append(bom_bubble(id))
columns.append(f'{part.qty * component.get_qty_multiplier(part.qty_multiplier)}' + (f' {part.unit}' if part.unit else 'x'))
columns.append(f'{part.type}')
if harness.show_part_numbers:
columns.append(f'P/N: {part.pn}' if part.pn else '')
columns.append(f'{manufacturer_str}' if manufacturer_str else '')
columns.append(f'{part.note}' if part.note else '')
def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dict]: parts.append(columns)
# remove unused columns
transp = list(map(list, zip(*parts))) # transpose list
transp = [item for item in transp if any(item)] # remove empty rows (easier)
parts = list(map(list, zip(*transp))) # transpose back
# generate HTML output
for part in parts:
rowstr = '\n <tr>\n'
for index, column in enumerate(part):
sides = "tbl" if index == 0 else "tbr" if index == len(part) -1 else "tb"
rowstr = rowstr + f' <td align="left" balign="left" sides="{sides}">{html_line_breaks(column)}</td>\n'
rowstr = rowstr + ' </tr>'
rows.append(rowstr)
pre = '<table border="0" cellspacing="0" cellpadding="3" cellborder="1">'
post = '\n </table>'
if len(rows) > 0:
tbl = pre + ''.join(rows) + post
else:
tbl = None
return tbl
def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOMEntry]:
"""Return a list of BOM entries with additional components."""
bom_entries = [] bom_entries = []
for part in component.additional_components: for part in component.additional_components:
qty = part.qty * component.get_qty_multiplier(part.qty_multiplier)
bom_entries.append({ bom_entries.append({
'item': part.description, 'description': part.description,
'qty': qty, 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier),
'unit': part.unit, 'unit': part.unit,
'manufacturer': part.manufacturer, 'designators': component.name if component.show_name else None,
'mpn': part.mpn, **optional_fields(part),
'pn': part.pn,
'designators': component.name if component.show_name else None
}) })
return(bom_entries) return bom_entries
def generate_bom(harness): def bom_types_group(entry: BOMEntry) -> Tuple[str, ...]:
"""Return a tuple of string values from the dict that must be equal to join BOM entries."""
return tuple(make_str(entry.get(key)) for key in ('description', 'unit', 'manufacturer', 'mpn', 'pn'))
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 from wireviz.Harness import Harness # Local import to avoid circular imports
bom_entries = [] bom_entries = []
# connectors # connectors
@ -48,15 +93,15 @@ def generate_bom(harness):
+ (f', {connector.pincount} pins' if connector.show_pincount else '') + (f', {connector.pincount} pins' if connector.show_pincount else '')
+ (f', {connector.color}' if connector.color else '')) + (f', {connector.color}' if connector.color else ''))
bom_entries.append({ bom_entries.append({
'item': description, 'qty': 1, 'unit': None, 'designators': connector.name if connector.show_name else None, 'description': description, 'designators': connector.name if connector.show_name else None,
'manufacturer': connector.manufacturer, 'mpn': connector.mpn, 'pn': connector.pn **optional_fields(connector),
}) })
# add connectors aditional components to bom # add connectors aditional components to bom
bom_entries.extend(get_additional_component_bom(connector)) bom_entries.extend(get_additional_component_bom(connector))
# cables # cables
# TODO: If category can have other non-empty values than 'bundle', maybe it should be part of item name? # TODO: If category can have other non-empty values than 'bundle', maybe it should be part of description?
for cable in harness.cables.values(): for cable in harness.cables.values():
if not cable.ignore_in_bom: if not cable.ignore_in_bom:
if cable.category != 'bundle': if cable.category != 'bundle':
@ -67,8 +112,8 @@ def generate_bom(harness):
+ (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires') + (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires')
+ (' shielded' if cable.shield else '')) + (' shielded' if cable.shield else ''))
bom_entries.append({ bom_entries.append({
'item': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None,
'manufacturer': cable.manufacturer, 'mpn': cable.mpn, 'pn': cable.pn **optional_fields(cable),
}) })
else: else:
# add each wire from the bundle to the bom # add each wire from the bundle to the bom
@ -78,101 +123,66 @@ def generate_bom(harness):
+ (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '') + (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '')
+ (f', {color}' if color else '')) + (f', {color}' if color else ''))
bom_entries.append({ bom_entries.append({
'item': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None, 'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None,
'manufacturer': index_if_list(cable.manufacturer, index), **{k: index_if_list(v, index) for k, v in optional_fields(cable).items()},
'mpn': index_if_list(cable.mpn, index), 'pn': index_if_list(cable.pn, index)
}) })
# add cable/bundles aditional components to bom # add cable/bundles aditional components to bom
bom_entries.extend(get_additional_component_bom(cable)) bom_entries.extend(get_additional_component_bom(cable))
for item in harness.additional_bom_items: # add harness aditional components to bom directly, as they both are List[BOMEntry]
bom_entries.append({ bom_entries.extend(harness.additional_bom_items)
'item': item.get('description', ''), 'qty': item.get('qty', 1), 'unit': item.get('unit'), 'designators': item.get('designators'),
'manufacturer': item.get('manufacturer'), 'mpn': item.get('mpn'), 'pn': item.get('pn')
})
# remove line breaks if present and cleanup any resulting whitespace issues # 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] bom_entries = [{k: clean_whitespace(v) for k, v in entry.items()} for entry in bom_entries]
# deduplicate bom # deduplicate bom
bom = [] bom = []
bom_types_group = lambda bt: (bt['item'], bt['unit'], bt['manufacturer'], bt['mpn'], bt['pn']) for _, group in groupby(sorted(bom_entries, key=bom_types_group), key=bom_types_group):
for group in Counter([bom_types_group(v) for v in bom_entries]): group_entries = list(group)
group_entries = [v for v in bom_entries if bom_types_group(v) == group] designators = sum((make_list(entry.get('designators')) for entry in group_entries), [])
designators = [] total_qty = sum(entry.get('qty', 1) for entry in group_entries)
for group_entry in group_entries: bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': sorted(set(designators))})
if group_entry.get('designators'):
if isinstance(group_entry['designators'], List):
designators.extend(group_entry['designators'])
else:
designators.append(group_entry['designators'])
designators = list(dict.fromkeys(designators)) # remove duplicates
designators.sort()
total_qty = sum(entry['qty'] for entry in group_entries)
bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': designators})
bom = sorted(bom, key=lambda k: k['item']) # sort list of dicts by their values (https://stackoverflow.com/a/73050) # add an incrementing id to each bom entry
return [{**entry, 'id': index} for index, entry in enumerate(bom, 1)]
# add an incrementing id to each bom item def get_bom_index(bom: List[BOMEntry], part: AdditionalComponent) -> int:
bom = [{**entry, 'id': index} for index, entry in enumerate(bom, 1)] """Return id of BOM entry or raise StopIteration if not found."""
return bom
def get_bom_index(harness, item, unit, manufacturer, mpn, pn):
# Remove linebreaks and clean whitespace of values in search # Remove linebreaks and clean whitespace of values in search
target = tuple(clean_whitespace(v) for v in (item, unit, manufacturer, mpn, pn)) target = tuple(clean_whitespace(v) for v in bom_types_group({**asdict(part), 'description': part.description}))
for entry in harness.bom(): return next(entry['id'] for entry in bom if bom_types_group(entry) == target)
if (entry['item'], entry['unit'], entry['manufacturer'], entry['mpn'], entry['pn']) == target:
return entry['id']
return None
def bom_list(bom): def bom_list(bom: List[BOMEntry]) -> List[List[str]]:
keys = ['id', 'item', 'qty', 'unit', 'designators'] # these BOM columns will always be included """Return list of BOM rows as lists of column strings with headings in top row."""
for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM item actually uses them keys = ['id', 'description', 'qty', 'unit', 'designators'] # these BOM columns will always be included
for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM entry actually uses them
if any(entry.get(fieldname) for entry in bom): if any(entry.get(fieldname) for entry in bom):
keys.append(fieldname) keys.append(fieldname)
bom_list = []
# list of staic bom header names, headers not specified here are generated by capitilising the internal name # list of staic bom header names, headers not specified here are generated by capitilising the internal name
bom_headings = { bom_headings = {
"description": "Item", # TODO: Remove this line to use 'Description' in BOM heading.
"pn": "P/N", "pn": "P/N",
"mpn": "MPN" "mpn": "MPN"
} }
bom_list.append([(bom_headings[k] if k in bom_headings else k.capitalize()) for k in keys]) # create header row with keys return ([[bom_headings.get(k, k.capitalize()) for k in keys]] + # Create header row with key names
for item in bom: [[make_str(entry.get(k)) for k in keys] for entry in bom]) # Create string list for each entry row
item_list = [item.get(key, '') for key in keys] # fill missing values with blanks
item_list = [', '.join(subitem) if isinstance(subitem, List) else subitem for subitem in item_list] # convert any lists into comma separated strings
item_list = ['' if subitem is None else subitem for subitem in item_list] # if a field is missing for some (but not all) BOM items
bom_list.append(item_list)
return bom_list
def component_table_entry(type, qty, unit=None, pn=None, manufacturer=None, mpn=None): def manufacturer_info_field(manufacturer: Optional[str], mpn: Optional[str]) -> Optional[str]:
output = f'{qty}' """Return the manufacturer and/or the mpn in one single string or None otherwise."""
if unit:
output += f' {unit}'
output += f' x {type}'
# print an extra line with part and manufacturer information if provided
manufacturer_str = manufacturer_info_field(manufacturer, mpn)
if pn or manufacturer_str:
output += '<br/>'
if pn:
output += f'P/N: {pn}'
if manufacturer_str:
output += ', '
if manufacturer_str:
output += manufacturer_str
output = html_line_breaks(output)
# 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"><tr>
<td align="left" balign="left">{output}</td>
</tr></table>'''
def manufacturer_info_field(manufacturer, mpn):
if manufacturer or mpn: if manufacturer or mpn:
return f'{manufacturer if manufacturer else "MPN"}{": " + str(mpn) if mpn else ""}' return f'{manufacturer if manufacturer else "MPN"}{": " + str(mpn) if mpn else ""}'
else: else:
return None return None
# Return the value indexed if it is a list, or simply the value otherwise. def index_if_list(value: Any, index: int) -> Any:
def index_if_list(value, index): """Return the value indexed if it is a list, or simply the value otherwise."""
return value[index] if isinstance(value, list) else value 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))

View File

@ -1,12 +1,25 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import List, Union from typing import List, Dict, Union
import re import re
from wireviz.wv_colors import translate_color from wireviz.wv_colors import translate_color
from wireviz.wv_helper import remove_links from wireviz.wv_helper import remove_links
def nested_html_table_dict(rows):
if isinstance(rows, Dict):
html = []
html.append('<table border="0" cellspacing="0" cellpadding="3" cellborder="1">')
for (key, value) in rows.items():
html.append(f' <tr><td align="left" balign="left">{key}</td>')
html.append(f' <td align="left" balign="left">{html_line_breaks(value)}</td></tr>')
html.append(' </table>')
out = '\n'.join(html)
else:
out = None
return out
def nested_html_table(rows): def nested_html_table(rows):
# input: list, each item may be scalar or list # input: list, each item may be scalar or list
# output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar # output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar
@ -64,3 +77,6 @@ def html_size_attr(image):
def html_line_breaks(inp): def html_line_breaks(inp):
return remove_links(inp).replace('\n', '<br />') if isinstance(inp, str) else inp return remove_links(inp).replace('\n', '<br />') if isinstance(inp, str) else inp
def bom_bubble(inp):
return(f'<table border="0"><tr><td border="1" style="rounded">{inp}</td></tr></table>')

9
test/bomnumbertest.bom.tsv generated Normal file
View File

@ -0,0 +1,9 @@
Id Item Qty Unit Designators P/N Manufacturer MPN
1 Cable, 4 x 0.25 mm² 99 m W1 qwerty uiop
2 Connector, Plug, 4 pins 1 X1 123 ACME ABC
3 Connector, Receptacle, 4 pins 1 X2 234 ACME DEF
4 Crimp 1 X2 876 ACME WVU
5 Crimp 4 X1 987 ACME ZYX
6 Housing 1 X1 345 OTHER
7 Label 1 X1
8 Sleeving 5 m W1
1 Id Item Qty Unit Designators P/N Manufacturer MPN
2 1 Cable, 4 x 0.25 mm² 99 m W1 qwerty uiop
3 2 Connector, Plug, 4 pins 1 X1 123 ACME ABC
4 3 Connector, Receptacle, 4 pins 1 X2 234 ACME DEF
5 4 Crimp 1 X2 876 ACME WVU
6 5 Crimp 4 X1 987 ACME ZYX
7 6 Housing 1 X1 345 OTHER
8 7 Label 1 X1
9 8 Sleeving 5 m W1