From 7c3f2736842b6ff43a92085130e813d26b4d9188 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Wed, 31 Mar 2021 12:27:55 -0300 Subject: [PATCH] Basic KiCost support. --- CHANGELOG.md | 1 + README.md | 43 +++- docs/README.in | 4 +- docs/samples/generic_plot.kibot.yaml | 53 +++++ experiments/variants/KiCost/README.md | 48 +++++ kibot/fil_base.py | 26 ++- kibot/fil_field_rename.py | 25 +-- kibot/fil_subparts.py | 8 +- kibot/fil_var_rename_kicost.py | 1 + kibot/misc.py | 13 +- kibot/optionable.py | 16 ++ kibot/out_kicost.py | 199 ++++++++++++++++++ kibot/var_ibom.py | 18 +- kibot/var_kibom.py | 11 +- kibot/var_kicost.py | 1 + .../kicad_5/kibom-variant_kicost.sch | 2 +- .../kicad_5/kibom-variant_kicost.xml | 128 +++++++++++ tests/reference/5_1_6/KiCost/simple.csv | 8 + .../reference/5_1_6/KiCost/simple_default.csv | 8 + .../5_1_6/KiCost/simple_production.csv | 9 + tests/reference/5_1_6/KiCost/simple_test.csv | 9 + tests/reference/5_1_7/KiCost | 1 + tests/reference/6_0_0/KiCost | 1 + tests/test_plot/test_kicost.py | 50 +++++ tests/yaml_samples/kicost_simple.kibot.yaml | 40 ++++ 25 files changed, 664 insertions(+), 59 deletions(-) create mode 100644 experiments/variants/KiCost/README.md create mode 100644 kibot/out_kicost.py create mode 100644 tests/board_samples/kicad_5/kibom-variant_kicost.xml create mode 100644 tests/reference/5_1_6/KiCost/simple.csv create mode 100644 tests/reference/5_1_6/KiCost/simple_default.csv create mode 100644 tests/reference/5_1_6/KiCost/simple_production.csv create mode 100644 tests/reference/5_1_6/KiCost/simple_test.csv create mode 120000 tests/reference/5_1_7/KiCost create mode 120000 tests/reference/6_0_0/KiCost create mode 100644 tests/test_plot/test_kicost.py create mode 100644 tests/yaml_samples/kicost_simple.kibot.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index f44d4b67..4e6d4e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New KiCost variant style. - `skip_if_no_field` and `invert` options to the regex used in the generic filter. +- Basic KiCost support. ### Changed - Errors and warnings from KiAuto now are printed as errors and warnings. diff --git a/README.md b/README.md index bcfa707e..c06e33b9 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ For example, it's common that you might want for each board rev: * Check ERC/DRC one last time (using [KiCad Automation Scripts](https://github.com/INTI-CMNB/kicad-automation-scripts/)) * Gerbers, drills and drill maps for a fab in their favourite format -* Fab docs for the assembler, including the BoM (Bill of Materials) +* Fab docs for the assembler, including the BoM (Bill of Materials) and costs spreadsheet * Pick and place files * PCB 3D model in STEP format @@ -483,6 +483,7 @@ The available values for *type* are: - Bill of Materials - `kibom` BoM in HTML or CSV format generated by [KiBoM](https://github.com/INTI-CMNB/KiBoM) - `ibom` Interactive HTML BoM generated by [InteractiveHtmlBom](https://github.com/INTI-CMNB/InteractiveHtmlBom) + - `kicost` BoM in XLSX format with costs generated by [KiCost](https://github.com/INTI-CMNB/KiCost) - 3D model: - `step` *Standard for the Exchange of Product Data* for the PCB @@ -1077,6 +1078,45 @@ Next time you need this list just use an alias, like this: variants with the ';' (semicolon) character. This isn't related to the KiBot concept of variants. +* KiCost (KiCad Cost calculator) + * Type: `kicost` + * Description: Generates a spreadsheet containing components costs. + For more information: https://github.com/INTI-CMNB/KiCost + This output is what you get from the KiCost plug-in (eeschema). + * Valid keys: + - `comment`: [string=''] A comment for documentation purposes. + - `dir`: [string='.'] Output directory for the generated files. + - `name`: [string=''] Used to identify this particular output definition. + - `options`: [dict] Options for the `kicost` output. + * Valid keys: + - `aggregate`: [list(dict)] Add components from other projects. + * Valid keys: + - `file`: [string=''] Name of the XML to aggregate. + - `variant`: [string=' '] Variant for this project. + - `currency`: [string|list(string)=USD] Currency priority. Use ISO4217 codes (i.e. USD, EUR). + - `distributors`: [string|list(string)] Use only this distributors list. Default is all the available. + Not compatible with `no_distributors` option. + - `dnf_filter`: [string|list(string)=''] Name of the filter to mark components as not fitted. + A short-cut to use for simple cases where a variant is an overkill. + Internal variants and filters are currently ignored. + - `fields`: [string|list(string)] List of fields to be added to the global data section. + - `group_fields`: [string|list(string)] List of fields that can be different for a group. + Parts with differences in these fields are grouped together, but displayed individually. + - `ignore_fields`: [string|list(string)] List of fields to be ignored. + - `kicost_variant`: [string=''] Regular expression to match the variant field (KiCost option, not internal variants). + - `no_collapse`: [boolean=false] Do not collapse the part references (collapse=R1-R4). + - `no_distributors`: [string|list(string)] Use all but this distributors list. Default is use all the available. + Not compatible with `distributors` option. + - `no_price`: [boolean=false] Do not look for components price. For testing purposes. + - `output`: [string='%f-%i%v.%x'] Filename for the output (%i=kicost, %x=xlsx). Affected by global options. + - `show_cat_url`: [boolean=false] Include the catalogue links in the catalogue code. + - `translate_fields`: [list(dict)] Fields to rename (KiCost option, not internal filters). + * Valid keys: + - `field`: [string=''] Name of the field to rename. + - `name`: [string=''] New name. + - `variant`: [string=''] Board variant to apply. + Internal variants and filters are currently ignored. + * PcbDraw - Beautiful 2D PCB render * Type: `pcbdraw` * Description: Exports the PCB as a 2D model (SVG, PNG or JPG). @@ -2196,6 +2236,7 @@ The internal list of rotations is: - **KiBoM**: Oliver Henry Walters (@SchrodingersGat) - **Interactive HTML BoM**: @qu1ck - **PcbDraw**: Jan Mrázek (@yaqwsx) +- **KiCost**: Dave Vandenbout (@devbisme) and Hildo Guillardi Júnior (@hildogjr) - **Contributors**: - **Error filters ideas**: Leandro Heck (@leoheck) - **GitHub Actions Integration/SVG output**: @nerdyscout diff --git a/docs/README.in b/docs/README.in index 33c30f7e..d4c2c840 100644 --- a/docs/README.in +++ b/docs/README.in @@ -54,7 +54,7 @@ For example, it's common that you might want for each board rev: * Check ERC/DRC one last time (using [KiCad Automation Scripts](https://github.com/INTI-CMNB/kicad-automation-scripts/)) * Gerbers, drills and drill maps for a fab in their favourite format -* Fab docs for the assembler, including the BoM (Bill of Materials) +* Fab docs for the assembler, including the BoM (Bill of Materials) and costs spreadsheet * Pick and place files * PCB 3D model in STEP format @@ -352,6 +352,7 @@ The available values for *type* are: - Bill of Materials - `kibom` BoM in HTML or CSV format generated by [KiBoM](https://github.com/INTI-CMNB/KiBoM) - `ibom` Interactive HTML BoM generated by [InteractiveHtmlBom](https://github.com/INTI-CMNB/InteractiveHtmlBom) + - `kicost` BoM in XLSX format with costs generated by [KiCost](https://github.com/INTI-CMNB/KiCost) - 3D model: - `step` *Standard for the Exchange of Product Data* for the PCB @@ -1230,6 +1231,7 @@ The internal list of rotations is: - **KiBoM**: Oliver Henry Walters (@SchrodingersGat) - **Interactive HTML BoM**: @qu1ck - **PcbDraw**: Jan Mrázek (@yaqwsx) +- **KiCost**: Dave Vandenbout (@devbisme) and Hildo Guillardi Júnior (@hildogjr) - **Contributors**: - **Error filters ideas**: Leandro Heck (@leoheck) - **GitHub Actions Integration/SVG output**: @nerdyscout diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 9f426abc..773940dc 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -661,6 +661,59 @@ outputs: # This isn't related to the KiBot concept of variants variant: '' + # KiCost (KiCad Cost calculator): + # For more information: https://github.com/INTI-CMNB/KiCost + # This output is what you get from the KiCost plug-in (eeschema). + - name: 'kicost_example' + comment: 'Generates a spreadsheet containing components costs.' + type: 'kicost' + dir: 'Example/kicost_dir' + options: + # [list(dict)] Add components from other projects + aggregate: + # [string=''] Name of the XML to aggregate + - file: '' + # [string=' '] Variant for this project + variant: ' ' + # [string|list(string)=USD] Currency priority. Use ISO4217 codes (i.e. USD, EUR) + currency: USD + # [string|list(string)] Use only this distributors list. Default is all the available. + # Not compatible with `no_distributors` option + distributors: + # [string|list(string)=''] Name of the filter to mark components as not fitted. + # A short-cut to use for simple cases where a variant is an overkill. + # Internal variants and filters are currently ignored + dnf_filter: '' + # [string|list(string)] List of fields to be added to the global data section + fields: + # [string|list(string)] List of fields that can be different for a group. + # Parts with differences in these fields are grouped together, but displayed individually + group_fields: + # [string|list(string)] List of fields to be ignored + ignore_fields: + # [string=''] Regular expression to match the variant field (KiCost option, not internal variants) + kicost_variant: '' + # [boolean=false] Do not collapse the part references (collapse=R1-R4) + no_collapse: false + # [string|list(string)] Use all but this distributors list. Default is use all the available. + # Not compatible with `distributors` option + no_distributors: + # [boolean=false] Do not look for components price. For testing purposes + no_price: false + # [string='%f-%i%v.%x'] Filename for the output (%i=kicost, %x=xlsx). Affected by global options + output: '%f-%i%v.%x' + # [boolean=false] Include the catalogue links in the catalogue code + show_cat_url: false + # [list(dict)] Fields to rename (KiCost option, not internal filters) + translate_fields: + # [string=''] Name of the field to rename + - field: 'mpn' + # [string=''] New name + name: 'manf#' + # [string=''] Board variant to apply. + # Internal variants and filters are currently ignored + variant: '' + # PcbDraw - Beautiful 2D PCB render: # Uses configurable colors. # Can also render the components if the 2D models are available diff --git a/experiments/variants/KiCost/README.md b/experiments/variants/KiCost/README.md new file mode 100644 index 00000000..bfe64d3d --- /dev/null +++ b/experiments/variants/KiCost/README.md @@ -0,0 +1,48 @@ +# KiCost variants (modern style) + +This is an analysis and test of the *variants* implementation of [KiCost](https://github.com/xesscorp/KiCost) +An old style used `kicost.VARIANT:FIELD` to assign fields. So you could define `kicost.V1:DNP`. + +## What goes inside the SCH + +- The variants are implemented using the `variant` field (`version` is an alias). +- The `variant` field can contain more than one value, valid separators are: comma `,`, semicolon `;`, slash `/` and space ` `. + Note: spaces are removed, and a vanriant tag can't contain spaces because this is a separator. (`re.split('[,;/ ]', variants)`) +- Components without a variant are always included. +- Components with one or more variants are included only if requested (`--variant REGEX` matches any of the listed variants) + +## What goes outside the SCH + +- When you generate the spreadsheet you can select one or more variants using a regex (`--variant REGEX`). +- Components containing one or more variants that match the regex are added. +- No exclusion mechanism is available. +- `REGEX` isn't case sensitive. + +## Where is in the code? + +Source `kicost/edas/tools.py` function `remove_dnp_parts(components, variant)`. +The old mechanism is part of `kicost/edas/eda_kicad.py` function `extract_fields(part, variant)` combined with the above code. + +## Conclusion + +### Advantages + +- Most of the information is inside the project. +- A regex allows pattern matching. + +### Disadvantages + +- You only have an "include only" option. +- The regex could become complex. + +## Old mechanism + +KiCost has a field rename mechanism. Fields named `kicost.VARIANT:FIELD` are: + +- Renamed to `FIELD` if `VARIANT` matches `--variant REGEX` +- Discarded otherwise + +This can be used with the DNP mechanism: + +- The name of the field is `dnp` or `nopop` (case insensitive) +- If it contains anything other than 0 (evaluated as float) the component is removed. diff --git a/kibot/fil_base.py b/kibot/fil_base.py index b6a10666..30be3ce7 100644 --- a/kibot/fil_base.py +++ b/kibot/fil_base.py @@ -4,6 +4,7 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) from .registrable import RegFilter, Registrable, RegOutput +from .optionable import Optionable from .misc import (IFILT_MECHANICAL, IFILT_VAR_RENAME, IFILT_ROT_FOOTPRINT, IFILT_KICOST_RENAME, DISTRIBUTORS, IFILT_VAR_RENAME_KICOST, IFILT_KICOST_DNP) from .error import KiPlotConfigurationError @@ -268,7 +269,8 @@ class BaseFilter(RegFilter): rename.append({'field': k, 'name': v}) for stub in ['part#', '#', 'p#', 'pn', 'vendor#', 'vp#', 'vpn', 'num']: for dist in DISTRIBUTORS: - base = dist[:-1] + base = dist + dist += '#' if stub != '#': rename.append({'field': base + stub, 'name': dist}) rename.append({'field': base + '_' + stub, 'name': dist}) @@ -371,3 +373,25 @@ class BaseFilter(RegFilter): if len(filters) == 1: return filters[0] return MultiFilter(filters, is_transform) + + +class FieldRename(Optionable): + """ Field translation """ + def __init__(self): + super().__init__() + self._unkown_is_error = True + with document: + self.field = '' + """ Name of the field to rename """ + self.name = '' + """ New name """ + self._field_example = 'mpn' + self._name_example = 'manf#' + + def config(self, parent): + super().config(parent) + if not self.field: + raise KiPlotConfigurationError("Missing or empty `field` in rename list ({})".format(str(self._tree))) + if not self.name: + raise KiPlotConfigurationError("Missing or empty `name` in rename list ({})".format(str(self._tree))) + self.field = self.field.lower() diff --git a/kibot/fil_field_rename.py b/kibot/fil_field_rename.py index b1edb9bc..80fbe547 100644 --- a/kibot/fil_field_rename.py +++ b/kibot/fil_field_rename.py @@ -6,38 +6,15 @@ """ Implements a field renamer """ -from .optionable import Optionable -from .error import KiPlotConfigurationError from .gs import GS from .misc import W_EMPTYREN from .macros import macros, document, filter_class # noqa: F401 +from .fil_base import FieldRename from . import log logger = log.get_logger(__name__) -class FieldRename(Optionable): - """ Field translation """ - def __init__(self): - super().__init__() - self._unkown_is_error = True - with document: - self.field = '' - """ Name of the field to rename """ - self.name = '' - """ New name """ - self._field_example = 'mpn' - self._name_example = 'manf#' - - def config(self, parent): - super().config(parent) - if not self.field: - raise KiPlotConfigurationError("Missing or empty `field` in rename list ({})".format(str(self._tree))) - if not self.name: - raise KiPlotConfigurationError("Missing or empty `name` in rename list ({})".format(str(self._tree))) - self.field = self.field.lower() - - @filter_class class Field_Rename(BaseFilter): # noqa: F821 """ Field_Rename diff --git a/kibot/fil_subparts.py b/kibot/fil_subparts.py index 84a57846..8f6c23d6 100644 --- a/kibot/fil_subparts.py +++ b/kibot/fil_subparts.py @@ -12,7 +12,7 @@ import re from copy import deepcopy from .gs import GS from .optionable import Optionable -from .misc import W_NUMSUBPARTS, W_PARTMULT, DISTRIBUTORS +from .misc import W_NUMSUBPARTS, W_PARTMULT, DISTRIBUTORS_F from .macros import macros, document, filter_class # noqa: F401 from . import log @@ -21,7 +21,7 @@ logger = log.get_logger(__name__) class DistributorsList(Optionable): - _default = DISTRIBUTORS + _default = DISTRIBUTORS_F @filter_class @@ -69,10 +69,10 @@ class Subparts(BaseFilter): # noqa: F821 if not self.ref_sep: self.ref_sep = '#' if isinstance(self.split_fields, type): - self.split_fields = DISTRIBUTORS + self.split_fields = DISTRIBUTORS_F else: if self.split_fields_expand: - self.split_fields.extend(DISTRIBUTORS) + self.split_fields.extend(DISTRIBUTORS_F) # (?]', '_', name) return name + @staticmethod + def force_list(val): + """ Used for values that accept a string or a list of strings. + The string can be a comma separated list """ + if isinstance(val, type): + # Not used + val = [] + elif isinstance(val, str): + # A string + if val: + val = [v.strip() for v in val.split(',')] + else: + # Empty string + val = [] + return val + class BaseOptions(Optionable): """ A class to validate and hold output options. diff --git a/kibot/out_kicost.py b/kibot/out_kicost.py new file mode 100644 index 00000000..141603dd --- /dev/null +++ b/kibot/out_kicost.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Salvador E. Tropea +# Copyright (c) 2021 Instituto Nacional de Tecnología Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +from os.path import isfile +from subprocess import check_output, STDOUT, CalledProcessError +from .misc import CMD_KICOST, URL_KICOST, BOM_ERROR, DISTRIBUTORS, W_UNKDIST, ISO_CURRENCIES, W_UNKCUR +from .error import KiPlotConfigurationError +from .optionable import Optionable +from .gs import GS +from .kiplot import check_script +from .out_base import VariantOptions +from .macros import macros, document, output_class # noqa: F401 +from .fil_base import FieldRename +from . import log + +logger = log.get_logger(__name__) +WARNING_MIX = "Internal variants and filters are currently ignored" + + +class Aggregate(Optionable): + def __init__(self): + super().__init__() + with document: + self.file = '' + """ Name of the XML to aggregate """ + self.variant = ' ' + """ Variant for this project """ + + def config(self, parent): + super().config(parent) + if not self.file: + raise KiPlotConfigurationError("Missing or empty `file` in aggregate list ({})".format(str(self._tree))) + + +class KiCostOptions(VariantOptions): + def __init__(self): + with document: + self.output = GS.def_global_output + """ Filename for the output (%i=kicost, %x=xlsx) """ + self.no_price = False + """ Do not look for components price. For testing purposes """ + self.no_collapse = False + """ Do not collapse the part references (collapse=R1-R4) """ + self.show_cat_url = False + """ Include the catalogue links in the catalogue code """ + self.distributors = Optionable + """ [string|list(string)] Use only this distributors list. Default is all the available. + Not compatible with `no_distributors` option """ + self.no_distributors = Optionable + """ [string|list(string)] Use all but this distributors list. Default is use all the available. + Not compatible with `distributors` option """ + self.currency = Optionable + """ [string|list(string)=USD] Currency priority. Use ISO4217 codes (i.e. USD, EUR) """ + self.group_fields = Optionable + """ [string|list(string)] List of fields that can be different for a group. + Parts with differences in these fields are grouped together, but displayed individually """ + self.ignore_fields = Optionable + """ [string|list(string)] List of fields to be ignored """ + self.fields = Optionable + """ [string|list(string)] List of fields to be added to the global data section """ + self.translate_fields = FieldRename + """ [list(dict)] Fields to rename (KiCost option, not internal filters) """ + self.kicost_variant = '' + """ Regular expression to match the variant field (KiCost option, not internal variants) """ + self.aggregate = Aggregate + """ [list(dict)] Add components from other projects """ + + super().__init__() + self.add_to_doc('variant', WARNING_MIX) + self.add_to_doc('dnf_filter', WARNING_MIX) + self._expand_id = 'kicost' + self._expand_ext = 'xlsx' + + @staticmethod + def _validate_dis(val): + val = Optionable.force_list(val) + for v in val: + if v not in DISTRIBUTORS: + logger.warning(W_UNKDIST+'Unknown distributor `{}`'.format(v)) + return val + + @staticmethod + def _validate_cur(val): + val = Optionable.force_list(val) + for v in val: + if v not in ISO_CURRENCIES: + logger.warning(W_UNKCUR+'Unknown currency `{}`'.format(v)) + return val + + def config(self, parent): + super().config(parent) + if not self.output: + self.output = '%f.%x' + self.distributors = self._validate_dis(self.distributors) + self.no_distributors = self._validate_dis(self.no_distributors) + if self.distributors and self.no_distributors: + raise KiPlotConfigurationError('`distributors` and `no_distributors` are incompatible, choose one') + self.currency = self._validate_cur(self.currency) + self.group_fields = Optionable.force_list(self.group_fields) + self.ignore_fields = Optionable.force_list(self.ignore_fields) + self.fields = Optionable.force_list(self.fields) + # Adapt translate_fields to its use + if isinstance(self.translate_fields, type): + self.translate_fields = [] + if self.translate_fields: + translate_fields = [] + for f in self.translate_fields: + translate_fields.append(f.field) + translate_fields.append(f.name) + self.translate_fields = translate_fields + # Make sure aggregate is a list + if isinstance(self.aggregate, type): + self.aggregate = [] + + def get_targets(self, out_dir): + return [self.expand_filename(out_dir, self.output, self._expand_id, self._expand_ext)] + + @staticmethod + def add_list_opt(cmd, name, val): + if val: + cmd.append('--'+name+'='+','.join(val)) + + @staticmethod + def add_bool_opt(cmd, name, val): + if val: + cmd.append('--'+name) + + def run(self, name): + super().run(name) + # Make sure the XML is there. + # Currently we only support the XML mechanism. + netlist = GS.sch_no_ext+'.xml' + if not isfile(netlist): + logger.error('Missing netlist in XML format `{}`'.format(netlist)) + logger.error('You can generate it using the `update_xml` pre-flight') + exit(BOM_ERROR) + # Check KiCost is available + check_script(CMD_KICOST, URL_KICOST) + # Construct the command + cmd = [CMD_KICOST, '-w', '-o', name, '-i', netlist] + # Add the rest of input files and their variants + if self.aggregate: + # More than one project + for p in self.aggregate: + cmd.append(p.file) + cmd.append('--variant') + # KiCost internally defaults to ' ' as a dummy variant + cmd.append(self.kicost_variant if self.kicost_variant else ' ') + for p in self.aggregate: + cmd.append(p.variant if p.variant else ' ') + else: + # Just this project + if self.kicost_variant: + cmd.extend(['--variant', self.kicost_variant]) + # Pass the debug level + if GS.debug_enabled: + cmd.append('--debug={}'.format(GS.debug_level)) + # Boolean options + self.add_bool_opt(cmd, 'no_price', self.no_price) + self.add_bool_opt(cmd, 'no_collapse', self.no_collapse) + self.add_bool_opt(cmd, 'show_cat_url', self.show_cat_url) + # List options + self.add_list_opt(cmd, 'include', self.distributors) + self.add_list_opt(cmd, 'exclude', self.no_distributors) + self.add_list_opt(cmd, 'currency', self.currency) + self.add_list_opt(cmd, 'group_fields', self.group_fields) + self.add_list_opt(cmd, 'ignore_fields', self.ignore_fields) + self.add_list_opt(cmd, 'fields', self.fields) + # Field translation + if self.translate_fields: + cmd.append('--translate_fields') + cmd.extend(self.translate_fields) + # Run the command + logger.debug('Running: '+str(cmd)) + try: + cmd_output = check_output(cmd, stderr=STDOUT) + cmd_output_dec = cmd_output.decode() + except CalledProcessError as e: + logger.error('Failed to create costs spreadsheet, error %d', e.returncode) + if e.output: + logger.debug('Output from command: '+e.output.decode()) + exit(BOM_ERROR) + logger.debug('Output from command:\n'+cmd_output_dec+'\n') + + +@output_class +class KiCost(BaseOutput): # noqa: F821 + """ KiCost (KiCad Cost calculator) + Generates a spreadsheet containing components costs. + For more information: https://github.com/INTI-CMNB/KiCost + This output is what you get from the KiCost plug-in (eeschema). """ + def __init__(self): + super().__init__() + self._sch_related = True + with document: + self.options = KiCostOptions + """ [dict] Options for the `kicost` output """ diff --git a/kibot/var_ibom.py b/kibot/var_ibom.py index 2db21301..8f98f4ba 100644 --- a/kibot/var_ibom.py +++ b/kibot/var_ibom.py @@ -32,28 +32,14 @@ class IBoM(BaseVariant): # noqa: F821 self.variants_whitelist = Optionable """ [string|list(string)=''] List of board variants to include in the BOM """ - @staticmethod - def _force_list(val): - if isinstance(val, type): - # Not used - val = [] - elif isinstance(val, str): - # A string - if val: - val = [v.strip() for v in val.split(',')] - else: - # Empty string - val = [] - return val - def config(self, parent): super().config(parent) self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform', is_transform=True) self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter', IFILT_MECHANICAL) self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter') self.dnc_filter = BaseFilter.solve_filter(self.dnc_filter, 'dnc_filter') - self.variants_blacklist = self._force_list(self.variants_blacklist) - self.variants_whitelist = self._force_list(self.variants_whitelist) + self.variants_blacklist = self.force_list(self.variants_blacklist) + self.variants_whitelist = self.force_list(self.variants_whitelist) def skip_component(self, c): """ Skip components that doesn't belong to this variant. """ diff --git a/kibot/var_kibom.py b/kibot/var_kibom.py index ef289baa..13716d2d 100644 --- a/kibot/var_kibom.py +++ b/kibot/var_kibom.py @@ -42,15 +42,8 @@ class KiBoM(BaseVariant): # noqa: F821 def config(self, parent): # Now we can let the parent initialize the filters super().config(parent) - # 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] + # Variants, ensure a lowercase list + self.variant = [v.lower() for v in self.force_list(self.variant)] self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform', is_transform=True) # Filters priority: # 1) Defined here diff --git a/kibot/var_kicost.py b/kibot/var_kicost.py index 1517725c..190c290c 100644 --- a/kibot/var_kicost.py +++ b/kibot/var_kicost.py @@ -3,6 +3,7 @@ # Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) +# The algorithm is from KiCost project (https://github.com/xesscorp/KiCost) """ Implements the KiCost variants mechanism. """ diff --git a/tests/board_samples/kicad_5/kibom-variant_kicost.sch b/tests/board_samples/kicad_5/kibom-variant_kicost.sch index 71a31f7d..639bc342 100644 --- a/tests/board_samples/kicad_5/kibom-variant_kicost.sch +++ b/tests/board_samples/kicad_5/kibom-variant_kicost.sch @@ -27,7 +27,7 @@ F 2 "" H 1038 1550 50 0001 C CNN F 3 "~" H 1000 1700 50 0001 C CNN F 4 "1" H 1000 1700 50 0001 C CNN "kicost.default:dnp" F 5 "0.0" H 1000 1700 50 0001 C CNN "kicost.test:dnp" -F 6 "" H 1000 1700 50 0001 C CNN "kicost.production:nopop" +F 6 " " H 1000 1700 50 0001 C CNN "kicost.production:nopop" 1 1000 1700 1 0 0 -1 $EndComp diff --git a/tests/board_samples/kicad_5/kibom-variant_kicost.xml b/tests/board_samples/kicad_5/kibom-variant_kicost.xml new file mode 100644 index 00000000..37786271 --- /dev/null +++ b/tests/board_samples/kicad_5/kibom-variant_kicost.xml @@ -0,0 +1,128 @@ + + + + /home/salvador/0Data/Eccosur/kibot/tests/board_samples/kicad_5/kibom-variant_kicost.sch + mar 30 mar 2021 09:46:24 + Eeschema 5.1.9+dfsg1-1 + + + KiBom Test Schematic + https://github.com/SchrodingersGat/KiBom + A + 2020-03-12 + kibom-variant_kicost.sch + + + + + + + + + + 1nF + ~ + + 1 + + 0.0 + + + + 5F43BEC2 + + + 1000 pF + ~ + + production,test + + + + 5F43CE1C + + + 1k + ~ + + 3k3 + + + + 5F43D144 + + + 1000 + ~ + + production default + + + + 5F43D4BB + + + + + Unpolarized capacitor + ~ + + C_* + + + C + C + + + + + + + + Resistor + ~ + + R_* + + + R + R + + + + + + + + + + /usr/share/kicad/library/Device.lib + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/reference/5_1_6/KiCost/simple.csv b/tests/reference/5_1_6/KiCost/simple.csv new file mode 100644 index 00000000..75ee43fc --- /dev/null +++ b/tests/reference/5_1_6/KiCost/simple.csv @@ -0,0 +1,8 @@ +Prj:,KiBom Test Schematic,,,,Board Qty:,100 +Co.:,https://github.com/SchrodingersGat/KiBom,,,,Unit Cost:,0 +Global Part Info,,,,,, +Refs,Value,Footprint,Manf#,Qty,Unit$,Ext$ +C1,1nF,,,100,,0 +R1,1k,,,100,,0 + + diff --git a/tests/reference/5_1_6/KiCost/simple_default.csv b/tests/reference/5_1_6/KiCost/simple_default.csv new file mode 100644 index 00000000..6cb4f44f --- /dev/null +++ b/tests/reference/5_1_6/KiCost/simple_default.csv @@ -0,0 +1,8 @@ +Prj:,KiBom Test Schematic,,,,Board Qty:,100 +Co.:,https://github.com/SchrodingersGat/KiBom,,,,Unit Cost:,0 +Global Part Info,,,,,, +Refs,Value,Footprint,Manf#,Qty,Unit$,Ext$ +R1,1k,,,100,,0 +R2,1000,,,100,,0 + + diff --git a/tests/reference/5_1_6/KiCost/simple_production.csv b/tests/reference/5_1_6/KiCost/simple_production.csv new file mode 100644 index 00000000..f8d17fe4 --- /dev/null +++ b/tests/reference/5_1_6/KiCost/simple_production.csv @@ -0,0 +1,9 @@ +Prj:,KiBom Test Schematic,,,,Board Qty:,100 +Co.:,https://github.com/SchrodingersGat/KiBom,,,,Unit Cost:,0 +Global Part Info,,,,,, +Refs,Value,Footprint,Manf#,Qty,Unit$,Ext$ +C2,1000 pF,,,100,,0 +R1,1k,,,100,,0 +R2,1000,,,100,,0 + + diff --git a/tests/reference/5_1_6/KiCost/simple_test.csv b/tests/reference/5_1_6/KiCost/simple_test.csv new file mode 100644 index 00000000..d0b6c5d6 --- /dev/null +++ b/tests/reference/5_1_6/KiCost/simple_test.csv @@ -0,0 +1,9 @@ +Prj:,KiBom Test Schematic,,,,Board Qty:,100 +Co.:,https://github.com/SchrodingersGat/KiBom,,,,Unit Cost:,0 +Global Part Info,,,,,, +Refs,Value,Footprint,Manf#,Qty,Unit$,Ext$ +C1,1nF,,,100,,0 +C2,1000 pF,,,100,,0 +R1,3k3,,,100,,0 + + diff --git a/tests/reference/5_1_7/KiCost b/tests/reference/5_1_7/KiCost new file mode 120000 index 00000000..e4f16de9 --- /dev/null +++ b/tests/reference/5_1_7/KiCost @@ -0,0 +1 @@ +../5_1_6/KiCost/ \ No newline at end of file diff --git a/tests/reference/6_0_0/KiCost b/tests/reference/6_0_0/KiCost new file mode 120000 index 00000000..e4f16de9 --- /dev/null +++ b/tests/reference/6_0_0/KiCost @@ -0,0 +1 @@ +../5_1_6/KiCost/ \ No newline at end of file diff --git a/tests/test_plot/test_kicost.py b/tests/test_plot/test_kicost.py new file mode 100644 index 00000000..705bcdc2 --- /dev/null +++ b/tests/test_plot/test_kicost.py @@ -0,0 +1,50 @@ +""" +Tests for the KiCost output. + +For debug information use: +pytest-3 --log-cli-level debug +""" + +import os +import sys +# Look for the 'utils' module from where the script is running +prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if prev_dir not in sys.path: + sys.path.insert(0, prev_dir) +# Utils import +from utils import context +import logging +import subprocess + + +OUT_DIR = 'KiCost' + + +def conver2csv(xlsx): + csv = xlsx[:-4]+'csv' + logging.debug('Converting to CSV') + p1 = subprocess.Popen(['xlsx2csv', '--skipemptycolumns', xlsx], stdout=subprocess.PIPE) + with open(csv, 'w') as f: + p2 = subprocess.Popen(['egrep', '-i', '-v', r'( date|kicost|Total purchase)'], stdin=p1.stdout, stdout=f) + p2.communicate()[0] + + +def check_simple(ctx, variant): + if variant: + variant = '_'+variant + name = os.path.join(OUT_DIR, 'simple'+variant+'.xlsx') + ctx.expect_out_file(name) + xlsx = ctx.get_out_path(name) + conver2csv(xlsx) + ctx.compare_txt(name[:-4]+'csv') + + +def test_kicost_simple(test_dir): + prj = 'kibom-variant_kicost' + ctx = context.TestContextSCH(test_dir, 'test_kicost_simple', prj, 'kicost_simple', OUT_DIR) + ctx.run() + check_simple(ctx, '') + check_simple(ctx, 'default') + check_simple(ctx, 'production') + check_simple(ctx, 'test') + ctx.clean_up() diff --git a/tests/yaml_samples/kicost_simple.kibot.yaml b/tests/yaml_samples/kicost_simple.kibot.yaml new file mode 100644 index 00000000..1a3e6128 --- /dev/null +++ b/tests/yaml_samples/kicost_simple.kibot.yaml @@ -0,0 +1,40 @@ +# KiCost basic test +kibot: + version: 1 + +outputs: + - name: 'Costs' + comment: "Components costs spreadsheet" + type: kicost + dir: KiCost + options: + output: 'simple' + no_price: true + no_collapse: true + + - name: 'Costs (default)' + comment: "Components costs spreadsheet default variant" + type: kicost + dir: KiCost + options: + output: 'simple_default' + no_price: true + kicost_variant: default + + - name: 'Costs (production)' + comment: "Components costs spreadsheet production variant" + type: kicost + dir: KiCost + options: + output: 'simple_production' + no_price: true + kicost_variant: production + + - name: 'Costs (test)' + comment: "Components costs spreadsheet test variant" + type: kicost + dir: KiCost + options: + output: 'simple_test' + no_price: true + kicost_variant: test