Added Internal BoM + KiCost integration
- Currently very basic, but you get "Costs" and "Costs (DNF)" work sheets in the XLSX output when the xlsx.kicost option is enabled.
This commit is contained in:
parent
3a3e88ec83
commit
16ddb9465f
|
|
@ -16,7 +16,8 @@ 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.
|
||||
- Basic KiCost support (experimental).
|
||||
- Basic internal BoM and KiCost integration (experimental)
|
||||
- Experimental mechanism to change 3D models according to the variant.
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
|
|
@ -705,9 +705,10 @@ Next time you need this list just use an alias, like this:
|
|||
- `hide_pcb_info`: [boolean=false] Hide project information.
|
||||
- `hide_stats_info`: [boolean=false] Hide statistics information.
|
||||
- `highlight_empty`: [boolean=true] Use a color for empty cells. Applies only when `col_colors` is `true`.
|
||||
- `kicost`: [boolean=false] Enable KiCost worksheet creation.
|
||||
- `logo`: [string|boolean=''] PNG file to use as logo, use false to remove.
|
||||
- `max_col_width`: [number=60] [20,999] Maximum column width (characters).
|
||||
- `style`: [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic..
|
||||
- `style`: [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic.
|
||||
- `title`: [string='KiBot Bill of Materials'] BoM title.
|
||||
|
||||
* Archiver (files compressor)
|
||||
|
|
|
|||
|
|
@ -173,11 +173,13 @@ outputs:
|
|||
hide_stats_info: false
|
||||
# [boolean=true] Use a color for empty cells. Applies only when `col_colors` is `true`
|
||||
highlight_empty: true
|
||||
# [boolean=false] Enable KiCost worksheet creation
|
||||
kicost: false
|
||||
# [string|boolean=''] PNG file to use as logo, use false to remove
|
||||
logo: ''
|
||||
# [number=60] [20,999] Maximum column width (characters)
|
||||
max_col_width: 60
|
||||
# [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic.
|
||||
# [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic
|
||||
style: 'modern-blue'
|
||||
# [string='KiBot Bill of Materials'] BoM title
|
||||
title: 'KiBot Bill of Materials'
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@
|
|||
XLSX Writer: Generates an XLSX BoM file.
|
||||
"""
|
||||
import io
|
||||
import pprint
|
||||
from textwrap import wrap
|
||||
from base64 import b64decode
|
||||
from .columnlist import ColumnList
|
||||
from .kibot_logo import KIBOT_LOGO
|
||||
from .. import log
|
||||
from ..misc import W_NOKICOST
|
||||
try:
|
||||
from xlsxwriter import Workbook
|
||||
XLSX_SUPPORT = True
|
||||
|
|
@ -22,6 +24,13 @@ except ModuleNotFoundError:
|
|||
|
||||
class Workbook():
|
||||
pass
|
||||
try:
|
||||
from kicost.kicost import query_part_info
|
||||
from kicost.spreadsheet import create_worksheet, Spreadsheet
|
||||
import kicost.global_vars as kvar
|
||||
KICOST_SUPPORT = True
|
||||
except ModuleNotFoundError:
|
||||
KICOST_SUPPORT = False
|
||||
|
||||
logger = log.get_logger(__name__)
|
||||
BG_GEN = "#E6FFEE"
|
||||
|
|
@ -274,6 +283,50 @@ def write_info(cfg, r_info_start, worksheet, column_widths, col1, fmt_info, fmt_
|
|||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", prj.comp_build)
|
||||
|
||||
|
||||
class Part(object):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
|
||||
def create_kicost_sheet(workbook, groups, cfg):
|
||||
if not KICOST_SUPPORT:
|
||||
logger.warning(W_NOKICOST, 'KiCost sheet requested but failed to load KiCost support')
|
||||
if cfg.debug_level > 2:
|
||||
logger.debug("Groups exported to KiCost:")
|
||||
for g in groups:
|
||||
logger.debug(pprint.pformat(g.__dict__))
|
||||
logger.debug("-- Components")
|
||||
for c in g.components:
|
||||
logger.debug(pprint.pformat(c.__dict__))
|
||||
# Force KiCost to use our logger
|
||||
kvar.logger = logger
|
||||
# Create the projects information structure
|
||||
prj_info = [{'title': p.name, 'company': p.sch.company, 'date': p.sch.date} for p in cfg.aggregate]
|
||||
# Create the worksheets
|
||||
ws_names = ['Costs', 'Costs (DNF)']
|
||||
for ws in range(2):
|
||||
# Second pass is DNF
|
||||
dnf = ws == 1
|
||||
# Should we generate the DNF?
|
||||
if dnf and (not cfg.xlsx.generate_dnf or cfg.n_total == cfg.n_fitted):
|
||||
break
|
||||
# Create the parts structure from the groups
|
||||
parts = []
|
||||
for g in groups:
|
||||
if (cfg.ignore_dnf and not g.is_fitted()) != dnf:
|
||||
continue
|
||||
part = Part()
|
||||
part.refs = [c.ref for c in g.components]
|
||||
part.fields = g.fields
|
||||
parts.append(part)
|
||||
# Get the prices
|
||||
query_part_info(parts)
|
||||
# Create a class to hold the spreadsheet parameters
|
||||
ss = Spreadsheet(workbook, ws_names[ws])
|
||||
# Add a worksheet with costs to the spreadsheet
|
||||
create_worksheet(ss, logger, parts, prj_info)
|
||||
|
||||
|
||||
def write_xlsx(filename, groups, col_fields, head_names, cfg):
|
||||
"""
|
||||
Write BoM out to a XLSX file
|
||||
|
|
@ -395,6 +448,10 @@ def write_xlsx(filename, groups, col_fields, head_names, cfg):
|
|||
|
||||
# Add a sheet for the color references
|
||||
create_color_ref(workbook, cfg.xlsx.col_colors, hl_empty, fmt_cols)
|
||||
# Optionally add KiCost information
|
||||
if cfg.xlsx.kicost:
|
||||
create_kicost_sheet(workbook, groups, cfg)
|
||||
|
||||
workbook.close()
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ W_BADFIELD = '(W062) '
|
|||
W_UNKDIST = '(W063) '
|
||||
W_UNKCUR = '(W064) '
|
||||
W_NONETLIST = '(W065) '
|
||||
W_NOKICOST = '(W066) '
|
||||
|
||||
|
||||
class Rect(object):
|
||||
|
|
|
|||
|
|
@ -156,7 +156,9 @@ class BoMXLSX(BoMLinkable):
|
|||
self.max_col_width = 60
|
||||
""" [20,999] Maximum column width (characters) """
|
||||
self.style = 'modern-blue'
|
||||
""" Head style: modern-blue, modern-green, modern-red and classic. """
|
||||
""" Head style: modern-blue, modern-green, modern-red and classic """
|
||||
self.kicost = False
|
||||
""" Enable KiCost worksheet creation """
|
||||
|
||||
def config(self, parent):
|
||||
super().config(parent)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
.local
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
EESchema Schematic File Version 4
|
||||
EELAYER 30 0
|
||||
EELAYER END
|
||||
$Descr A4 11693 8268
|
||||
encoding utf-8
|
||||
Sheet 1 1
|
||||
Title "KiCost Test Schematic"
|
||||
Date "2021-04-06"
|
||||
Rev "A"
|
||||
Comp "INTI - MyNT"
|
||||
Comment1 ""
|
||||
Comment2 ""
|
||||
Comment3 ""
|
||||
Comment4 ""
|
||||
$EndDescr
|
||||
Text Notes 500 600 0 79 ~ 0
|
||||
This schematic serves as a test-file for the KiBot export script.\n
|
||||
Text Notes 5950 2600 0 118 ~ 0
|
||||
The test tests the following \nvariants matrix:\n production test default\nC1 X\nC2 X X\nR1 X X X\nR2 X X\n
|
||||
$Comp
|
||||
L Device:C C1
|
||||
U 1 1 5F43BEC2
|
||||
P 1000 1700
|
||||
F 0 "C1" H 1115 1746 50 0000 L CNN
|
||||
F 1 "1nF" H 1115 1655 50 0000 L CNN
|
||||
F 2 "" H 1038 1550 50 0001 C CNN
|
||||
F 3 "~" H 1000 1700 50 0001 C CNN
|
||||
F 4 "-production,+test" H 1000 1700 50 0001 C CNN "Config"
|
||||
F 5 "Samsung" H 1000 1700 50 0001 C CNN "manf"
|
||||
F 6 "CL10B102KC8NNNC" H 1000 1700 50 0001 C CNN "manf#"
|
||||
F 7 "1276-1131-1-ND" H 1000 1700 50 0001 C CNN "digikey#"
|
||||
1 1000 1700
|
||||
1 0 0 -1
|
||||
$EndComp
|
||||
$Comp
|
||||
L Device:C C2
|
||||
U 1 1 5F43CE1C
|
||||
P 1450 1700
|
||||
F 0 "C2" H 1565 1746 50 0000 L CNN
|
||||
F 1 "1000 pF" H 1565 1655 50 0000 L CNN
|
||||
F 2 "" H 1488 1550 50 0001 C CNN
|
||||
F 3 "~" H 1450 1700 50 0001 C CNN
|
||||
F 4 "+production,+test" H 1450 1700 50 0001 C CNN "Config"
|
||||
F 5 "Samsung" H 1000 1700 50 0001 C CNN "manf"
|
||||
F 6 "CL10B102KC8NNNC" H 1000 1700 50 0001 C CNN "manf#"
|
||||
F 7 "1276-1131-1-ND" H 1000 1700 50 0001 C CNN "digikey#"
|
||||
1 1450 1700
|
||||
1 0 0 -1
|
||||
$EndComp
|
||||
$Comp
|
||||
L Device:R R1
|
||||
U 1 1 5F43D144
|
||||
P 2100 1700
|
||||
F 0 "R1" H 2170 1746 50 0000 L CNN
|
||||
F 1 "1k" H 2170 1655 50 0000 L CNN
|
||||
F 2 "" V 2030 1700 50 0001 C CNN
|
||||
F 3 "~" H 2100 1700 50 0001 C CNN
|
||||
F 4 "3k3" H 2100 1700 50 0001 C CNN "test:Value"
|
||||
F 5 "Bourns" H 1000 1700 50 0001 C CNN "manf"
|
||||
F 6 "CR0603-JW-102ELF" H 1000 1700 50 0001 C CNN "manf#"
|
||||
F 7 "CR0603-JW-102ELFCT-ND" H 1000 1700 50 0001 C CNN "digikey#"
|
||||
F 9 "CR0603-JW-332ELF" H 1000 1700 50 0001 C CNN "test:manf#"
|
||||
F 10 "CR0603-JW-332ELFCT-ND" H 1000 1700 50 0001 C CNN "test:digikey#"
|
||||
1 2100 1700
|
||||
1 0 0 -1
|
||||
$EndComp
|
||||
$Comp
|
||||
L Device:R R2
|
||||
U 1 1 5F43D4BB
|
||||
P 2500 1700
|
||||
F 0 "R2" H 2570 1746 50 0000 L CNN
|
||||
F 1 "1000" H 2570 1655 50 0000 L CNN
|
||||
F 2 "" V 2430 1700 50 0001 C CNN
|
||||
F 3 "~" H 2500 1700 50 0001 C CNN
|
||||
F 4 "-test" H 2500 1700 50 0001 C CNN "Config"
|
||||
F 5 "Bourns" H 1000 1700 50 0001 C CNN "manf"
|
||||
F 6 "CR0603-JW-102ELF" H 1000 1700 50 0001 C CNN "manf#"
|
||||
F 7 "CR0603-JW-102ELFCT-ND" H 1000 1700 50 0001 C CNN "digikey#"
|
||||
1 2500 1700
|
||||
1 0 0 -1
|
||||
$EndComp
|
||||
$EndSCHEMATC
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<export version="D">
|
||||
<design>
|
||||
<source>/home/salvador/0Data/Eccosur/kibot/tests/board_samples/kicad_5/1/kibom-variant_2c.sch</source>
|
||||
<date>mar 06 abr 2021 14:20:00</date>
|
||||
<tool>Eeschema 5.1.9+dfsg1-1</tool>
|
||||
<sheet number="1" name="/" tstamps="/">
|
||||
<title_block>
|
||||
<title>KiCost Test Schematic</title>
|
||||
<company>INTI - MyNT</company>
|
||||
<rev>A</rev>
|
||||
<date>2021-04-06</date>
|
||||
<source>kibom-variant_2c.sch</source>
|
||||
<comment number="1" value=""/>
|
||||
<comment number="2" value=""/>
|
||||
<comment number="3" value=""/>
|
||||
<comment number="4" value=""/>
|
||||
</title_block>
|
||||
</sheet>
|
||||
</design>
|
||||
<components>
|
||||
<comp ref="C1">
|
||||
<value>1nF</value>
|
||||
<datasheet>~</datasheet>
|
||||
<fields>
|
||||
<field name="Config">-production,+test</field>
|
||||
<field name="digikey#">1276-1131-1-ND</field>
|
||||
<field name="manf">Samsung</field>
|
||||
<field name="manf#">CL10B102KC8NNNC</field>
|
||||
</fields>
|
||||
<libsource lib="Device" part="C" description="Unpolarized capacitor"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
<tstamp>5F43BEC2</tstamp>
|
||||
</comp>
|
||||
<comp ref="C2">
|
||||
<value>1000 pF</value>
|
||||
<datasheet>~</datasheet>
|
||||
<fields>
|
||||
<field name="Config">+production,+test</field>
|
||||
<field name="digikey#">1276-1131-1-ND</field>
|
||||
<field name="manf">Samsung</field>
|
||||
<field name="manf#">CL10B102KC8NNNC</field>
|
||||
</fields>
|
||||
<libsource lib="Device" part="C" description="Unpolarized capacitor"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
<tstamp>5F43CE1C</tstamp>
|
||||
</comp>
|
||||
<comp ref="R1">
|
||||
<value>1k</value>
|
||||
<datasheet>~</datasheet>
|
||||
<fields>
|
||||
<field name="digikey#">CR0603-JW-102ELFCT-ND</field>
|
||||
<field name="manf">Bourns</field>
|
||||
<field name="manf#">CR0603-JW-102ELF</field>
|
||||
<field name="test:Value">3k3</field>
|
||||
<field name="test:digikey#">CR0603-JW-332ELFCT-ND</field>
|
||||
<field name="test:manf#">CR0603-JW-332ELF</field>
|
||||
</fields>
|
||||
<libsource lib="Device" part="R" description="Resistor"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
<tstamp>5F43D144</tstamp>
|
||||
</comp>
|
||||
<comp ref="R2">
|
||||
<value>1000</value>
|
||||
<datasheet>~</datasheet>
|
||||
<fields>
|
||||
<field name="Config">-test</field>
|
||||
<field name="digikey#">CR0603-JW-102ELFCT-ND</field>
|
||||
<field name="manf">Bourns</field>
|
||||
<field name="manf#">CR0603-JW-102ELF</field>
|
||||
</fields>
|
||||
<libsource lib="Device" part="R" description="Resistor"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
<tstamp>5F43D4BB</tstamp>
|
||||
</comp>
|
||||
</components>
|
||||
<libparts>
|
||||
<libpart lib="Device" part="C">
|
||||
<description>Unpolarized capacitor</description>
|
||||
<docs>~</docs>
|
||||
<footprints>
|
||||
<fp>C_*</fp>
|
||||
</footprints>
|
||||
<fields>
|
||||
<field name="Reference">C</field>
|
||||
<field name="Value">C</field>
|
||||
</fields>
|
||||
<pins>
|
||||
<pin num="1" name="~" type="passive"/>
|
||||
<pin num="2" name="~" type="passive"/>
|
||||
</pins>
|
||||
</libpart>
|
||||
<libpart lib="Device" part="R">
|
||||
<description>Resistor</description>
|
||||
<docs>~</docs>
|
||||
<footprints>
|
||||
<fp>R_*</fp>
|
||||
</footprints>
|
||||
<fields>
|
||||
<field name="Reference">R</field>
|
||||
<field name="Value">R</field>
|
||||
</fields>
|
||||
<pins>
|
||||
<pin num="1" name="~" type="passive"/>
|
||||
<pin num="2" name="~" type="passive"/>
|
||||
</pins>
|
||||
</libpart>
|
||||
</libparts>
|
||||
<libraries>
|
||||
<library logical="Device">
|
||||
<uri>/usr/share/kicad/library/Device.lib</uri>
|
||||
</library>
|
||||
</libraries>
|
||||
<nets>
|
||||
<net code="1" name="Net-(C1-Pad1)">
|
||||
<node ref="C1" pin="1"/>
|
||||
</net>
|
||||
<net code="2" name="Net-(C1-Pad2)">
|
||||
<node ref="C1" pin="2"/>
|
||||
</net>
|
||||
<net code="3" name="Net-(C2-Pad1)">
|
||||
<node ref="C2" pin="1"/>
|
||||
</net>
|
||||
<net code="4" name="Net-(C2-Pad2)">
|
||||
<node ref="C2" pin="2"/>
|
||||
</net>
|
||||
<net code="5" name="Net-(R1-Pad1)">
|
||||
<node ref="R1" pin="1"/>
|
||||
</net>
|
||||
<net code="6" name="Net-(R1-Pad2)">
|
||||
<node ref="R1" pin="2"/>
|
||||
</net>
|
||||
<net code="7" name="Net-(R2-Pad1)">
|
||||
<node ref="R2" pin="1"/>
|
||||
</net>
|
||||
<net code="8" name="Net-(R2-Pad2)">
|
||||
<node ref="R2" pin="2"/>
|
||||
</net>
|
||||
</nets>
|
||||
</export>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gesmes:Envelope xmlns:gesmes="http://www.gesmes.org/xml/2002-08-01" xmlns="http://www.ecb.int/vocabulary/2002-08-01/eurofxref">
|
||||
<gesmes:subject>Reference rates</gesmes:subject>
|
||||
<gesmes:Sender>
|
||||
<gesmes:name>European Central Bank</gesmes:name>
|
||||
</gesmes:Sender>
|
||||
<Cube>
|
||||
<Cube time='2021-04-08'>
|
||||
<Cube currency='USD' rate='1.1873'/>
|
||||
<Cube currency='JPY' rate='129.71'/>
|
||||
<Cube currency='BGN' rate='1.9558'/>
|
||||
<Cube currency='CZK' rate='25.875'/>
|
||||
<Cube currency='DKK' rate='7.4377'/>
|
||||
<Cube currency='GBP' rate='0.86290'/>
|
||||
<Cube currency='HUF' rate='358.65'/>
|
||||
<Cube currency='PLN' rate='4.5513'/>
|
||||
<Cube currency='RON' rate='4.9198'/>
|
||||
<Cube currency='SEK' rate='10.2073'/>
|
||||
<Cube currency='CHF' rate='1.1021'/>
|
||||
<Cube currency='ISK' rate='151.00'/>
|
||||
<Cube currency='NOK' rate='10.0780'/>
|
||||
<Cube currency='HRK' rate='7.5835'/>
|
||||
<Cube currency='RUB' rate='91.4618'/>
|
||||
<Cube currency='TRY' rate='9.6799'/>
|
||||
<Cube currency='AUD' rate='1.5539'/>
|
||||
<Cube currency='BRL' rate='6.6545'/>
|
||||
<Cube currency='CAD' rate='1.4947'/>
|
||||
<Cube currency='CNY' rate='7.7749'/>
|
||||
<Cube currency='HKD' rate='9.2356'/>
|
||||
<Cube currency='IDR' rate='17257.41'/>
|
||||
<Cube currency='ILS' rate='3.8994'/>
|
||||
<Cube currency='INR' rate='88.5885'/>
|
||||
<Cube currency='KRW' rate='1324.91'/>
|
||||
<Cube currency='MXN' rate='23.9497'/>
|
||||
<Cube currency='MYR' rate='4.9125'/>
|
||||
<Cube currency='NZD' rate='1.6855'/>
|
||||
<Cube currency='PHP' rate='57.713'/>
|
||||
<Cube currency='SGD' rate='1.5916'/>
|
||||
<Cube currency='THB' rate='37.347'/>
|
||||
<Cube currency='ZAR' rate='17.2677'/>
|
||||
</Cube>
|
||||
</Cube>
|
||||
</gesmes:Envelope>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,8 @@
|
|||
Prj:,kibom-variant_2c,,,,,Board Qty:,100,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Co.:,INTI - MyNT,,,,,Unit Cost:,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Global Part Info,,,,,,,,Arrow,,,,,Digi-Key,,,,,Farnell,,,,,LCSC,,,,,Mouser,,,,,Newark,,,,,RS Components,,,,,TME,,,,,test,,,,
|
||||
Refs,Value,Footprint,Manf,Manf#,Qty,Unit$,Ext$,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#
|
||||
"R1,R2",1k,,Bourns,CR0603-JW-102ELF,200,0,0,,,,,,51387,,0,0,CR0603-JW-102ELFCT-ND,55000,,0,0,2333561,,,,,,52251,,0,0,652CR0603JW102ELF,110000,,0,0,02J2284,,,,,,,,,,,,,,,
|
||||
|
||||
,USD($)/GBP(£):,1.375941592305018,,,,Purchase description:,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
Prj:,kibom-variant_2c,,,,,Board Qty:,100,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Co.:,INTI - MyNT,,,,,Unit Cost:,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Global Part Info,,,,,,,,Arrow,,,,,Digi-Key,,,,,Farnell,,,,,LCSC,,,,,Mouser,,,,,Newark,,,,,RS Components,,,,,TME,,,,,test,,,,
|
||||
Refs,Value,Footprint,Manf,Manf#,Qty,Unit$,Ext$,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#,Avail,Purch,Unit$,Ext$,Cat#
|
||||
"C1,C2",1nF,,Samsung,CL10B102KC8NNNC,200,0,0,,,,,,NonStk,,0,0,1276-1131-1-ND,3860,,0,0,3013404,542250,,0,0,C153291,NonStk,,0,0,187CL10B102KC8NNNC,19600,,0,0,82AC9311,NonStk,,0,0,7665480,5789,,0,0,CL10B102KC8NNNC,,,,,
|
||||
|
||||
,USD($)/EUR(€):,1.1873,,,,Purchase description:,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,,,0,,,
|
||||
,USD($)/GBP(£):,1.375941592305018,,,,,,,,,,,,,,,,,,,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ For debug information use:
|
|||
pytest-3 --log-cli-level debug
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path as op
|
||||
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__)))
|
||||
prev_dir = op.dirname(op.dirname(op.abspath(__file__)))
|
||||
if prev_dir not in sys.path:
|
||||
sys.path.insert(0, prev_dir)
|
||||
# Utils import
|
||||
|
|
@ -20,10 +20,16 @@ import subprocess
|
|||
OUT_DIR = 'KiCost'
|
||||
|
||||
|
||||
def conver2csv(xlsx):
|
||||
def convert2csv(xlsx, skip_empty=False, sheet=None):
|
||||
csv = xlsx[:-4]+'csv'
|
||||
logging.debug('Converting to CSV')
|
||||
p1 = subprocess.Popen(['xlsx2csv', '--skipemptycolumns', xlsx], stdout=subprocess.PIPE)
|
||||
cmd = ['xlsx2csv']
|
||||
if skip_empty:
|
||||
cmd.append('--skipemptycolumns')
|
||||
if sheet:
|
||||
cmd.extend(['-n', sheet])
|
||||
cmd.append(xlsx)
|
||||
p1 = subprocess.Popen(cmd, 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]
|
||||
|
|
@ -32,10 +38,10 @@ def conver2csv(xlsx):
|
|||
def check_simple(ctx, variant):
|
||||
if variant:
|
||||
variant = '_'+variant
|
||||
name = os.path.join(OUT_DIR, 'simple'+variant+'.xlsx')
|
||||
name = op.join(OUT_DIR, 'simple'+variant+'.xlsx')
|
||||
ctx.expect_out_file(name)
|
||||
xlsx = ctx.get_out_path(name)
|
||||
conver2csv(xlsx)
|
||||
convert2csv(xlsx, skip_empty=True)
|
||||
ctx.compare_txt(name[:-4]+'csv')
|
||||
|
||||
|
||||
|
|
@ -48,3 +54,17 @@ def test_kicost_simple(test_dir):
|
|||
check_simple(ctx, 'production')
|
||||
check_simple(ctx, 'test')
|
||||
ctx.clean_up()
|
||||
|
||||
|
||||
def test_kicost_bom_simple(test_dir):
|
||||
prj = 'kibom-variant_2c'
|
||||
ctx = context.TestContextSCH(test_dir, 'test_kicost_bom_simple', prj, 'int_bom_kicost_simple_xlsx', OUT_DIR)
|
||||
ctx.run(kicost=True) # , extra_debug=True
|
||||
output = op.join(OUT_DIR, prj+'-bom.xlsx')
|
||||
ctx.expect_out_file(output)
|
||||
convert2csv(ctx.get_out_path(output), sheet='Costs')
|
||||
csv = output[:-4]+'csv'
|
||||
ctx.compare_txt(csv)
|
||||
convert2csv(ctx.get_out_path(output), sheet='Costs (DNF)')
|
||||
ctx.compare_txt(csv, output[:-5]+'_dnf.csv')
|
||||
ctx.clean_up()
|
||||
|
|
|
|||
|
|
@ -356,6 +356,7 @@ def test_help_filters(test_dir):
|
|||
|
||||
def test_help_output_plugin_1(test_dir, monkeypatch):
|
||||
ctx = context.TestContext(test_dir, 'test_help_output_plugin_1', '3Rs', 'pre_and_position', POS_DIR)
|
||||
ctx.home_local_link()
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
|
||||
logging.debug('HOME='+os.environ['HOME'])
|
||||
|
|
@ -371,6 +372,7 @@ def test_help_output_plugin_1(test_dir, monkeypatch):
|
|||
|
||||
def test_help_output_plugin_2(test_dir, monkeypatch):
|
||||
ctx = context.TestContext(test_dir, 'test_help_output_plugin_2', '3Rs', 'pre_and_position', POS_DIR)
|
||||
ctx.home_local_link()
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
|
||||
logging.debug('HOME='+os.environ['HOME'])
|
||||
|
|
@ -384,6 +386,7 @@ def test_help_output_plugin_2(test_dir, monkeypatch):
|
|||
|
||||
def test_help_output_plugin_3(test_dir, monkeypatch):
|
||||
ctx = context.TestContext(test_dir, 'test_help_output_plugin_3', '3Rs', 'pre_and_position', POS_DIR)
|
||||
ctx.home_local_link()
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
|
||||
logging.debug('HOME='+os.environ['HOME'])
|
||||
|
|
@ -394,6 +397,7 @@ def test_help_output_plugin_3(test_dir, monkeypatch):
|
|||
|
||||
def test_help_output_plugin_4(test_dir, monkeypatch):
|
||||
ctx = context.TestContext(test_dir, 'test_help_output_plugin_4', '3Rs', 'pre_and_position', POS_DIR)
|
||||
ctx.home_local_link()
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
|
||||
logging.debug('HOME='+os.environ['HOME'])
|
||||
|
|
@ -475,6 +479,7 @@ def test_example_6(test_dir):
|
|||
def test_example_7(test_dir, monkeypatch):
|
||||
""" With dummy plug-ins """
|
||||
ctx = context.TestContext(test_dir, 'Example7', '3Rs', 'pre_and_position', '')
|
||||
ctx.home_local_link()
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
|
||||
ctx.run(extra=['--example'], no_verbose=True, no_yaml_file=True, no_board_file=True)
|
||||
|
|
@ -796,6 +801,7 @@ def test_empty_zip(test_dir):
|
|||
|
||||
def test_compress_fail_deps(test_dir, monkeypatch):
|
||||
ctx = context.TestContext(test_dir, 'test_compress_fail_deps', '3Rs', 'compress_fail_deps', 'Test')
|
||||
ctx.home_local_link()
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("HOME", os.path.join(ctx.get_board_dir(), '../..'))
|
||||
ctx.run(INTERNAL_ERROR)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ KICAD_VERSION_5_99 = 5099000
|
|||
KICAD_VERSION_5_1_7 = 5001007
|
||||
MODE_SCH = 1
|
||||
MODE_PCB = 0
|
||||
# Defined as True to collect real world queries
|
||||
ADD_QUERY_TO_KNOWN = False
|
||||
|
||||
ng_ver = os.environ.get('KIAUS_USE_NIGHTLY')
|
||||
if ng_ver:
|
||||
|
|
@ -280,7 +282,7 @@ class TestContext(object):
|
|||
self.err = self.err.decode()
|
||||
|
||||
def run(self, ret_val=None, extra=None, use_a_tty=False, filename=None, no_out_dir=False, no_board_file=False,
|
||||
no_yaml_file=False, chdir_out=False, no_verbose=False, extra_debug=False, do_locale=False):
|
||||
no_yaml_file=False, chdir_out=False, no_verbose=False, extra_debug=False, do_locale=False, kicost=False):
|
||||
logging.debug('Running '+self.test_name)
|
||||
# Change the command to be local and add the board and output arguments
|
||||
cmd = [os.path.abspath(os.path.dirname(os.path.abspath(__file__))+'/../../src/kibot')]
|
||||
|
|
@ -311,7 +313,29 @@ class TestContext(object):
|
|||
os.environ['LANG'] = do_locale
|
||||
logging.debug('LOCPATH='+os.environ['LOCPATH'])
|
||||
logging.debug('LANG='+os.environ['LANG'])
|
||||
self.do_run(cmd, ret_val, use_a_tty, chdir_out)
|
||||
# KiCost fake environment setup
|
||||
if kicost:
|
||||
# Always fake the currency rates
|
||||
os.environ['KICOST_CURRENCY_RATES'] = 'tests/data/currency_rates.xml'
|
||||
if ADD_QUERY_TO_KNOWN:
|
||||
queries_file = 'tests/data/kitspace_queries.txt'
|
||||
os.environ['KICOST_LOG_HTTP'] = queries_file
|
||||
with open(queries_file, 'at') as f:
|
||||
f.write('# ' + self.board_name + '\n')
|
||||
server = None
|
||||
else:
|
||||
os.environ['KICOST_KITSPACE_URL'] = 'http://localhost:8000'
|
||||
fo = open(self.get_out_path('server_stdout.txt'), 'at')
|
||||
fe = open(self.get_out_path('server_stderr.txt'), 'at')
|
||||
server = subprocess.Popen('./tests/utils/dummy-web-server.py', stdout=fo, stderr=fe)
|
||||
try:
|
||||
self.do_run(cmd, ret_val, use_a_tty, chdir_out)
|
||||
finally:
|
||||
# Always kill the fake web server
|
||||
if kicost and server is not None:
|
||||
server.terminate()
|
||||
fo.close()
|
||||
fe.close()
|
||||
# Do we need to restore the locale?
|
||||
if do_locale:
|
||||
if old_LOCPATH:
|
||||
|
|
@ -689,6 +713,16 @@ class TestContext(object):
|
|||
targets[parts[0].strip()] = parts[1].strip()
|
||||
return targets
|
||||
|
||||
def home_local_link(self):
|
||||
""" Make sure that ./tests can be used as a replacement for HOME.
|
||||
Currently just links ~/.local """
|
||||
home = os.environ.get('HOME')
|
||||
if home is not None:
|
||||
local = os.path.join(home, '.local')
|
||||
fake_local = os.path.join('tests', '.local')
|
||||
if os.path.isdir(local) and not os.path.isdir(fake_local):
|
||||
os.symlink(local, fake_local)
|
||||
|
||||
|
||||
class TestContextSCH(TestContext):
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/python3
|
||||
"""
|
||||
Very simple HTTP server in python (Updated for Python 3.7)
|
||||
|
||||
Usage:
|
||||
|
||||
./dummy-web-server.py -h
|
||||
./dummy-web-server.py -l localhost -p 8000
|
||||
|
||||
Send a GET request:
|
||||
|
||||
curl http://localhost:8000
|
||||
|
||||
Send a HEAD request:
|
||||
|
||||
curl -I http://localhost:8000
|
||||
|
||||
Send a POST request:
|
||||
|
||||
curl -d 'foo=bar&bin=baz' http://localhost:8000
|
||||
|
||||
This code is available for use under the MIT license.
|
||||
|
||||
----
|
||||
|
||||
Copyright 2021 Brad Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the 'Software'), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"""
|
||||
import argparse
|
||||
import os.path as op
|
||||
import sys
|
||||
from urllib.parse import unquote
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
queries = {}
|
||||
comments = {}
|
||||
|
||||
|
||||
class S(BaseHTTPRequestHandler):
|
||||
def _set_headers(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
|
||||
def _html(self, message):
|
||||
"""This just generates an HTML document that includes `message`
|
||||
in the body. Override, or re-write this do do more interesting stuff.
|
||||
|
||||
"""
|
||||
content = "<html><body><h1>{}</h1></body></html>".format(message)
|
||||
return content.encode("utf8") # NOTE: must return a bytes object!
|
||||
|
||||
def do_GET(self):
|
||||
self._set_headers()
|
||||
self.wfile.write(self._html("hi!"))
|
||||
|
||||
def do_HEAD(self):
|
||||
self._set_headers()
|
||||
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf8')
|
||||
self._set_headers()
|
||||
if post_data in queries:
|
||||
self.wfile.write(queries[post_data].encode("utf8"))
|
||||
print("Known query "+comments[post_data])
|
||||
else:
|
||||
data = unquote(post_data.replace('+', ' '))
|
||||
print('Unknown query, len={}\n{}\n{}'.format(content_length, post_data, data))
|
||||
content = "<html><body><h1>POST!</h1><pre>{}</pre></body></html>".format(post_data)
|
||||
self.wfile.write(content.encode("utf8"))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def load_queries(file):
|
||||
global queries
|
||||
with open(file, 'rt') as f:
|
||||
is_query = True
|
||||
last_comment = None
|
||||
id = 1
|
||||
for line in f:
|
||||
line = line[:-1]
|
||||
if line[0] == '#':
|
||||
last_comment = line[1:].strip()
|
||||
id = 1
|
||||
elif is_query:
|
||||
query = line
|
||||
is_query = False
|
||||
else:
|
||||
# print(query)
|
||||
# print(len(query))
|
||||
queries[query] = line
|
||||
comments[query] = '{} ({})'.format(last_comment, id)
|
||||
id += 1
|
||||
is_query = True
|
||||
|
||||
|
||||
def run(server_class=HTTPServer, handler_class=S, addr="localhost", port=8000):
|
||||
server_address = (addr, port)
|
||||
httpd = server_class(server_address, handler_class)
|
||||
load_queries(op.join(op.dirname(__file__), '../data/kitspace_queries.txt'))
|
||||
|
||||
print("Starting httpd server on {}:{}".format(addr, port))
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description="Run a simple HTTP server")
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--listen",
|
||||
default="localhost",
|
||||
help="Specify the IP address on which the server listens",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Specify the port on which the server listens",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
run(addr=args.listen, port=args.port)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Example KiBot config file
|
||||
kibot:
|
||||
version: 1
|
||||
|
||||
outputs:
|
||||
- name: 'bom_internal'
|
||||
comment: "Bill of Materials in HTML format"
|
||||
type: bom
|
||||
dir: KiCost
|
||||
options:
|
||||
xlsx:
|
||||
kicost: true
|
||||
Loading…
Reference in New Issue