[BoM][Added] Human readable text output format.

Closes #334
This commit is contained in:
Salvador E. Tropea 2022-11-24 11:32:47 -03:00
parent d72913a280
commit 02aa6bce0d
5 changed files with 159 additions and 28 deletions

View File

@ -1443,13 +1443,22 @@ Notes:
- `hide_header`: [boolean=false] Hide the header line (names of the columns).
- `hide_pcb_info`: [boolean=false] Hide project information.
- `hide_stats_info`: [boolean=false] Hide statistics information.
- **`format`**: [string=''] [HTML,CSV,TXT,TSV,XML,XLSX] format for the BoM.
Defaults to CSV or a guess according to the options..
- **`format`**: [string=''] [HTML,CSV,TXT,TSV,XML,XLSX,HRTXT] format for the BoM.
Defaults to CSV or a guess according to the options.
HRTXT stands for Human Readable TeXT.
- **`group_fields`**: [list(string)] List of fields used for sorting individual components into groups.
Components which match (comparing *all* fields) will be grouped together.
Field names are case-insensitive.
If empty: ['Part', 'Part Lib', 'Value', 'Footprint', 'Footprint Lib',
'Voltage', 'Tolerance', 'Current', 'Power'] is used.
- **`hrtxt`**: [dict] Options for the HRTXT formats.
* Valid keys:
- **`separator`**: [string='I'] Column Separator.
- `header_sep`: [string='-'] Separator between the header and the data.
- `hide_header`: [boolean=false] Hide the header line (names of the columns).
- `hide_pcb_info`: [boolean=false] Hide project information.
- `hide_stats_info`: [boolean=false] Hide statistics information.
- `justify`: [string='left'] [left,right,center] Text justification.
- **`html`**: [dict] Options for the HTML format.
* Valid keys:
- **`datasheet_as_link`**: [string=''] Column with links to the datasheet.

View File

@ -244,8 +244,9 @@ outputs:
footprint_populate_values: 'no,yes'
# [string|list(string)='SMD,THT,VIRTUAL'] Values for the `Footprint Type` column
footprint_type_values: 'SMD,THT,VIRTUAL'
# [string=''] [HTML,CSV,TXT,TSV,XML,XLSX] format for the BoM.
# [string=''] [HTML,CSV,TXT,TSV,XML,XLSX,HRTXT] format for the BoM.
# Defaults to CSV or a guess according to the options.
# HRTXT stands for Human Readable TeXT
format: 'CSV'
# [boolean=true] Connectors with the same footprints will be grouped together, independent of the name of the connector
group_connectors: true
@ -258,6 +259,20 @@ outputs:
# [list(string)] List of fields to be used when the fields in `group_fields` are empty.
# The first field in this list is the fallback for the first in `group_fields`, and so on
group_fields_fallbacks:
# [dict] Options for the HRTXT formats
hrtxt:
# [string='-'] Separator between the header and the data
header_sep: '-'
# [boolean=false] Hide the header line (names of the columns)
hide_header: false
# [boolean=false] Hide project information
hide_pcb_info: false
# [boolean=false] Hide statistics information
hide_stats_info: false
# [string='left'] [left,right,center] Text justification
justify: 'left'
# [string='I'] Column Separator
separator: 'I'
# [dict] Options for the HTML format
html:
# [boolean=true] Use colors to show the field type

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020 Salvador E. Tropea
# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial
# Copyright (c) 2020-2022 Salvador E. Tropea
# Copyright (c) 2020-2022 Instituto Nacional de Tecnología Industrial
# Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat)
# License: MIT
# Project: KiBot (formerly KiPlot)
@ -35,13 +35,13 @@ def write_bom(filename, ext, groups, headings, cfg):
headings = [h.lower() for h in headings]
result = False
# CSV file writing
if ext in ["csv", "tsv", "txt"]:
if ext in ["csv", "tsv", "txt", "hrtxt"]:
result = write_csv(filename, ext, groups, headings, head_names, cfg)
elif ext in ["htm", "html"]:
result = write_html(filename, groups, headings, head_names, cfg)
elif ext in ["xml"]:
elif ext == "xml":
result = write_xml(filename, groups, headings, head_names, cfg)
elif ext in ["xlsx"]:
elif ext == "xlsx":
# We delay the module load to give out_bom the chance to install XLSXWriter dependencies
from .xlsx_writer import write_xlsx
result = write_xlsx(filename, groups, headings, head_names, cfg)

View File

