KiBot/kibot/out_kicost.py

199 lines
8.2 KiB
Python

# -*- 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, abspath, join, dirname
from subprocess import check_output, STDOUT, CalledProcessError
from .misc import CMD_KICOST, URL_KICOST, BOM_ERROR, DISTRIBUTORS, W_UNKDIST, ISO_CURRENCIES, W_UNKCUR, KICOST_SUBMODULE
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)] Include this distributors list. Default is all the available """
self.no_distributors = Optionable
""" [string|list(string)] Exclude this distributors list. They are removed after computing `distributors` """
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)
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
cmd_kicost = abspath(join(dirname(__file__), KICOST_SUBMODULE))
if not isfile(cmd_kicost):
check_script(CMD_KICOST, URL_KICOST)
cmd_kicost = CMD_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 """