Implemented the new variants mechanism in the internal BoM.

This commit is contained in:
Salvador E. Tropea 2020-08-28 16:49:20 -03:00
parent a19c6157b7
commit 0bdce78004
17 changed files with 330 additions and 257 deletions

View File

@ -14,33 +14,10 @@ from copy import deepcopy
from .units import compare_values, comp_match
from .bom_writer import write_bom
from .columnlist import ColumnList
from ..misc import DNF
from .. import log
logger = log.get_logger(__name__)
# Supported values for "do not fit"
DNF = {
"dnf": 1,
"dnl": 1,
"dnp": 1,
"do not fit": 1,
"do not place": 1,
"do not load": 1,
"nofit": 1,
"nostuff": 1,
"noplace": 1,
"noload": 1,
"not fitted": 1,
"not loaded": 1,
"not placed": 1,
"no stuff": 1,
}
# String matches for marking a component as "do not change" or "fixed"
DNC = {
"dnc": 1,
"do not change": 1,
"no change": 1,
"fixed": 1
}
# RV == Resistor Variable or Varistor
# RN == Resistor 'N'(Pack)
# RT == Thermistor
@ -412,88 +389,11 @@ def group_components(cfg, components):
return groups
def comp_is_fixed(value, config, variants):
""" Determine if a component is FIXED or not.
Fixed components shouldn't be replaced without express authorization.
value: component value (lowercase).
config: content of the 'Config' field (lowercase).
variants: list of variants to match. """
# Check the value field first
if value in DNC:
return True
# Empty is not fixed
if not config:
return False
# Also support space separated list (simple cases)
opts = config.split(" ")
for opt in opts:
if opt in DNC:
return True
# Normal separator is ","
opts = config.split(",")
for opt in opts:
if opt in DNC:
return True
return False
def comp_is_fitted(value, config, variants):
""" Determine if a component will be or not.
value: component value (lowercase).
config: content of the 'Config' field (lowercase).
variants: list of variants to match. """
# Check the value field first
if value in DNF:
return False
# Empty value means part is fitted
if not config:
return True
# Also support space separated list (simple cases)
opts = config.split(" ")
for opt in opts:
if opt in DNF:
return False
# Variants logic
opts = config.split(",")
# Only fit for ...
exclusive = False
for opt in opts:
opt = opt.strip()
# Any option containing a DNF is not fitted
if opt in DNF:
return False
# Options that start with '-' are explicitly removed from certain configurations
if opt.startswith("-") and opt[1:] in variants:
return False
# Options that start with '+' are fitted only for certain configurations
if opt.startswith("+"):
exclusive = True
if opt[1:] in variants:
return True
# No match
return not exclusive
def do_bom(file_name, ext, comps, cfg):
# Make the config field name lowercase
cfg.fit_field = cfg.fit_field.lower()
f_config = cfg.fit_field
# Make the variants lowercase
variants = [v.lower() for v in cfg.variant]
# Solve `fixed` and `fitted` attributes for all components
for c in comps:
value = c.value.lower()
config = c.get_field_value(f_config).lower()
c.fitted = comp_is_fitted(value, config, variants)
if cfg.debug_level > 2:
logger.debug('ref: {} value: {} config: {} variants: {} -> fitted {}'.
format(c.ref, value, config, variants, c.fitted))
c.fixed = comp_is_fixed(value, config, variants)
cfg.variant.filter(comps)
# Group components according to group_fields
groups = group_components(cfg, comps)
# Give a name to empty variant
if not variants:
cfg.variant = ['default']
# Create the BoM
logger.debug("Saving BOM File: "+file_name)
write_bom(file_name, ext, groups, cfg.columns, cfg)

View File

@ -53,7 +53,7 @@ def write_csv(filename, ext, groups, headings, head_names, cfg):
if not cfg.csv.hide_pcb_info:
writer.writerow(["Project info:"])
writer.writerow(["Schematic:", cfg.source])
writer.writerow(["Variant:", ' + '.join(cfg.variant)])
writer.writerow(["Variant:", cfg.variant.name])
writer.writerow(["Revision:", cfg.revision])
writer.writerow(["Date:", cfg.date])
writer.writerow(["KiCad Version:", cfg.kicad_version])

