WireViz/src/wireviz/wv_bom.py
Daniel Rojas c33a19708c Refactor code (squashed commit)
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
2025-03-01 19:41:27 +01:00

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()