@ -9,44 +9,101 @@
CSV Writer: Generates a CSV, TSV or TXT BoM file.
"""
import csv
ALIGN_CODE = {'right': '>', 'left': '<', 'center': '^'}
def write_stats(writer, cfg):
class HRTXT(object):
def __init__(self, fh, delimiter=',', hsep='-', align='left'):
self.f = fh
self.delimiter = delimiter
self.hsep = hsep
self.cols_w = []
self.data = []
self.align = ALIGN_CODE[align]
def writerow(self, row):
self.data.append(row)
for c, d in enumerate(row):
d = str(d)
l_cell = len(d)
try:
self.cols_w[c] = max(self.cols_w[c], l_cell)
except IndexError:
self.cols_w.append(l_cell)
def add_sep(self):
self.data.append(None)
def flush(self):
self.col_fmt = []
for ln in self.cols_w:
self.col_fmt.append("{:"+self.align+str(ln)+"s}")
prev = None
for r in self.data:
# Is a separator?
if r is None:
# Skip if we don't want separators
if not self.hsep:
continue
# Create a fake row using the separator
r = []
for _, ln in zip(prev, self.cols_w):
r.append(self.hsep*ln)
if len(r):
self.f.write(self.delimiter)
for cell, fmt in zip(r, self.col_fmt):
cell = str(cell)
self.f.write(fmt.format(cell.replace("\n", self.delimiter)))
self.f.write(self.delimiter)
self.f.write("\n")
prev = r
def write_stats(writer, cfg, ops, write_sep):
if len(cfg.aggregate) == 1:
# Only one project
if not cfg.csv.hide_pcb_info:
if not ops.hide_pcb_info:
prj = cfg.aggregate[0]
writer.writerow(["Project info:"])
write_sep()
writer.writerow(["Schematic:", prj.name])
writer.writerow(["Variant:", cfg.variant.name])
writer.writerow(["Revision:", prj.sch.revision])
writer.writerow(["Date:", prj.sch.date])
writer.writerow(["KiCad Version:", cfg.kicad_version])
if not cfg.csv.hide_stats_info:
write_sep()
if not ops.hide_stats_info:
writer.writerow(["Statistics:"])
write_sep()
writer.writerow(["Component Groups:", cfg.n_groups])
writer.writerow(["Component Count:", cfg.total_str])
writer.writerow(["Fitted Components:", cfg.fitted_str])
writer.writerow(["Number of PCBs:", cfg.number])
writer.writerow(["Total Components:", cfg.n_build])
write_sep()
else:
# Multiple projects
if not cfg.csv.hide_pcb_info:
if not ops.hide_pcb_info:
prj = cfg.aggregate[0]
writer.writerow(["Project info:"])
write_sep()
writer.writerow(["Variant:", cfg.variant.name])
writer.writerow(["KiCad Version:", cfg.kicad_version])
if not cfg.csv.hide_stats_info:
write_sep()
if not ops.hide_stats_info:
writer.writerow(["Global statistics:"])
write_sep()
writer.writerow(["Component Groups:", cfg.n_groups])
writer.writerow(["Component Count:", cfg.total_str])
writer.writerow(["Fitted Components:", cfg.fitted_str])
writer.writerow(["Number of PCBs:", cfg.number])
writer.writerow(["Total Components:", cfg.n_build])
write_sep()
# Individual stats
for prj in cfg.aggregate:
if not cfg.csv.hide_pcb_info:
if not ops.hide_pcb_info:
writer.writerow(["Project info:", prj.sch.title])
write_sep()
writer.writerow(["Schematic:", prj.name])
writer.writerow(["Revision:", prj.sch.revision])
writer.writerow(["Date:", prj.sch.date])
@ -54,13 +111,20 @@ def write_stats(writer, cfg):
writer.writerow(["Company:", prj.sch.company])
if prj.ref_id:
writer.writerow(["ID", prj.ref_id])
if not cfg.csv.hide_stats_info:
write_sep()
if not ops.hide_stats_info:
writer.writerow(["Statistics:", prj.sch.title])
write_sep()
writer.writerow(["Component Groups:", prj.comp_groups])
writer.writerow(["Component Count:", prj.total_str])
writer.writerow(["Fitted Components:", prj.fitted_str])
writer.writerow(["Number of PCBs:", prj.number])
writer.writerow(["Total Components:", prj.comp_build])
write_sep()
def dummy():
pass
def write_csv(filename, ext, groups, headings, head_names, cfg):
@ -72,37 +136,47 @@ def write_csv(filename, ext, groups, headings, head_names, cfg):
head_names = [list of headings to display in the BoM file]
cfg = BoMOptions object with all the configuration
"""
is_hrtxt = ext == "hrtxt"
ops = cfg.hrtxt if is_hrtxt else cfg.csv
# Delimiter is assumed from file extension
# Override delimiter if separator specified
if ext == "csv" and cfg.csv.separator:
delimiter = cfg.csv.separator
if is_hrtxt or (ext == "csv" and ops.separator):
delimiter = ops.separator
else:
if ext == "csv":
delimiter = ","
elif ext == "tsv" or ext == "txt":
delimiter = "\t"
if cfg.csv.quote_all:
quoting = csv.QUOTE_MINIMAL
if hasattr(ops, 'quote_all') and ops.quote_all:
quoting = csv.QUOTE_ALL
else:
quoting = csv.QUOTE_MINIMAL
with open(filename, "wt") as f:
writer = csv.writer(f, delimiter=delimiter, lineterminator="\n", quoting=quoting)
if is_hrtxt:
writer = HRTXT(f, delimiter=delimiter, hsep=ops.header_sep, align=ops.justify)
else:
writer = csv.writer(f, delimiter=delimiter, lineterminator="\n", quoting=quoting)
write_sep = writer.add_sep if is_hrtxt else dummy
# Headers
if not cfg.csv.hide_header:
if not ops.hide_header:
writer.writerow(head_names)
write_sep()
# Body
for group in groups:
if cfg.ignore_dnf and not group.is_fitted():
continue
row = group.get_row(headings)
writer.writerow(row)
write_sep()
# PCB info
if not (cfg.csv.hide_pcb_info and cfg.csv.hide_stats_info):
if not (ops.hide_pcb_info and ops.hide_stats_info):
# Add some blank rows
for _ in range(5):
writer.writerow([])
# The info
write_stats(writer, cfg)
write_stats(writer, cfg, ops, write_sep)
if is_hrtxt:
writer.flush()
return True