View File

@ -213,7 +213,7 @@ def write_html(filename, groups, headings, head_names, cfg):
html.write(' <td class="cell-info">\n')
if not cfg.html.hide_pcb_info:
html.write(" <b>Schematic</b>: {}<br>\n".format(cfg.source))
html.write(" <b>Variant</b>: {}<br>\n".format(', '.join(cfg.variant)))
html.write(" <b>Variant</b>: {}<br>\n".format(cfg.variant.name))
html.write(" <b>Revision</b>: {}<br>\n".format(cfg.revision))
html.write(" <b>Date</b>: {}<br>\n".format(cfg.date))
html.write(" <b>KiCad Version</b>: {}<br>\n".format(cfg.kicad_version))

View File

@ -313,7 +313,7 @@ def write_xlsx(filename, groups, col_fields, head_names, cfg):
rc = r_info_start
if not cfg.xlsx.hide_pcb_info:
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Schematic:", cfg.source)
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Variant:", ' + '.join(cfg.variant))
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Variant:", cfg.variant.name)
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Revision:", cfg.revision)
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Date:", cfg.date)
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "KiCad Version:", cfg.kicad_version)

View File

@ -26,7 +26,7 @@ def write_xml(filename, groups, headings, head_names, cfg):
attrib['Schematic_Source'] = cfg.source
attrib['Schematic_Revision'] = cfg.revision
attrib['Schematic_Date'] = cfg.date
attrib['PCB_Variant'] = ', '.join(cfg.variant)
attrib['PCB_Variant'] = cfg.variant.name
attrib['KiCad_Version'] = cfg.kicad_version
attrib['Component_Groups'] = str(cfg.n_groups)
attrib['Component_Count'] = str(cfg.n_total)

View File

@ -45,3 +45,28 @@ PCBDRAW = 'pcbdraw'
URL_PCBDRAW = 'https://github.com/INTI-CMNB/pcbdraw'
EXAMPLE_CFG = 'example.kibot.yaml'
AUTO_SCALE = 0
# Supported values for "do not fit"
DNF = {
"dnf": 1,
"dnl": 1,
"dnp": 1,
"do not fit": 1,
"do not place": 1,
"do not load": 1,
"nofit": 1,
"nostuff": 1,
"noplace": 1,
"noload": 1,
"not fitted": 1,
"not loaded": 1,
"not placed": 1,
"no stuff": 1,
}
# String matches for marking a component as "do not change" or "fixed"
DNC = {
"dnc": 1,
"do not change": 1,
"no change": 1,
"fixed": 1
}

View File

@ -11,10 +11,12 @@ import os
from re import compile, IGNORECASE
from .gs import GS
from .optionable import Optionable, BaseOptions
from .registrable import RegOutput
from .error import KiPlotConfigurationError
from .macros import macros, document, output_class # noqa: F401
from .bom.columnlist import ColumnList, BoMError
from .bom.bom import do_bom
from .var_kibom import KiBoM
from . import log
logger = log.get_logger(__name__)
@ -207,8 +209,8 @@ class BoMOptions(BaseOptions):
with document:
self.number = 1
""" Number of boards to build (components multiplier) """
self.variant = Optionable
""" [string|list(string)=''] Board variant(s), used to determine which components
self.variant = ''
""" Board variant(s), used to determine which components
are output to the BoM. """
self.output = GS.def_global_output
""" filename for the output (%i=bom)"""
@ -228,7 +230,8 @@ class BoMOptions(BaseOptions):
self.merge_blank_fields = True
""" Component groups with blank fields will be merged into the most compatible group, where possible """
self.fit_field = 'Config'
""" Field name used to determine if a particular part is to be fitted (also DNC and variants) """
""" Field name used to determine if a particular part is to be fitted (also DNC, not for variants).
This value is used only when no variants are specified """
self.group_fields = GroupFields
""" [list(string)] List of fields used for sorting individual components into groups.
Components which match (comparing *all* fields) will be grouped together.
@ -316,16 +319,18 @@ class BoMOptions(BaseOptions):
col = col[:-1]
return col
@staticmethod
def _normalize_variant(variant):
if isinstance(variant, type):
variant = []
elif isinstance(variant, str):
if variant:
variant = [variant]
else:
variant = []
return variant
def _normalize_variant(self):
""" Replaces the name of the variant by an object handling it. """
if self.variant:
if self.variant not in RegOutput._def_variants:
raise KiPlotConfigurationError("Unknown variant name `{}`".format(self.variant))
self.variant = RegOutput._def_variants[self.variant]
else:
# If no variant is specified use the KiBoM variant class with basic functionality
self.variant = KiBoM()
self.variant.config_field = self.fit_field
self.variant.variant = []
self.variant.name = 'default'
def config(self):
super().config()
@ -372,8 +377,10 @@ class BoMOptions(BaseOptions):
for r in self.exclude_any:
r.column = self._fix_ref_field(r.column)
r.regex = compile(r.regex, flags=IGNORECASE)
# Variants, ensure a list
self.variant = self._normalize_variant(self.variant)
# Make the config field name lowercase
self.fit_field = self.fit_field.lower()
# Variants, make it an object
self._normalize_variant()
# Columns
self.column_rename = {}
self.join = []

