diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb653053..e2c571ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- The multipart id to references of multipart components others than part 1.
-- Internal BoM: `no_conflict` option to exclude fields from conflict detection.
-- Internal BoM: HTML tables can be sorted selecting a column (Java Script).
+- Internal BoM:
+ - `no_conflict` option to exclude fields from conflict detection.
+ - HTML tables can be sorted selecting a column (Java Script).
+ - You can consolidate more than one project in one BoM.
- Support for KICAD_CONFIG_HOME defined from inside KiCad.
- Now layers can be selected using the default KiCad names.
- More control over the name of the drill and gerber files.
diff --git a/README.md b/README.md
index 13f6b46d..0a96062f 100644
--- a/README.md
+++ b/README.md
@@ -570,6 +570,12 @@ Next time you need this list just use an alias, like this:
- `name`: [string=''] Used to identify this particular output definition.
- `options`: [dict] Options for the `bom` output.
* Valid keys:
+ - `aggregate`: [list(dict)] Add components from other projects.
+ * Valid keys:
+ - `file`: [string=''] Name of the schematic to aggregate.
+ - `name`: [string=''] Name to identify this source. If empty we use the name of the schematic.
+ - `number`: [number=1] Number of boards to build (components multiplier). Use negative to substract.
+ - `ref_id`: [string=''] A prefix to add to all the references from this project.
- `columns`: [list(dict)|list(string)] List of columns to display.
Can be just the name of the field.
* Valid keys:
@@ -628,7 +634,9 @@ Next time you need this list just use an alias, like this:
- `normalize_values`: [boolean=false] Try to normalize the R, L and C values, producing uniform units and prefixes.
- `number`: [number=1] Number of boards to build (components multiplier).
- `output`: [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options.
+ - `ref_id`: [string=''] A prefix to add to all the references from this project. Used for multiple projects.
- `ref_separator`: [string=' '] Separator used for the list of references.
+ - `source_by_id`: [boolean=false] Generate the `Source BoM` column using the reference ID instead of the project name.
- `use_alt`: [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18.
- `variant`: [string=''] Board variant, used to determine which components
are output to the BoM..
@@ -1846,9 +1854,11 @@ The internal list of rotations is:
- **KiBoM**: Oliver Henry Walters (@SchrodingersGat)
- **Interactive HTML BoM**: @qu1ck
- **PcbDraw**: Jan Mrázek (@yaqwsx)
-- **KiCad Gerber Zipper**: @g200kg
- **Contributors**:
- **Error filters ideas**: Leandro Heck (@leoheck)
- **GitHub Actions Integration/SVG output**: @nerdyscout
+- **Sources of inspiration and good ideas**:
+ - **KiCad Gerber Zipper**: @g200kg
+ - **pimpmykicadbom **: Anton Savov (@antto)
- **Others**:
- **Robot in the logo**: Christian Plaza (from pixabay)
diff --git a/docs/README.in b/docs/README.in
index adac2e41..8f77435d 100644
--- a/docs/README.in
+++ b/docs/README.in
@@ -942,9 +942,11 @@ The internal list of rotations is:
- **KiBoM**: Oliver Henry Walters (@SchrodingersGat)
- **Interactive HTML BoM**: @qu1ck
- **PcbDraw**: Jan Mrázek (@yaqwsx)
-- **KiCad Gerber Zipper**: @g200kg
- **Contributors**:
- **Error filters ideas**: Leandro Heck (@leoheck)
- **GitHub Actions Integration/SVG output**: @nerdyscout
+- **Sources of inspiration and good ideas**:
+ - **KiCad Gerber Zipper**: @g200kg
+ - **pimpmykicadbom **: Anton Savov (@antto)
- **Others**:
- **Robot in the logo**: Christian Plaza (from pixabay)
diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml
index 4b37ed9b..f069dfe3 100644
--- a/docs/samples/generic_plot.kibot.yaml
+++ b/docs/samples/generic_plot.kibot.yaml
@@ -39,6 +39,16 @@ outputs:
type: 'bom'
dir: 'Example/bom_dir'
options:
+ # [list(dict)] Add components from other projects
+ aggregate:
+ # [string=''] Name of the schematic to aggregate
+ - file: ''
+ # [string=''] Name to identify this source. If empty we use the name of the schematic
+ name: ''
+ # [number=1] Number of boards to build (components multiplier). Use negative to substract
+ number: 1
+ # [string=''] A prefix to add to all the references from this project
+ ref_id: ''
# [list(dict)|list(string)] List of columns to display.
# Can be just the name of the field
columns:
@@ -129,8 +139,12 @@ outputs:
number: 1
# [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options
output: '%f-%i%v.%x'
+ # [string=''] A prefix to add to all the references from this project. Used for multiple projects
+ ref_id: ''
# [string=' '] Separator used for the list of references
ref_separator: ' '
+ # [boolean=false] Generate the `Source BoM` column using the reference ID instead of the project name
+ source_by_id: false
# [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18
use_alt: false
# [string=''] Board variant, used to determine which components
diff --git a/kibot/bom/bom.py b/kibot/bom/bom.py
index c7ff1112..09faafa8 100644
--- a/kibot/bom/bom.py
+++ b/kibot/bom/bom.py
@@ -166,8 +166,38 @@ class ComponentGroup(object):
self.components.append(c)
self.refs[c.ref] = c
- def get_count(self):
- return len(self.components)
+ def get_count(self, project=None):
+ if project is None:
+ # Total components
+ return len(self.components)
+ # Only for the specified project
+ return sum(map(lambda c: c.project == project, self.components))
+
+ def get_build_count(self):
+ if not self.is_fitted():
+ # Not fitted -> 0
+ return 0
+ if len(self.cfg.aggregate) == 1:
+ # Just one project
+ return len(self.components)*self.cfg.number
+ # Multiple projects, count them using the number of board for each project
+ return sum(map(lambda c: self.cfg.qtys[c.project], self.components))
+
+ def get_sources(self):
+ sources = {}
+ for c in self.components:
+ if c.project in sources:
+ sources[c.project] += 1
+ else:
+ sources[c.project] = 1
+ field = ''
+ for prj, n in sources.items():
+ if len(field):
+ field += ' '
+ if self.cfg.source_by_id:
+ prj = self.cfg.source_to_id[prj]
+ field += prj+'('+str(n)+')'
+ return field
def is_fitted(self):
# compare_components ensures all has the same status
@@ -196,7 +226,7 @@ class ComponentGroup(object):
""" Alternative list of references using ranges """
S = Joiner()
for n in self.components:
- P, N = (n.ref_prefix, _suffix_to_num(n.ref_suffix))
+ P, N = (n.ref_id+n.ref_prefix, _suffix_to_num(n.ref_suffix))
S.add(P, N)
return S.flush(self.cfg.ref_separator)
@@ -232,9 +262,10 @@ class ComponentGroup(object):
else:
self.fields[ColumnList.COL_REFERENCE_L] = self.get_refs()
# Quantity
- q = self.get_count()
- self.fields[ColumnList.COL_GRP_QUANTITY_L] = str(q)
- self.fields[ColumnList.COL_GRP_BUILD_QUANTITY_L] = str(q * self.cfg.number) if self.is_fitted() else "0"
+ self.fields[ColumnList.COL_GRP_QUANTITY_L] = str(self.get_count())
+ self.total = self.get_build_count()
+ self.fields[ColumnList.COL_GRP_BUILD_QUANTITY_L] = str(self.total)
+ self.fields[ColumnList.COL_SOURCE_BOM_L] = self.get_sources()
# Group status
status = ' '
if not self.is_fitted():
@@ -301,6 +332,25 @@ def normalize_value(c, decimal_point):
return '{} {}{}'.format(value, mult_s, unit)
+def compute_multiple_stats(cfg, groups):
+ for sch in cfg.aggregate:
+ sch.comp_total = 0
+ sch.comp_fitted = 0
+ sch.comp_build = 0
+ sch.comp_groups = 0
+ for g in groups:
+ g_l = g.get_count(sch.name)
+ if g_l:
+ sch.comp_groups = sch.comp_groups+1
+ sch.comp_total += g_l
+ if g.is_fitted():
+ sch.comp_fitted += g_l
+ sch.comp_build = sch.comp_fitted*sch.number
+ if cfg.debug_level > 1:
+ logger.debug('Stats for {}: total {} fitted {} build {}'.
+ format(sch.name, sch.comp_total, sch.comp_fitted, sch.comp_build))
+
+
def group_components(cfg, components):
groups = []
# Iterate through each component, and test whether a group for these already exists
@@ -343,6 +393,7 @@ def group_components(cfg, components):
# Enumerate the groups and compute stats
n_total = 0
n_fitted = 0
+ n_build = 0
c = 1
dnf = 1
cfg.n_groups = len(groups)
@@ -359,9 +410,15 @@ def group_components(cfg, components):
n_total += g_l
if is_fitted:
n_fitted += g_l
+ n_build += g.total
cfg.n_total = n_total
cfg.n_fitted = n_fitted
- cfg.n_build = n_fitted * cfg.number
+ cfg.n_build = n_build
+ if cfg.debug_level > 1:
+ logger.debug('Global stats: total {} fitted {} build {}'.format(n_total, n_fitted, n_build))
+ # Compute stats for multiple schematics
+ if len(cfg.aggregate) > 1:
+ compute_multiple_stats(cfg, groups)
return groups
@@ -370,4 +427,7 @@ def do_bom(file_name, ext, comps, cfg):
groups = group_components(cfg, comps)
# Create the BoM
logger.debug("Saving BOM File: "+file_name)
+ number = cfg.number
+ cfg.number = sum(map(lambda prj: prj.number, cfg.aggregate))
write_bom(file_name, ext, groups, cfg.columns, cfg)
+ cfg.number = number
diff --git a/kibot/bom/columnlist.py b/kibot/bom/columnlist.py
index 73d71a01..06af7959 100644
--- a/kibot/bom/columnlist.py
+++ b/kibot/bom/columnlist.py
@@ -49,6 +49,8 @@ class ColumnList:
COL_GRP_QUANTITY_L = COL_GRP_QUANTITY.lower()
COL_GRP_BUILD_QUANTITY = 'Build Quantity'
COL_GRP_BUILD_QUANTITY_L = COL_GRP_BUILD_QUANTITY.lower()
+ COL_SOURCE_BOM = 'Source BoM'
+ COL_SOURCE_BOM_L = COL_SOURCE_BOM.lower()
# Generated columns
COLUMNS_GEN_L = {
@@ -56,6 +58,7 @@ class ColumnList:
COL_GRP_BUILD_QUANTITY_L: 1,
COL_ROW_NUMBER_L: 1,
COL_STATUS_L: 1,
+ COL_SOURCE_BOM_L: 1,
}
# Default columns
@@ -72,7 +75,8 @@ class ColumnList:
COL_GRP_BUILD_QUANTITY,
COL_STATUS,
COL_DATASHEET,
- COL_SHEETPATH
+ COL_SHEETPATH,
+ COL_SOURCE_BOM,
]
# Default columns
diff --git a/kibot/bom/csv_writer.py b/kibot/bom/csv_writer.py
index dca72765..e1976b12 100644
--- a/kibot/bom/csv_writer.py
+++ b/kibot/bom/csv_writer.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2020 Salvador E. Tropea
-# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial
+# Copyright (c) 2020-2021 Salvador E. Tropea
+# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial
# Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat)
# License: MIT
# Project: KiBot (formerly KiPlot)
@@ -11,6 +11,58 @@ CSV Writer: Generates a CSV, TSV or TXT BoM file.
import csv
+def write_stats(writer, cfg):
+ if len(cfg.aggregate) == 1:
+ # Only one project
+ if not cfg.csv.hide_pcb_info:
+ prj = cfg.aggregate[0]
+ writer.writerow(["Project info:"])
+ writer.writerow(["Schematic:", prj.name])
+ writer.writerow(["Variant:", cfg.variant.name])
+ writer.writerow(["Revision:", prj.sch.revision])
+ writer.writerow(["Date:", prj.sch.date])
+ writer.writerow(["KiCad Version:", cfg.kicad_version])
+ if not cfg.csv.hide_stats_info:
+ writer.writerow(["Statistics:"])
+ writer.writerow(["Component Groups:", cfg.n_groups])
+ writer.writerow(["Component Count:", cfg.n_total])
+ writer.writerow(["Fitted Components:", cfg.n_fitted])
+ writer.writerow(["Number of PCBs:", cfg.number])
+ writer.writerow(["Total Components:", cfg.n_build])
+ else:
+ # Multiple projects
+ if not cfg.csv.hide_pcb_info:
+ prj = cfg.aggregate[0]
+ writer.writerow(["Project info:"])
+ writer.writerow(["Variant:", cfg.variant.name])
+ writer.writerow(["KiCad Version:", cfg.kicad_version])
+ if not cfg.csv.hide_stats_info:
+ writer.writerow(["Global statistics:"])
+ writer.writerow(["Component Groups:", cfg.n_groups])
+ writer.writerow(["Component Count:", cfg.n_total])
+ writer.writerow(["Fitted Components:", cfg.n_fitted])
+ writer.writerow(["Number of PCBs:", cfg.number])
+ writer.writerow(["Total Components:", cfg.n_build])
+ # Individual stats
+ for prj in cfg.aggregate:
+ if not cfg.csv.hide_pcb_info:
+ writer.writerow(["Project info:", prj.sch.title])
+ writer.writerow(["Schematic:", prj.name])
+ writer.writerow(["Revision:", prj.sch.revision])
+ writer.writerow(["Date:", prj.sch.date])
+ if prj.sch.company:
+ writer.writerow(["Company:", prj.sch.company])
+ if prj.ref_id:
+ writer.writerow(["ID", prj.ref_id])
+ if not cfg.csv.hide_stats_info:
+ writer.writerow(["Statistics:", prj.sch.title])
+ writer.writerow(["Component Groups:", prj.comp_groups])
+ writer.writerow(["Component Count:", prj.comp_total])
+ writer.writerow(["Fitted Components:", prj.comp_fitted])
+ writer.writerow(["Number of PCBs:", prj.number])
+ writer.writerow(["Total Components:", prj.comp_build])
+
+
def write_csv(filename, ext, groups, headings, head_names, cfg):
"""
Write BoM out to a CSV file
@@ -50,19 +102,6 @@ def write_csv(filename, ext, groups, headings, head_names, cfg):
for i in range(5):
writer.writerow([])
# The info
- if not cfg.csv.hide_pcb_info:
- writer.writerow(["Project info:"])
- writer.writerow(["Schematic:", cfg.source])
- writer.writerow(["Variant:", cfg.variant.name])
- writer.writerow(["Revision:", cfg.revision])
- writer.writerow(["Date:", cfg.date])
- writer.writerow(["KiCad Version:", cfg.kicad_version])
- if not cfg.csv.hide_stats_info:
- writer.writerow(["Statistics:"])
- writer.writerow(["Component Groups:", cfg.n_groups])
- writer.writerow(["Component Count:", cfg.n_total])
- writer.writerow(["Fitted Components:", cfg.n_fitted])
- writer.writerow(["Number of PCBs:", cfg.number])
- writer.writerow(["Total Components:", cfg.n_build])
+ write_stats(writer, cfg)
return True
diff --git a/kibot/bom/html_writer.py b/kibot/bom/html_writer.py
index 6bfe10f2..2a9857c5 100644
--- a/kibot/bom/html_writer.py
+++ b/kibot/bom/html_writer.py
@@ -32,6 +32,7 @@ STYLE_COMMON = (" .cell-title { vertical-align: bottom; }\n"
" .cell-info { vertical-align: top; padding: 1em;}\n"
" .cell-stats { vertical-align: top; padding: 1em;}\n"
" .title { font-size:2.5em; font-weight: bold; }\n"
+ " .subtitle { font-size:1.5em; font-weight: bold; }\n"
" .h2 { font-size:1.5em; font-weight: bold; }\n"
" .td-empty0 { text-align: center; background-color: "+BG_EMPTY+";}\n"
" .td-gen0 { text-align: center; background-color: "+BG_GEN+";}\n"
@@ -274,6 +275,75 @@ def embed_image(file):
return int(w), int(h), 'data:image/png;base64,'+b64encode(s).decode('ascii')
+def write_stats(html, cfg):
+ if len(cfg.aggregate) == 1:
+ # Only one project
+ html.write('
\n')
+ html.write(' \n')
+ if not cfg.html.hide_pcb_info:
+ prj = cfg.aggregate[0]
+ html.write(" Schematic: {} \n".format(prj.name))
+ html.write(" Variant: {} \n".format(cfg.variant.name))
+ html.write(" Revision: {} \n".format(prj.sch.revision))
+ html.write(" Date: {} \n".format(prj.sch.date))
+ html.write(" KiCad Version: {} \n".format(cfg.kicad_version))
+ html.write(' | \n')
+ html.write(' \n')
+ if not cfg.html.hide_stats_info:
+ html.write(" Component Groups: {} \n".format(cfg.n_groups))
+ html.write(" Component Count: {} (per PCB) \n\n".format(cfg.n_total))
+ html.write(" Fitted Components: {} (per PCB) \n".format(cfg.n_fitted))
+ html.write(" Number of PCBs: {} \n".format(cfg.number))
+ html.write(" Total Components: {t} (for {n} PCBs) \n".format(n=cfg.number, t=cfg.n_build))
+ html.write(' | \n')
+ html.write('
\n')
+ else:
+ # Multiple projects
+ # Global stats
+ html.write('\n')
+ html.write(' \n')
+ if not cfg.html.hide_pcb_info:
+ html.write(" Variant: {} \n".format(cfg.variant.name))
+ html.write(" KiCad Version: {} \n".format(cfg.kicad_version))
+ html.write(' | \n')
+ html.write(' \n')
+ if not cfg.html.hide_stats_info:
+ html.write(" Component Groups: {} \n".format(cfg.n_groups))
+ html.write(" Component Count: {} (per PCB) \n\n".format(cfg.n_total))
+ html.write(" Fitted Components: {} (per PCB) \n".format(cfg.n_fitted))
+ html.write(" Number of PCBs: {} \n".format(cfg.number))
+ html.write(" Total Components: {t} (for {n} PCBs) \n".format(n=cfg.number, t=cfg.n_build))
+ html.write(' | \n')
+ html.write('
\n')
+ # Individual stats
+ for prj in cfg.aggregate:
+ html.write('\n')
+ html.write(' | \n')
+ html.write(' '+prj.sch.title+' \n')
+ html.write(' | \n')
+ html.write('
\n')
+ html.write('\n')
+ html.write(' \n')
+ if not cfg.html.hide_pcb_info:
+ html.write(" Schematic: {} \n".format(prj.name))
+ html.write(" Revision: {} \n".format(prj.sch.revision))
+ html.write(" Date: {} \n".format(prj.sch.date))
+ if prj.sch.company:
+ html.write(" Company: {} \n".format(prj.sch.company))
+ if prj.ref_id:
+ html.write(" ID: {} \n".format(prj.ref_id))
+ html.write(' | \n')
+ html.write(' \n')
+ if not cfg.html.hide_stats_info:
+ html.write(" Component Groups: {} \n".format(prj.comp_groups))
+ html.write(" Component Count: {} (per PCB) \n\n".format(prj.comp_total))
+ html.write(" Fitted Components: {} (per PCB) \n".format(prj.comp_fitted))
+ html.write(" Number of PCBs: {} \n".format(prj.number))
+ html.write(" Total Components: {t} (for {n} PCBs) \n".format(n=prj.number, t=prj.comp_build))
+ html.write(' | \n')
+ html.write('
\n')
+
+
def write_html(filename, groups, headings, head_names, cfg):
"""
Write BoM out to a HTML file
@@ -340,7 +410,10 @@ def write_html(filename, groups, headings, head_names, cfg):
if img or not cfg.html.hide_pcb_info or not cfg.html.hide_stats_info or cfg.html.title:
html.write('\n')
html.write('\n')
- html.write(' | \n')
+ n = 2
+ if len(cfg.aggregate) > 1:
+ n += 2*len(cfg.aggregate)
+ html.write(' | \n'.format(n))
if img:
html.write(' \n')
html.write(' | \n')
@@ -349,24 +422,7 @@ def write_html(filename, groups, headings, head_names, cfg):
html.write(' '+cfg.html.title+'
\n')
html.write(' \n')
html.write('
\n')
- html.write('\n')
- html.write(' \n')
- if not cfg.html.hide_pcb_info:
- html.write(" Schematic: {} \n".format(cfg.source))
- html.write(" Variant: {} \n".format(cfg.variant.name))
- html.write(" Revision: {} \n".format(cfg.revision))
- html.write(" Date: {} \n".format(cfg.date))
- html.write(" KiCad Version: {} \n".format(cfg.kicad_version))
- html.write(' | \n')
- html.write(' \n')
- if not cfg.html.hide_stats_info:
- html.write(" Component Groups: {} \n".format(cfg.n_groups))
- html.write(" Component Count: {} (per PCB) \n\n".format(cfg.n_total))
- html.write(" Fitted Components: {} (per PCB) \n".format(cfg.n_fitted))
- html.write(" Number of PCBs: {} \n".format(cfg.number))
- html.write(" Total Components: {t} (for {n} PCBs) \n".format(n=cfg.number, t=cfg.n_build))
- html.write(' | \n')
- html.write('
\n')
+ write_stats(html, cfg)
html.write('
\n')
# Fitted groups
diff --git a/kibot/bom/xlsx_writer.py b/kibot/bom/xlsx_writer.py
index 5874f9f1..04708c1f 100644
--- a/kibot/bom/xlsx_writer.py
+++ b/kibot/bom/xlsx_writer.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2020 Salvador E. Tropea
-# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial
+# Copyright (c) 2020-2021 Salvador E. Tropea
+# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial
# Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat)
# License: MIT
# Project: KiBot (formerly KiPlot)
@@ -67,14 +67,16 @@ def add_info(worksheet, column_widths, row, col_offset, formats, text, value):
def compute_head_size(cfg):
- head_size = 7
- if cfg.xlsx.logo is None:
- if not cfg.xlsx.title:
- head_size -= 1
- if cfg.xlsx.hide_pcb_info and cfg.xlsx.hide_stats_info:
- head_size -= 5
- if head_size == 1:
- head_size = 0
+ col_logo = 0 if cfg.xlsx.logo is None else 6
+ col_info = 1 if cfg.xlsx.title else 0
+ if not (cfg.xlsx.hide_pcb_info and cfg.xlsx.hide_stats_info):
+ col_info += 5
+ if len(cfg.aggregate) > 1:
+ col_info += 6*len(cfg.aggregate)
+ head_size = max(col_logo, col_info)
+ if head_size:
+ # To separate
+ head_size += 1
return head_size
@@ -116,6 +118,15 @@ def create_fmt_title(workbook, title):
return fmt_title
+def create_fmt_subtitle(workbook):
+ fmt_title = workbook.add_format(DEFAULT_FMT)
+ fmt_title.set_font_size(18)
+ fmt_title.set_bold()
+ fmt_title.set_font_name('Arial')
+ fmt_title.set_align('left')
+ return fmt_title
+
+
def create_fmt_cols(workbook, col_colors):
""" Create the possible column formats """
fmt_cols = []
@@ -202,6 +213,67 @@ def adjust_heights(worksheet, rows, max_width, head_size):
worksheet.set_row(head_size+rn, 15.0*max_h)
+def write_info(cfg, r_info_start, worksheet, column_widths, col1, fmt_info, fmt_subtitle):
+ if len(cfg.aggregate) == 1:
+ # Only one project
+ rc = r_info_start
+ if not cfg.xlsx.hide_pcb_info:
+ prj = cfg.aggregate[0]
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Schematic:", prj.name)
+ 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:", prj.sch.revision)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Date:", prj.sch.date)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "KiCad Version:", cfg.kicad_version)
+ col1 += 2
+ rc = r_info_start
+ if not cfg.xlsx.hide_stats_info:
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Groups:", cfg.n_groups)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Count:", cfg.n_total)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Fitted Components:", cfg.n_fitted)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Number of PCBs:", cfg.number)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", cfg.n_build)
+ else:
+ # Multiple projects
+ # Global stats
+ old_col1 = col1
+ rc = r_info_start
+ if not cfg.xlsx.hide_pcb_info:
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Variant:", cfg.variant.name)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "KiCad Version:", cfg.kicad_version)
+ col1 += 2
+ rc = r_info_start
+ if not cfg.xlsx.hide_stats_info:
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Groups:", cfg.n_groups)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Count:", cfg.n_total)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Fitted Components:", cfg.n_fitted)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Number of PCBs:", cfg.number)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", cfg.n_build)
+ # Individual stats
+ for prj in cfg.aggregate:
+ r_info_start += 5
+ col1 = old_col1
+ worksheet.set_row(r_info_start, 24)
+ worksheet.merge_range(r_info_start, col1, r_info_start, len(column_widths)-1, prj.sch.title, fmt_subtitle)
+ r_info_start += 1
+ rc = r_info_start
+ if not cfg.xlsx.hide_pcb_info:
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Schematic:", prj.name)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Revision:", prj.sch.revision)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Date:", prj.sch.date)
+ if prj.sch.company:
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Company:", prj.sch.company)
+ if prj.ref_id:
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "ID:", prj.ref_id)
+ col1 += 2
+ rc = r_info_start
+ if not cfg.xlsx.hide_stats_info:
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Groups:", prj.comp_groups)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Count:", prj.comp_total)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Fitted Components:", prj.comp_fitted)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Number of PCBs:", prj.number)
+ rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", prj.comp_build)
+
+
def write_xlsx(filename, groups, col_fields, head_names, cfg):
"""
Write BoM out to a XLSX file
@@ -245,6 +317,7 @@ def write_xlsx(filename, groups, col_fields, head_names, cfg):
image_data = get_logo_data(cfg.xlsx.logo)
# Title
fmt_title = create_fmt_title(workbook, cfg.xlsx.title)
+ fmt_subtitle = create_fmt_subtitle(workbook)
# Info
fmt_info = create_fmt_info(workbook, cfg)
@@ -310,21 +383,7 @@ def write_xlsx(filename, groups, col_fields, head_names, cfg):
worksheet.merge_range(0, col1, 0, len(column_widths)-1, cfg.xlsx.title, fmt_title)
# PCB & Stats Info
if not (cfg.xlsx.hide_pcb_info and cfg.xlsx.hide_stats_info):
- 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:", 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)
- col1 += 2
- rc = r_info_start
- if not cfg.xlsx.hide_stats_info:
- rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Groups:", cfg.n_groups)
- rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Count:", cfg.n_total)
- rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Fitted Components:", cfg.n_fitted)
- rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Number of PCBs:", cfg.number)
- rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", cfg.n_build)
+ write_info(cfg, r_info_start, worksheet, column_widths, col1, fmt_info, fmt_subtitle)
# Adjust cols and rows
adjust_widths(worksheet, column_widths, max_width)
diff --git a/kibot/bom/xml_writer.py b/kibot/bom/xml_writer.py
index fa514896..869ad582 100644
--- a/kibot/bom/xml_writer.py
+++ b/kibot/bom/xml_writer.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2020 Salvador E. Tropea
-# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial
+# Copyright (c) 2020-2021 Salvador E. Tropea
+# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial
# Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat)
# License: MIT
# Project: KiBot (formerly KiPlot)
@@ -23,9 +23,6 @@ def write_xml(filename, groups, headings, head_names, cfg):
cfg = BoMOptions object with all the configuration
"""
attrib = {}
- attrib['Schematic_Source'] = cfg.source
- attrib['Schematic_Revision'] = cfg.revision
- attrib['Schematic_Date'] = cfg.date
attrib['PCB_Variant'] = cfg.variant.name
attrib['KiCad_Version'] = cfg.kicad_version
attrib['Component_Groups'] = str(cfg.n_groups)
@@ -33,7 +30,22 @@ def write_xml(filename, groups, headings, head_names, cfg):
attrib['Fitted_Components'] = str(cfg.n_fitted)
attrib['Number_of_PCBs'] = str(cfg.number)
attrib['Total_Components'] = str(cfg.n_build)
-
+ if len(cfg.aggregate) == 1:
+ prj = cfg.aggregate[0]
+ attrib['Schematic_Source'] = prj.name
+ attrib['Schematic_Revision'] = prj.sch.revision
+ attrib['Schematic_Date'] = prj.sch.date
+ else:
+ for n, prj in enumerate(cfg.aggregate):
+ attrib['Schematic{}_Source'.format(n)] = prj.name
+ attrib['Schematic{}_Revision'.format(n)] = prj.sch.revision
+ attrib['Schematic{}_Date'.format(n)] = prj.sch.date
+ attrib['Schematic{}_ID'.format(n)] = prj.ref_id
+ attrib['Component_Groups{}'.format(n)] = str(prj.comp_groups)
+ attrib['Component_Count{}'.format(n)] = str(prj.comp_total)
+ attrib['Fitted_Components{}'.format(n)] = str(prj.comp_fitted)
+ attrib['Number_of_PCBs{}'.format(n)] = str(prj.number)
+ attrib['Total_Components{}'.format(n)] = str(prj.comp_build)
xml = ElementTree.Element('KiCad_BOM', attrib=attrib, encoding='utf-8')
for group in groups:
if cfg.ignore_dnf and not group.is_fitted():
diff --git a/kibot/gs.py b/kibot/gs.py
index 7c0c9f86..ab0783d6 100644
--- a/kibot/gs.py
+++ b/kibot/gs.py
@@ -4,7 +4,6 @@
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
import os
-import re
from datetime import datetime
from sys import exit
from .misc import (EXIT_BAD_ARGS)
@@ -94,37 +93,11 @@ class GS(object):
def load_sch_title_block():
if GS.sch_title is not None:
return
- GS.sch_title = ''
- GS.sch_date = ''
- GS.sch_rev = ''
- GS.sch_comp = ''
- re_val = re.compile(r"(\w+)\s+\"([^\"]+)\"")
- with open(GS.sch_file) as f:
- for line in f:
- m = re_val.match(line)
- if not m:
- if line.startswith("$EndDescr"):
- break
- # This line is executed, but coverage fails to detect it
- continue # pragma: no cover
- name, val = m.groups()
- if name == "Title":
- GS.sch_title = val
- elif name == "Date":
- GS.sch_date = val
- elif name == "Rev":
- GS.sch_rev = val
- elif name == "Comp":
- GS.sch_comp = val
- if not GS.sch_date:
- file_mtime = os.path.getmtime(GS.sch_file)
- GS.sch_date = datetime.fromtimestamp(file_mtime).strftime('%Y-%m-%d_%H-%M-%S')
- if not GS.sch_title:
- GS.sch_title = GS.sch_basename
- logger.debug("SCH title: `{}`".format(GS.sch_title))
- logger.debug("SCH date: `{}`".format(GS.sch_date))
- logger.debug("SCH revision: `{}`".format(GS.sch_rev))
- logger.debug("SCH company: `{}`".format(GS.sch_comp))
+ assert GS.sch is not None
+ GS.sch_title = GS.sch.title
+ GS.sch_date = GS.sch.date
+ GS.sch_rev = GS.sch.revision
+ GS.sch_comp = GS.sch.company
@staticmethod
def load_pcb_title_block():
diff --git a/kibot/kicad/v5_sch.py b/kibot/kicad/v5_sch.py
index 7f3757e4..94ed6119 100644
--- a/kibot/kicad/v5_sch.py
+++ b/kibot/kicad/v5_sch.py
@@ -11,6 +11,7 @@ Currently oriented to collect the components for the BoM.
# Encapsulate file/line
import re
import os
+from datetime import datetime
from copy import deepcopy
from collections import OrderedDict
from .config import KiConf, un_quote
@@ -807,6 +808,10 @@ class SchematicComponent(object):
self.footprint = ''
self.datasheet = ''
self.desc = ''
+ self.fields = []
+ self.dfields = {}
+ self.fields_bkp = None
+ self.dfields_bkp = None
# Will be computed
self.fitted = True
self.included = True
@@ -941,7 +946,7 @@ class SchematicComponent(object):
return '{} ({} {})'.format(ref, self.name, self.value)
@staticmethod
- def load(f, sheet_path, sheet_path_h, libs, fields, fields_lc):
+ def load(f, project, sheet_path, sheet_path_h, libs, fields, fields_lc):
# L lib:name reference
line = f.get_line()
if not line or line[0] != 'L':
@@ -950,6 +955,7 @@ class SchematicComponent(object):
if len(res) != 2:
raise SchFileError('Malformed component label', line, f)
comp = SchematicComponent()
+ comp.project = project
comp.name, comp.f_ref = res
res = comp.name.split(':')
comp.lib = None
@@ -985,10 +991,6 @@ class SchematicComponent(object):
comp.ar.append(SchematicAltRef.parse(line))
line = f.get_line()
# F field_number "text" orientation posX posY size Flags (see below) hjustify vjustify/italic/bold "name"
- comp.fields = []
- comp.dfields = {}
- comp.fields_bkp = None
- comp.dfields_bkp = None
while line[0] == 'F':
field = SchematicField.parse(line, f)
name_lc = field.name.lower()
@@ -1275,7 +1277,7 @@ class SchematicSheet(object):
self.sheet = None
self.id = ''
- def load_sheet(self, parent, sheet_path, sheet_path_h, libs, fields, fields_lc):
+ def load_sheet(self, project, parent, sheet_path, sheet_path_h, libs, fields, fields_lc):
assert self.name
self.sheet = Schematic()
parent_dir = os.path.dirname(parent)
@@ -1283,7 +1285,7 @@ class SchematicSheet(object):
if len(sheet_path_h) > 1:
sheet_path_h += '/'
sheet_path_h += self.name if self.name else 'Unknown'
- self.sheet.load(os.path.join(parent_dir, self.file), sheet_path, sheet_path_h, libs, fields, fields_lc)
+ self.sheet.load(os.path.join(parent_dir, self.file), project, sheet_path, sheet_path_h, libs, fields, fields_lc)
return self.sheet
@staticmethod
@@ -1366,6 +1368,10 @@ class Schematic(object):
while True:
line = f.get_line()
if line.startswith('$EndDescr'):
+ self.title = self.title_block['Title'] if 'Title' in self.title_block else ''
+ self.date = self.title_block['Date'] if 'Date' in self.title_block else ''
+ self.revision = self.title_block['Rev'] if 'Rev' in self.title_block else ''
+ self.company = self.title_block['Comp'] if 'Comp' in self.title_block else ''
return
elif line.startswith('encoding'):
if line[9:14] != 'utf-8':
@@ -1382,7 +1388,7 @@ class Schematic(object):
raise SchFileError('Wrong entry in title block', line, f)
self.title_block[m.group(1)] = m.group(2)
- def load(self, fname, sheet_path='', sheet_path_h='/', libs={}, fields=[], fields_lc=set()):
+ def load(self, fname, project, sheet_path='', sheet_path_h='/', libs={}, fields=[], fields_lc=set()):
""" Load a v5.x KiCad Schematic.
The caller must be sure the file exists.
Only the schematics are loaded not the libs. """
@@ -1391,6 +1397,7 @@ class Schematic(object):
self.libs = libs
self.fields = fields
self.fields_lc = fields_lc
+ self.project = project
with open(fname, 'rt') as fh:
f = SCHLineReader(fh, fname)
line = f.get_line()
@@ -1410,7 +1417,18 @@ class Schematic(object):
line = f.get_line()
if not line.startswith('EELAYER END'):
raise SchFileError('Missing EELAYER END', line, f)
+ # Load the title block
self._get_title_block(f)
+ # Fill in some missing info
+ if not self.date:
+ file_mtime = os.path.getmtime(fname)
+ self.date = datetime.fromtimestamp(file_mtime).strftime('%Y-%m-%d_%H-%M-%S')
+ if not self.title:
+ self.title = os.path.splitext(os.path.basename(fname))[0]
+ logger.debug("SCH title: `{}`".format(self.title))
+ logger.debug("SCH date: `{}`".format(self.date))
+ logger.debug("SCH revision: `{}`".format(self.revision))
+ logger.debug("SCH company: `{}`".format(self.company))
line = f.get_line()
self.all = []
self.components = []
@@ -1421,7 +1439,7 @@ class Schematic(object):
self.sheets = []
while not line.startswith('$EndSCHEMATC'):
if line.startswith('$Comp'):
- obj = SchematicComponent.load(f, sheet_path, sheet_path_h, libs, fields, fields_lc)
+ obj = SchematicComponent.load(f, project, sheet_path, sheet_path_h, libs, fields, fields_lc)
self.components.append(obj)
elif line.startswith('NoConn'):
obj = SchematicConnection.parse(False, line[7:], f)
@@ -1448,7 +1466,7 @@ class Schematic(object):
# Load sub-sheets
self.sub_sheets = []
for sch in self.sheets:
- self.sub_sheets.append(sch.load_sheet(fname, sheet_path, sheet_path_h, libs, fields, fields_lc))
+ self.sub_sheets.append(sch.load_sheet(project, fname, sheet_path, sheet_path_h, libs, fields, fields_lc))
def get_files(self):
""" A list of the names for all the sheets, including this one. """
diff --git a/kibot/kiplot.py b/kibot/kiplot.py
index c487dac5..e6d654ac 100644
--- a/kibot/kiplot.py
+++ b/kibot/kiplot.py
@@ -145,19 +145,12 @@ def load_board(pcb_file=None):
return board
-def load_sch():
- if GS.sch: # Already loaded
- return
- GS.check_sch()
- # We can't yet load the new format
- if GS.sch_file[-9:] == 'kicad_sch':
- return
- GS.sch = Schematic()
+def load_any_sch(sch, file, project):
try:
- GS.sch.load(GS.sch_file)
- GS.sch.load_libs(GS.sch_file)
+ sch.load(file, project)
+ sch.load_libs(file)
if GS.debug_level > 1:
- logger.debug('Schematic dependencies: '+str(GS.sch.get_files()))
+ logger.debug('Schematic dependencies: '+str(sch.get_files()))
except SchFileError as e:
trace_dump()
logger.error('At line {} of `{}`: {}'.format(e.line, e.file, e.msg))
@@ -170,6 +163,17 @@ def load_sch():
exit(EXIT_BAD_CONFIG)
+def load_sch():
+ if GS.sch: # Already loaded
+ return
+ GS.check_sch()
+ # We can't yet load the new format
+ if GS.sch_file[-9:] == 'kicad_sch':
+ return
+ GS.sch = Schematic()
+ load_any_sch(GS.sch, GS.sch_file, GS.sch_basename)
+
+
def get_board_comps_data(comps):
""" Add information from the PCB to the list of components from the schematic.
Note that we do it every time the function is called to reset transformation filters like rot_footprint. """
diff --git a/kibot/out_bom.py b/kibot/out_bom.py
index 58563183..84e6c670 100644
--- a/kibot/out_bom.py
+++ b/kibot/out_bom.py
@@ -12,12 +12,13 @@ from .gs import GS
from .optionable import Optionable, BaseOptions
from .registrable import RegOutput
from .error import KiPlotConfigurationError
-from .kiplot import get_board_comps_data
-from .macros import macros, document, output_class # noqa: F401
+from .kiplot import get_board_comps_data, load_any_sch
from .bom.columnlist import ColumnList, BoMError
from .bom.bom import do_bom
from .var_kibom import KiBoM
+from .kicad.v5_sch import Schematic
from .fil_base import BaseFilter, apply_exclude_filter, apply_fitted_filter, apply_fixed_filter, reset_filters
+from .macros import macros, document, output_class # noqa: F401
from . import log
# To debug the `with document` we can use:
# from .mcpyrate.debug import macros, step_expansion
@@ -186,6 +187,27 @@ class NoConflict(Optionable):
super().__init__()
+class Aggregate(Optionable):
+ def __init__(self):
+ super().__init__()
+ with document:
+ self.file = ''
+ """ Name of the schematic to aggregate """
+ self.name = ''
+ """ Name to identify this source. If empty we use the name of the schematic """
+ self.ref_id = ''
+ """ A prefix to add to all the references from this project """
+ self.number = 1
+ """ Number of boards to build (components multiplier). Use negative to substract """
+
+ def config(self):
+ super().config()
+ if not self.file:
+ raise KiPlotConfigurationError("Missing or empty `file` in aggregate list ({})".format(str(self._tree)))
+ if not self.name:
+ self.name = os.path.splitext(os.path.basename(self.file))[0]
+
+
class BoMOptions(BaseOptions):
def __init__(self):
with document:
@@ -256,6 +278,12 @@ class BoMOptions(BaseOptions):
""" [list(string)] List of fields where we tolerate conflicts.
Use it to avoid undesired warnings.
By default the field indicated in `fit_field` and the field `part` are excluded """
+ self.aggregate = Aggregate
+ """ [list(dict)] Add components from other projects """
+ self.ref_id = ''
+ """ A prefix to add to all the references from this project. Used for multiple projects """
+ self.source_by_id = False
+ """ Generate the `Source BoM` column using the reference ID instead of the project name """
self._format_example = 'CSV'
super().__init__()
@@ -341,6 +369,9 @@ class BoMOptions(BaseOptions):
for field in self.no_conflict:
no_conflict.add(field.lower())
self.no_conflict = no_conflict
+ # Make sure aggregate is a list
+ if isinstance(self.aggregate, type):
+ self.aggregate = []
# Columns
self.column_rename = {}
self.join = []
@@ -351,9 +382,11 @@ class BoMOptions(BaseOptions):
# Ignore the part and footprint library, also sheetpath and the Reference in singular
ignore = [ColumnList.COL_PART_LIB_L, ColumnList.COL_FP_LIB_L, ColumnList.COL_SHEETPATH_L,
ColumnList.COL_REFERENCE_L[:-1]]
- if self.number <= 1:
- # For one board avoid COL_GRP_BUILD_QUANTITY
- ignore.append(ColumnList.COL_GRP_BUILD_QUANTITY_L)
+ if len(self.aggregate) == 0:
+ ignore.append(ColumnList.COL_SOURCE_BOM_L)
+ if self.number == 1:
+ # For one board avoid COL_GRP_BUILD_QUANTITY
+ ignore.append(ColumnList.COL_GRP_BUILD_QUANTITY_L)
# Exclude the particular columns
self.columns = [h for h in valid_columns if not h.lower() in ignore]
else:
@@ -384,6 +417,24 @@ class BoMOptions(BaseOptions):
# This is the ordered list with the case style defined by the user
self.columns = columns
+ def aggregate_comps(self, comps):
+ self.qtys = {GS.sch_basename: self.number}
+ for prj in self.aggregate:
+ if not os.path.isfile(prj.file):
+ raise KiPlotConfigurationError("Missing `{}`".format(prj.file))
+ logger.debug('Adding components from project {} ({}) using reference id `{}`'.
+ format(prj.name, prj.file, prj.ref_id))
+ self.qtys[prj.name] = prj.number
+ prj.sch = Schematic()
+ load_any_sch(prj.sch, prj.file, prj.name)
+ new_comps = prj.sch.get_components()
+ if prj.ref_id:
+ for c in new_comps:
+ c.ref = prj.ref_id+c.ref
+ c.ref_id = prj.ref_id
+ comps.extend(new_comps)
+ prj.source = os.path.basename(prj.file)
+
def run(self, output_dir):
format = self.format.lower()
output = self.expand_filename_sch(output_dir, self.output, 'bom', format)
@@ -397,6 +448,12 @@ class BoMOptions(BaseOptions):
# Get the components list from the schematic
comps = GS.sch.get_components()
get_board_comps_data(comps)
+ # Apply the reference prefix
+ for c in comps:
+ c.ref = self.ref_id+c.ref
+ c.ref_id = self.ref_id
+ # Aggregate components from other projects
+ self.aggregate_comps(comps)
# Apply all the filters
reset_filters(comps)
apply_exclude_filter(comps, self.exclude_filter)
@@ -404,10 +461,27 @@ class BoMOptions(BaseOptions):
apply_fixed_filter(comps, self.dnc_filter)
# Apply the variant
self.variant.filter(comps)
+ # We add the main project to the aggregate list so do_bom sees a complete list
+ base_sch = Aggregate()
+ base_sch.file = GS.sch_file
+ base_sch.name = GS.sch_basename
+ base_sch.ref_id = self.ref_id
+ base_sch.number = self.number
+ base_sch.sch = GS.sch
+ self.aggregate.insert(0, base_sch)
+ # To translate project to ID
+ if self.source_by_id:
+ self.source_to_id = {prj.name: prj.ref_id for prj in self.aggregate}
try:
do_bom(output, format, comps, self)
except BoMError as e:
raise KiPlotConfigurationError(str(e))
+ # Undo the reference prefix
+ if self.ref_id:
+ l_id = len(self.ref_id)
+ for c in filter(lambda c: c.project == GS.sch_basename, comps):
+ c.ref = c.ref[l_id:]
+ c.ref_id = ''
def get_targets(self, parent, out_dir):
return [self.expand_filename_sch(out_dir, self.output, 'bom', self.format.lower())]
diff --git a/kibot/pre_erc.py b/kibot/pre_erc.py
index 6bf0aaae..a95f7cdf 100644
--- a/kibot/pre_erc.py
+++ b/kibot/pre_erc.py
@@ -7,7 +7,7 @@ from sys import (exit)
from .macros import macros, pre_class # noqa: F401
from .gs import (GS)
from .optionable import Optionable
-from .kiplot import check_eeschema_do, exec_with_retry
+from .kiplot import check_eeschema_do, exec_with_retry, load_sch
from .error import (KiPlotConfigurationError)
from .misc import (CMD_EESCHEMA_DO, ERC_ERROR)
from .log import (get_logger)
@@ -28,6 +28,9 @@ class Run_ERC(BasePreFlight): # noqa: F821
def run(self):
check_eeschema_do()
+ # The schematic is loaded only before executing an output related to it.
+ # But here we need data from it.
+ load_sch()
output = Optionable.expand_filename_sch(None, GS.out_dir, GS.def_global_output, 'erc', 'txt')
logger.debug('ERC report: '+output)
cmd = [CMD_EESCHEMA_DO, 'run_erc', '-o', output]
diff --git a/tests/board_samples/kicad_5/RLC_sort.sch b/tests/board_samples/kicad_5/RLC_sort.sch
index a4c3d852..9e45fb47 100644
--- a/tests/board_samples/kicad_5/RLC_sort.sch
+++ b/tests/board_samples/kicad_5/RLC_sort.sch
@@ -87,7 +87,7 @@ U 1 1 5E6A448B
P 2200 3650
F 0 "R9" V 2280 3650 50 0000 C CNN
F 1 "100R" V 2200 3650 50 0000 C CNN
-F 2 "Resistor_SMD:R_0603_1608Metric" V 2130 3650 50 0001 C CNN
+F 2 "Resistor_SMD:R_0805_2012Metric" V 2130 3650 50 0001 C CNN
F 3 "~" H 2200 3650 50 0001 C CNN
1 2200 3650
1 0 0 -1
@@ -98,7 +98,7 @@ U 1 1 5E6A491A
P 2500 3650
F 0 "R10" V 2580 3650 50 0000 C CNN
F 1 "100" V 2500 3650 50 0000 C CNN
-F 2 "Resistor_SMD:R_0603_1608Metric" V 2430 3650 50 0001 C CNN
+F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 3650 50 0001 C CNN
F 3 "~" H 2500 3650 50 0001 C CNN
1 2500 3650
1 0 0 -1
diff --git a/tests/test_plot/test_preflight.py b/tests/test_plot/test_preflight.py
index 19327876..5d59c596 100644
--- a/tests/test_plot/test_preflight.py
+++ b/tests/test_plot/test_preflight.py
@@ -23,7 +23,7 @@ if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context
-from kibot.misc import (DRC_ERROR, ERC_ERROR, BOM_ERROR, CORRUPTED_PCB)
+from kibot.misc import (DRC_ERROR, ERC_ERROR, BOM_ERROR, CORRUPTED_PCB, CORRUPTED_SCH)
def test_erc_1():
@@ -49,7 +49,7 @@ def test_erc_fail_2():
""" Using a dummy SCH """
prj = '3Rs'
ctx = context.TestContext('ERCFail2', prj, 'erc', '')
- ctx.run(ERC_ERROR)
+ ctx.run(CORRUPTED_SCH)
ctx.clean_up()
diff --git a/tests/test_plot/test_print_sch.py b/tests/test_plot/test_print_sch.py
index e2909590..be7fa971 100644
--- a/tests/test_plot/test_print_sch.py
+++ b/tests/test_plot/test_print_sch.py
@@ -67,7 +67,7 @@ def check_l1(ctx):
ctx.expect_out_file(o_name)
sch = Schematic()
try:
- sch.load(ctx.get_out_path(o_name))
+ sch.load(ctx.get_out_path(o_name), 'no_project')
except SchFileError as e:
logging.error('At line {} of `{}`: {}'.format(e.line, e.file, e.msg))
logging.error('Line content: `{}`'.format(e.code))