Refactor connector node generation Further refactor connector node generation Rebuild demos Generate gauge string inside Cable object WIP: refactor cable node generation Implement HTML indentation WIP More WIP Remove old stuff, slightly simplify code Outsource `gv_pin_table()`, simplify padding Add TODOs Outsource `set_dot_basics()` and `apply_dot_tweaks()` Make setting HTML tag attributes easier through `kwargs` Fix and simplify bgcolor logic Reactivate cable edge generation Outsource `gv_edge_wire()` Make connecting things more object-oriented Alphabetize HTML tags, improve bgcolor rendering Make mates object-oriented Run `autoflake -i` Run `autoflake -i --remove-all-unused-imports` Streamline assignment of ports to simple connectors Implement color objects Use color objects in WireViz Re-sort `wv_colors.py` Make green color darker Break longer lines not caught by `black` because they were unbroken strings or comments Make variable name more expressive Apply dot tweaks last Remove unused line Improve subclassing of components, prepare for BOM refactoring Clean up Include nested additional components in BOM do not add autogenerated designators to BOM Improve BOM generation (TODO: wires from a bundle) Prepare `harness.populate_bom()` Change `description` to `type` in additional BOM item YAML Define CLI epilog str in single statement Rename modules, adjust imports, move `build_examples.py` Restructure and update `.gitignore` Clarify `wireviz.parse()` input types Implement BOM population (missing: qty multipliers) Make `pin_objects` and `wire_objects` dictionaries Compute qty's of additional components (WIP) Add qty test file Adapt `tutorial08.yml` (remove `unit` field) Add `tabulate` to dependency list (might remove later if not needed) Sort BOM by category, assign BOM IDs Rename `Options.color_mode` to `.color_output_mod` for consistency Change BOM output file extension from `.bom.tsv` to `.tsv` Implement BOM bubbles Stop recursive nesting of additional components Add BOM bubble to additional component list (WIP) Fix gauge conversion Fix line breaks in code Optimize BOM bubble geometry Implement pin color output Small issue: GraphViz warning ``` Warning: table size too small for content ``` Add some test files to `tests/` directory Update test files Allow multiple colors for components Implement multiple colors for components, improve multicolor table rendering Fix color cell implementation Fix node background color rendering Add test file for node and title bgcolors WIP: BOM modes Add TODO for empty connector pin tables Comment out BOM modes (WIP) and BOM bubbles Resume work on BOM Include part number info in BOM table Fix BOM output in TSV and HTML Add bundles' wires' part number info to BOM Add TODOs Implement bundle part number rendering Improve conductor table rendering Fix additional component BOM table layout Disable CLI BOM output Add suggestions from #246 Add suggestions from #186 Add .vscode/ to .gitignore Fix PyLance problems Update interim version number Fix zero-size cell for simple connectors without type Implement additional parameters dict for components Implement note for additional components Thicken additional component table Add placeholder for add.comp. PN info Apply black
146 lines
4.6 KiB
Python
146 lines
4.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from collections import namedtuple
|
|
from dataclasses import dataclass
|
|
from enum import Enum, IntEnum
|
|
from typing import List, Optional, Union
|
|
|
|
import tabulate as tabulate_module
|
|
|
|
from wireviz.wv_utils import html_line_breaks
|
|
|
|
BOM_HASH_FIELDS = "description qty_unit amount partnumbers"
|
|
|
|
|
|
BomEntry = namedtuple("BomEntry", "category qty designators")
|
|
BomHash = namedtuple("BomHash", BOM_HASH_FIELDS)
|
|
BomHashList = namedtuple("BomHashList", BOM_HASH_FIELDS)
|
|
PartNumberInfo = namedtuple("PartNumberInfo", "pn manufacturer mpn supplier spn")
|
|
|
|
# TODO: different BOM modes
|
|
# BomMode
|
|
# "normal" # no bubbles, full PN info in GV node
|
|
# "bubbles" # = "full" -> maximum info in GV node
|
|
# "hide PN info"
|
|
# "PN crossref" = "PN bubbles" + "hide PN info"
|
|
# "additionally: BOM table in GV graph label (#227)"
|
|
# "title block in GV graph label"
|
|
|
|
|
|
BomCategory = IntEnum( # to enforce ordering in BOM
|
|
"BomEntry", "CONNECTOR CABLE WIRE ADDITIONAL_INSIDE ADDITIONAL_OUTSIDE"
|
|
)
|
|
QtyMultiplierConnector = Enum(
|
|
"QtyMultiplierConnector", "PINCOUNT POPULATED CONNECTIONS"
|
|
)
|
|
QtyMultiplierCable = Enum(
|
|
"QtyMultiplierCable", "WIRECOUNT TERMINATION LENGTH TOTAL_LENGTH"
|
|
)
|
|
|
|
PART_NUMBER_HEADERS = PartNumberInfo(
|
|
pn="P/N", manufacturer=None, mpn="MPN", supplier=None, spn="SPN"
|
|
)
|
|
|
|
|
|
def partnumbers2list(
|
|
partnumbers: PartNumberInfo, parent_partnumbers: PartNumberInfo = None
|
|
) -> List[str]:
|
|
if parent_partnumbers is None:
|
|
_is_toplevel = True
|
|
parent_partnumbers = partnumbers
|
|
else:
|
|
_is_toplevel = False
|
|
|
|
# Note: != operator used as XOR in the following section (https://stackoverflow.com/a/433161)
|
|
|
|
if _is_toplevel != isinstance(parent_partnumbers.pn, List):
|
|
# top level and not a list, or wire level and list
|
|
cell_pn = pn_info_string(PART_NUMBER_HEADERS.pn, None, partnumbers.pn)
|
|
else:
|
|
# top level and list -> do per wire later
|
|
# wire level and not list -> already done at top level
|
|
cell_pn = None
|
|
|
|
if _is_toplevel != isinstance(parent_partnumbers.mpn, List):
|
|
# TODO: edge case: different manufacturers, but same MPN?
|
|
cell_mpn = pn_info_string(
|
|
PART_NUMBER_HEADERS.mpn, partnumbers.manufacturer, partnumbers.mpn
|
|
)
|
|
else:
|
|
cell_mpn = None
|
|
|
|
if _is_toplevel != isinstance(parent_partnumbers.spn, List):
|
|
# TODO: edge case: different suppliers, but same SPN?
|
|
cell_spn = pn_info_string(
|
|
PART_NUMBER_HEADERS.spn, partnumbers.supplier, partnumbers.spn
|
|
)
|
|
else:
|
|
cell_spn = None
|
|
|
|
cell_contents = [cell_pn, cell_mpn, cell_spn]
|
|
if any(cell_contents):
|
|
return [html_line_breaks(cell) for cell in cell_contents]
|
|
else:
|
|
return None
|
|
|
|
|
|
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
|
|
|
|
|
|
def bom_list(bom):
|
|
headers = (
|
|
"# Qty Unit Description Amount Unit Designators "
|
|
"P/N Manufacturer MPN Supplier SPN Category".split(" ")
|
|
)
|
|
rows = []
|
|
rows.append(headers)
|
|
# fill rows
|
|
for hash, entry in bom.items():
|
|
cells = [
|
|
entry["id"],
|
|
entry["qty"],
|
|
hash.qty_unit,
|
|
hash.description,
|
|
hash.amount.number if hash.amount else None,
|
|
hash.amount.unit if hash.amount else None,
|
|
", ".join(sorted(entry["designators"])),
|
|
]
|
|
if hash.partnumbers:
|
|
cells.extend(
|
|
[
|
|
hash.partnumbers.pn,
|
|
hash.partnumbers.manufacturer,
|
|
hash.partnumbers.mpn,
|
|
hash.partnumbers.supplier,
|
|
hash.partnumbers.spn,
|
|
]
|
|
)
|
|
else:
|
|
cells.extend([None, None, None, None, None])
|
|
# cells.extend([f"{entry['category']} ({entry['category'].name})"]) # for debugging
|
|
rows.append(cells)
|
|
# remove empty columns
|
|
transposed = list(map(list, zip(*rows)))
|
|
transposed = [
|
|
column
|
|
for column in transposed
|
|
if any([cell is not None for cell in column[1:]])
|
|
# ^ ignore header cell in check
|
|
]
|
|
rows = list(map(list, zip(*transposed)))
|
|
return rows
|
|
|
|
|
|
def print_bom_table(bom):
|
|
print()
|
|
print(tabulate_module.tabulate(bom_list(bom), headers="firstrow"))
|
|
print()
|