141
kibot/var_kibom.py Normal file
View File

@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020 Salvador E. Tropea
# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
"""
Implements the KiBoM variants mechanism.
"""
from .optionable import Optionable
from .gs import GS
from .misc import DNF, DNC
from .macros import macros, document, variant_class # noqa: F401
from . import log
logger = log.get_logger(__name__)
@variant_class
class KiBoM(BaseVariant): # noqa: F821
""" KiBoM variant style
The Config field (configurable) contains a comma separated list of variant directives.
-VARIANT excludes a component from VARIANT.
+VARIANT includes the component only if we are using this variant. """
def __init__(self):
super().__init__()
with document:
self.config_field = 'Config'
""" Name of the field used to clasify components """
self.variant = Optionable
""" [string|list(string)=''] Board variant(s) """
def config(self):
super().config()
# Variants, ensure a list
if isinstance(self.variant, type):
self.variant = []
elif isinstance(self.variant, str):
if self.variant:
self.variant = [self.variant]
else:
self.variant = []
self.variant = [v.lower() for v in self.variant]
# Config field must be lowercase
self.config_field = self.config_field.lower()
@staticmethod
def basic_comp_is_fitted(value, config):
""" Basic `fitted` criteria, no variants.
value: component value (lowercase).
config: content of the 'Config' field (lowercase). """
# Check the value field first
if value in DNF:
return False
# Empty value means part is fitted
if not config:
return True
# Also support space separated list (simple cases)
opts = config.split(" ")
for opt in opts:
if opt in DNF:
return False
# Normal separator is ","
opts = config.split(",")
for opt in opts:
if opt in DNF:
return False
return True
@staticmethod
def basic_comp_is_fixed(value, config):
""" Basic `fixed` criteria, no variants
Fixed components shouldn't be replaced without express authorization.
value: component value (lowercase).
config: content of the 'Config' field (lowercase). """
# Check the value field first
if value in DNC:
return True
# Empty is not fixed
if not config:
return False
# Also support space separated list (simple cases)
opts = config.split(" ")
for opt in opts:
if opt in DNC:
return True
# Normal separator is ","
opts = config.split(",")
for opt in opts:
if opt in DNC:
return True
return False
@staticmethod
def _base_filter(comps, f_config):
""" Fill the `fixed` and `fitted` using the basic criteria.
No variant is applied in this step. """
logger.debug("- Generic KiBoM rules")
for c in comps:
value = c.value.lower()
config = c.get_field_value(f_config).lower()
c.fitted = KiBoM.basic_comp_is_fitted(value, config)
if GS.debug_level > 2:
logger.debug('ref: {} value: {} config: {} -> fitted {}'.
format(c.ref, value, config, c.fitted))
c.fixed = KiBoM.basic_comp_is_fixed(value, config)
def variant_comp_is_fitted(self, value, config):
""" Apply the variants to determine if this component will be fitted.
value: component value (lowercase).
config: content of the 'Config' field (lowercase). """
# Variants logic
opts = config.split(",")
# Only fit for ...
exclusive = False
for opt in opts:
opt = opt.strip()
# Options that start with '-' are explicitly removed from certain configurations
if opt.startswith("-") and opt[1:] in self.variant:
return False
# Options that start with '+' are fitted only for certain configurations
if opt.startswith("+"):
exclusive = True
if opt[1:] in self.variant:
return True
# No match
return not exclusive
def filter(self, comps):
logger.debug("Applying KiBoM style filter `{}`".format(self.name))
self._base_filter(comps, self.config_field)
logger.debug("- Variant specific rules")
for c in comps:
if not c.fitted:
# Don't check if we already discarded it during the basic test
continue
value = c.value.lower()
config = c.get_field_value(self.config_field).lower()
c.fitted = self.variant_comp_is_fitted(value, config)
if not c.fitted and GS.debug_level > 2:
logger.debug('ref: {} value: {} config: {} variant: {} -> False'.
format(c.ref, value, config, self.variant))

