Compare commits
38 Commits
master
...
feature/bo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9821ca31f7 | ||
|
|
37bf5309f0 | ||
|
|
b4a0ae50b8 | ||
|
|
1f86c97c67 | ||
|
|
09d5496ca4 | ||
|
|
6aa53f6ce4 | ||
|
|
aaf88d7601 | ||
|
|
f928bc73ca | ||
|
|
7e168ea775 | ||
|
|
4338b7a1c5 | ||
|
|
9a93d86058 | ||
|
|
0dc02fe8b5 | ||
|
|
53fefa8f3c | ||
|
|
e61d14ba43 | ||
|
|
8c26c8fbbd | ||
|
|
523d0c659e | ||
|
|
183a9432c3 | ||
|
|
30dbd9573b | ||
|
|
8e7c48d42e | ||
|
|
2e244981fe | ||
|
|
d15eeb1f9f | ||
|
|
d6d0d2a486 | ||
|
|
c22c42e722 | ||
|
|
12e570fdad | ||
|
|
1d653c44ed | ||
|
|
96d393dfb7 | ||
|
|
f13f8a7dd7 | ||
|
|
cdca708da9 | ||
|
|
10b1198b77 | ||
|
|
74462cd225 | ||
|
|
e1d7babf63 | ||
|
|
d2f8034961 | ||
|
|
6378b96541 | ||
|
|
347f1e3031 | ||
|
|
da453db9f0 | ||
|
|
45b13ef797 | ||
|
|
6525537312 | ||
|
|
0f3b5e9edf |
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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))
|
||||||
|
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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
9
test/bomnumbertest.bom.tsv
generated
Normal 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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user