199 lines
8.2 KiB
Python
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 """
|