View File

@ -81,6 +81,7 @@ KIBOM_STATS = [KIBOM_TEST_GROUPS+len(KIBOM_TEST_EXCLUDE),
len(KIBOM_TEST_COMPONENTS),
1,
len(KIBOM_TEST_COMPONENTS)]
VARIANTE_PRJ_INFO = ['kibom-variante', 'default', 'A', '2020-03-12', None]
LINK_HEAD = ['References', 'Part', 'Value', 'Quantity Per PCB', 'digikey#', 'digikey_alt#', 'manf#']
LINKS_COMPONENTS = ['J1', 'J2', 'R1']
LINKS_EXCLUDE = ['C1']
@ -1158,82 +1159,54 @@ def test_int_bom_missing_lib():
ctx.clean_up()
def test_int_bom_variant_t1_1():
def test_int_bom_variant_t1():
prj = 'kibom-variante'
ctx = context.TestContextSCH('test_int_bom_variant_t1_1', prj, 'int_bom_var_v1_csv', BOM_DIR)
ctx.run()
rows, header, info = ctx.load_csv(prj+'-bom.csv')
ref_column = header.index(REF_COLUMN_NAME)
check_kibom_test_netlist(rows, ref_column, 2, ['R3', 'R4'], ['R1', 'R2'])
ctx.search_err(r'Field Config of component (.*) contains extra spaces')
ctx.clean_up()
def test_int_bom_variant_t1_2():
prj = 'kibom-variante'
ctx = context.TestContextSCH('test_int_bom_variant_t1_2', prj, 'int_bom_var_v2_csv', BOM_DIR)
ctx.run()
rows, header, info = ctx.load_csv(prj+'-bom.csv')
ref_column = header.index(REF_COLUMN_NAME)
check_kibom_test_netlist(rows, ref_column, 1, ['R2', 'R4'], ['R1', 'R3'])
ctx.clean_up()
def test_int_bom_variant_t1_3():
prj = 'kibom-variante'
ctx = context.TestContextSCH('test_int_bom_variant_t1_3', prj, 'int_bom_var_v3_csv', BOM_DIR)
ctx.run()
rows, header, info = ctx.load_csv(prj+'-bom.csv')
ref_column = header.index(REF_COLUMN_NAME)
check_kibom_test_netlist(rows, ref_column, 1, ['R2', 'R3'], ['R1', 'R4'])
ctx.clean_up()
def test_int_bom_variant_t1_4():
prj = 'kibom-variante'
ctx = context.TestContextSCH('test_int_bom_variant_t1_4', prj, 'int_bom_simple_csv', BOM_DIR)
ctx = context.TestContextSCH('test_int_bom_variant_t1', prj, 'int_bom_var_t1_csv', BOM_DIR)
ctx.run()
# No variant
logging.debug("* No variant")
rows, header, info = ctx.load_csv(prj+'-bom.csv')
ref_column = header.index(REF_COLUMN_NAME)
check_kibom_test_netlist(rows, ref_column, 2, ['R4'], ['R1', 'R2', 'R3'])
ctx.clean_up()
def test_int_bom_variant_t1_5():
prj = 'kibom-variante'
ctx = context.TestContextSCH('test_int_bom_variant_t1_5', prj, 'int_bom_var_v1v3_csv', BOM_DIR)
ctx.run()
rows, header, info = ctx.load_csv(prj+'-bom.csv')
ref_column = header.index(REF_COLUMN_NAME)
VARIANTE_PRJ_INFO[1] = 'default'
check_csv_info(info, VARIANTE_PRJ_INFO, [4, 20, 3, 1, 3])
# V1
logging.debug("* t1_v1 variant")
rows, header, info = ctx.load_csv(prj+'-bom_(V1).csv')
check_kibom_test_netlist(rows, ref_column, 2, ['R3', 'R4'], ['R1', 'R2'])
ctx.search_err(r'Field Config of component (.*) contains extra spaces')
VARIANTE_PRJ_INFO[1] = 't1_v1'
check_csv_info(info, VARIANTE_PRJ_INFO, [4, 20, 2, 1, 2])
# V2
logging.debug("* t1_v2 variant")
rows, header, info = ctx.load_csv(prj+'-bom_(V2).csv')
check_kibom_test_netlist(rows, ref_column, 1, ['R2', 'R4'], ['R1', 'R3'])
VARIANTE_PRJ_INFO[1] = 't1_v2'
check_csv_info(info, VARIANTE_PRJ_INFO, [3, 20, 2, 1, 2])
# V3
logging.debug("* t1_v3 variant")
rows, header, info = ctx.load_csv(prj+'-bom_V3.csv')
check_kibom_test_netlist(rows, ref_column, 1, ['R2', 'R3'], ['R1', 'R4'])
VARIANTE_PRJ_INFO[1] = 't1_v3'
check_csv_info(info, VARIANTE_PRJ_INFO, [3, 20, 2, 1, 2])
# V1,V3
logging.debug("* `bla bla` variant")
rows, header, info = ctx.load_csv(prj+'-bom_bla_bla.csv')
check_kibom_test_netlist(rows, ref_column, 1, ['R2', 'R3'], ['R1', 'R4'])
VARIANTE_PRJ_INFO[1] = 'bla bla'
check_csv_info(info, VARIANTE_PRJ_INFO, [3, 20, 2, 1, 2])
ctx.clean_up()
def test_int_bom_variant_t2_1():
def test_int_bom_variant_t2():
prj = 'kibom-variant_2'
ctx = context.TestContextSCH('test_int_bom_variant_t2_1', prj, 'int_bom_var_production_csv', BOM_DIR)
ctx.run()
rows, header, info = ctx.load_csv(prj+'-bom.csv')
ref_column = header.index(REF_COLUMN_NAME)
check_kibom_test_netlist(rows, ref_column, 2, ['C1'], ['R1', 'R2', 'C2'])
ctx.clean_up()
def test_int_bom_variant_t2_2():
prj = 'kibom-variant_2'
ctx = context.TestContextSCH('test_int_bom_variant_t2_2', prj, 'int_bom_var_test_csv', BOM_DIR)
ctx.run()
rows, header, info = ctx.load_csv(prj+'-bom.csv')
ref_column = header.index(REF_COLUMN_NAME)
check_kibom_test_netlist(rows, ref_column, 2, ['R2'], ['R1', 'C1', 'C2'])
ctx.clean_up()
def test_int_bom_variant_t2_3():
prj = 'kibom-variant_2'
ctx = context.TestContextSCH('test_int_bom_variant_t2_3', prj, 'int_bom_simple_csv', BOM_DIR)
ctx = context.TestContextSCH('test_int_bom_variant_t2', prj, 'int_bom_var_t2_csv', BOM_DIR)
ctx.run()
rows, header, info = ctx.load_csv(prj+'-bom.csv')
ref_column = header.index(REF_COLUMN_NAME)
check_kibom_test_netlist(rows, ref_column, 1, ['C1', 'C2'], ['R1', 'R2'])
rows, header, info = ctx.load_csv(prj+'-bom_(production).csv')
check_kibom_test_netlist(rows, ref_column, 2, ['C1'], ['R1', 'R2', 'C2'])
rows, header, info = ctx.load_csv(prj+'-bom_(test).csv')
check_kibom_test_netlist(rows, ref_column, 2, ['R2'], ['R1', 'C1', 'C2'])
ctx.clean_up()