View File

@ -269,6 +269,33 @@ class BoMCSV(Optionable):
raise KiPlotConfigurationError('The CSV separator must be one character (`{}`)'.format(self.separator))
class BoMTXT(Optionable):
""" HRTXT options """
def __init__(self):
super().__init__()
with document:
self.separator = 'I'
""" *Column Separator """
self.header_sep = '-'
""" Separator between the header and the data """
self.justify = 'left'
""" [left,right,center] Text justification """
self.hide_header = False
""" Hide the header line (names of the columns) """
self.hide_pcb_info = False
""" Hide project information """
self.hide_stats_info = False
""" Hide statistics information """
def config(self, parent):
super().config(parent)
if self.separator:
self.separator = self.separator.replace(r'\t', '\t')
self.separator = self.separator.replace(r'\n', '\n')
self.separator = self.separator.replace(r'\r', '\r')
self.separator = self.separator.replace(r'\\', '\\')
class BoMXLSX(BoMLinkable):
""" XLSX options """
def __init__(self):
@ -411,8 +438,9 @@ class BoMOptions(BaseOptions):
self.output = GS.def_global_output
""" *filename for the output (%i=bom)"""
self.format = ''
""" *[HTML,CSV,TXT,TSV,XML,XLSX] format for the BoM.
Defaults to CSV or a guess according to the options. """
""" *[HTML,CSV,TXT,TSV,XML,XLSX,HRTXT] format for the BoM.
Defaults to CSV or a guess according to the options.
HRTXT stands for Human Readable TeXT """
# Equivalent to KiBoM INI:
self.ignore_dnf = True
""" *Exclude DNF (Do Not Fit) components """
@ -438,6 +466,8 @@ class BoMOptions(BaseOptions):
""" *[dict] Options for the XLSX format """
self.csv = BoMCSV
""" *[dict] Options for the CSV, TXT and TSV formats """
self.hrtxt = BoMTXT
""" *[dict] Options for the HRTXT formats """
# * Filters
self.pre_transform = Optionable
""" [string|list(string)='_none'] Name of the filter to transform fields before applying other filters.
@ -553,6 +583,9 @@ class BoMOptions(BaseOptions):
# Same for XLSX
if not isinstance(self.xlsx, type):
return 'xlsx'
# Same for HRTXT
if not isinstance(self.hrtxt, type):
return 'hrtxt'
# Default to a simple and common format: CSV
return 'csv'
# Explicit selection
@ -639,7 +672,7 @@ class BoMOptions(BaseOptions):
super().config(parent)
self.format = self._guess_format()
self._expand_id = 'bom'
self._expand_ext = self.format.lower()
self._expand_ext = 'txt' if self.format.lower() == 'hrtxt' else self.format.lower()
# HTML options
if self.format == 'html' and isinstance(self.html, type):
# If no options get the defaults
@ -1018,7 +1051,7 @@ class BoM(BaseOutput): # noqa: F821
if join_fields:
logger.debug(' - Fields to join with Value: {}'.format(join_fields))
# Create a generic version
SIMP_FMT = ['HTML', 'CSV', 'TXT', 'TSV', 'XML']
SIMP_FMT = ['HTML', 'CSV', 'HRTXT', 'TSV', 'XML']
XYRS_FMT = ['HTML']
if GS.check_tool(name, 'XLSXWriter') is not None:
SIMP_FMT.append('XLSX')