View File

@ -1,12 +0,0 @@
# Example KiBot config file
kibot:
version: 1
outputs:
- name: 'bom_internal'
comment: "Bill of Materials in CSV format"
type: bom
dir: BoM
options:
variant: production

View File

@ -0,0 +1,63 @@
# Example KiBot config file
kibot:
version: 1
variants:
- name: 't1_v1'
comment: 'Test 1 Variant V1'
type: kibom
file_id: '_(V1)'
variant: V1
- name: 't1_v2'
comment: 'Test 1 Variant V2'
type: kibom
file_id: '_(V2)'
variant: V2
- name: 't1_v3'
comment: 'Test 1 Variant V3'
type: kibom
file_id: '_V3'
variant: V3
- name: 'bla bla'
comment: 'Test 1 Variant V1+V3'
type: kibom
file_id: '_bla_bla'
variant: ['V1', 'V3']
outputs:
- name: 'bom_internal'
comment: "Bill of Materials in CSV format"
type: bom
dir: BoM
- name: 'bom_internal_v1'
comment: "Bill of Materials in CSV format for variant t1_v1"
type: bom
dir: BoM
options:
variant: t1_v1
- name: 'bom_internal_v2'
comment: "Bill of Materials in CSV format for variant t1_v2"
type: bom
dir: BoM
options:
variant: t1_v2
- name: 'bom_internal_v3'
comment: "Bill of Materials in CSV format for variant t1_v3"
type: bom
dir: BoM
options:
variant: t1_v3
- name: 'bom_internal_bla_bla'
comment: "Bill of Materials in CSV format for variant `bla bla`"
type: bom
dir: BoM
options:
variant: 'bla bla'

View File

@ -0,0 +1,36 @@
# Example KiBot config file
kibot:
version: 1
variants:
- name: 'production'
comment: 'Production variant'
type: kibom
file_id: '_(production)'
variant: production
- name: 'test'
comment: 'Test variant'
type: kibom
file_id: '_(test)'
variant: test
outputs:
- name: 'bom_internal'
comment: "Bill of Materials in CSV format"
type: bom
dir: BoM
- name: 'bom_internal_production'
comment: "Bill of Materials in CSV format for production"
type: bom
dir: BoM
options:
variant: production
- name: 'bom_internal_test'
comment: "Bill of Materials in CSV format for test"
type: bom
dir: BoM
options:
variant: test

View File

@ -1,12 +0,0 @@
# Example KiBot config file
kibot:
version: 1
outputs:
- name: 'bom_internal'
comment: "Bill of Materials in CSV format"
type: bom
dir: BoM
options:
variant: test

View File

@ -1,12 +0,0 @@
# Example KiBot config file
kibot:
version: 1
outputs:
- name: 'bom_internal'
comment: "Bill of Materials in CSV format"
type: bom
dir: BoM
options:
variant: V1

View File

@ -1,12 +0,0 @@
# Example KiBot config file
kibot:
version: 1
outputs:
- name: 'bom_internal'
comment: "Bill of Materials in CSV format"
type: bom
dir: BoM
options:
variant: ['V1', 'V3']

View File

@ -1,12 +0,0 @@
# Example KiBot config file
kibot:
version: 1
outputs:
- name: 'bom_internal'
comment: "Bill of Materials in CSV format"
type: bom
dir: BoM
options:
variant: V2

View File

@ -1,12 +0,0 @@
# Example KiBot config file
kibot:
version: 1
outputs:
- name: 'bom_internal'
comment: "Bill of Materials in CSV format"
type: bom
dir: BoM
options:
variant: V3