Skip to content

Commit f2b92bc

Browse files
committed
[BoM][Added] JSON format
See #917
1 parent 9e1372b commit f2b92bc

12 files changed

Lines changed: 412 additions & 99 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- CLI:
1111
- --keep-temporals: to keep temporal files
12+
- BoM:
13+
- JSON output format. As a helper for external renderers (See #917)
1214
- Diff:
1315
- `tag_filter`: to select which tags are used for KIBOT_TAG
16+
- KiCad Site:
17+
- `bom` configuration
1418

1519

1620
## [1.9.0] - 2026-05-12

docs/samples/generic_plot.kibot.yaml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ outputs:
614614
footprint_populate_values: 'no,yes'
615615
# [string|list(string)='SMD,THT,VIRTUAL'] {comma_sep} {L:3} Values for the `Footprint Type` column
616616
footprint_type_values: 'SMD,THT,VIRTUAL'
617-
# [string='Auto'] [HTML,CSV,TXT,TSV,XML,XLSX,HRTXT,KICAD,Auto] format for the BoM.
617+
# [string='Auto'] [HTML,CSV,TXT,TSV,XML,XLSX,HRTXT,KICAD,JSON,Auto] format for the BoM.
618618
# `Auto` defaults to CSV or a guess according to the options.
619619
# HRTXT stands for Human Readable TeXT.
620620
# KICAD is used to get the options from KiCad project. In KiCad you can configure CSV like options
@@ -642,7 +642,7 @@ outputs:
642642
group_fields_fallbacks: []
643643
# [boolean=false] Enable it to group fitted and not fitted components together. This is how KiCad's internal BoM behaves
644644
group_not_fitted: false
645-
# [dict={}] Options for the HRTXT formats
645+
# [dict={}] Options for the HRTXT format
646646
hrtxt:
647647
# [string='-'] Separator between the header and the data
648648
header_sep: '-'
@@ -705,6 +705,30 @@ outputs:
705705
ignore_dnf: true
706706
# [boolean=true] Component quantities are always expressed as integers. Using the ceil() function
707707
int_qtys: true
708+
# [dict={}] Options for the JSON format
709+
json:
710+
# [string=''] {no_case} Column with links to the datasheet
711+
datasheet_as_link: ''
712+
# [string|list(string)=''] {no_case} Column/s containing Digi-Key part numbers, will be linked to web page
713+
digikey_link: ''
714+
# [string|list(string)=''] Information to put after the title and before the pcb and stats info
715+
extra_info: ''
716+
# [boolean=true] Generate a separated section for DNF (Do Not Fit) components
717+
generate_dnf: true
718+
# [boolean=true] Use a color for empty cells. Applies only when `col_colors` is `true`
719+
highlight_empty: true
720+
# [boolean|string|list(string)=''] {no_case} Column/s containing LCSC part numbers, will be linked to web page.
721+
# Use **true** to copy the value indicated by the `field_lcsc_part` global option
722+
lcsc_link: ''
723+
# [string|boolean=''] PNG/SVG file to use as logo, use false to remove.
724+
# Note that when using an SVG this is first converted to a PNG using `logo_width`
725+
logo: ''
726+
# [number=370] Used when the logo is an SVG image. This width is used to render the SVG image
727+
logo_width: 370
728+
# [string|list(string)=''] {no_case} Column/s containing Mouser part numbers, will be linked to web page
729+
mouser_link: ''
730+
# [string='KiBot Bill of Materials'] BoM title
731+
title: 'KiBot Bill of Materials'
708732
# [string='global'] [global,yes,no] What we do with the KiCad DNP flag.
709733
# `global` means we apply the `kicad_dnp_applied` global option.
710734
# `yes` means we always remove DNP components.
@@ -2262,6 +2286,8 @@ outputs:
22622286
# [string=''] Base URL for the generated site. I.e. https://USER.github.io/PROJECT/
22632287
# Without the version part
22642288
base_url: ''
2289+
# [string=''] Name of the BoM output to use as embedded HTML. Use `None` to skip
2290+
bom: ''
22652291
# [string='static'] Subdirectory where the files will be copied inside the destination `dir`
22662292
dest_subdir: 'static'
22672293
# [dict|list(dict)=[]] Diff resources, usually one for the PCB and another for the schematic

docs/source/Changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,18 @@ Added
2323

2424
- –keep-temporals: to keep temporal files
2525

26+
- BoM:
27+
28+
- JSON output format. As a helper for external renderers (See #917)
29+
2630
- Diff:
2731

2832
- ``tag_filter``: to select which tags are used for KIBOT_TAG
2933

34+
- KiCad Site:
35+
36+
- ``bom`` configuration
37+
3038
[1.9.0] - 2026-05-12
3139
--------------------
3240

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.. _BoMLinkableSimple:
2+
3+
:orphan:
4+
5+
6+
BoMLinkableSimple parameters
7+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8+
9+
- **datasheet_as_link** :index:`: <pair: output - bom - options - json; datasheet_as_link>` [:ref:`string <string>`] (default: ``''``) [:ref:`case insensitive <no_case>`]Column with links to the datasheet.
10+
- **generate_dnf** :index:`: <pair: output - bom - options - json; generate_dnf>` [:ref:`boolean <boolean>`] (default: ``true``) Generate a separated section for DNF (Do Not Fit) components.
11+
- **logo** :index:`: <pair: output - bom - options - json; logo>` [:ref:`string <string>` | :ref:`boolean <boolean>`] (default: ``''``) PNG/SVG file to use as logo, use false to remove.
12+
Note that when using an SVG this is first converted to a PNG using `logo_width`.
13+
14+
- **title** :index:`: <pair: output - bom - options - json; title>` [:ref:`string <string>`] (default: ``'KiBot Bill of Materials'``) BoM title.
15+
- ``digikey_link`` :index:`: <pair: output - bom - options - json; digikey_link>` [:ref:`string <string>` | :ref:`list(string) <list(string)>`] (default: ``''``) [:ref:`case insensitive <no_case>`]Column/s containing Digi-Key part numbers, will be linked to web page.
16+
17+
- ``extra_info`` :index:`: <pair: output - bom - options - json; extra_info>` [:ref:`string <string>` | :ref:`list(string) <list(string)>`] (default: ``''``) Information to put after the title and before the pcb and stats info.
18+
19+
- ``highlight_empty`` :index:`: <pair: output - bom - options - json; highlight_empty>` [:ref:`boolean <boolean>`] (default: ``true``) Use a color for empty cells. Applies only when `col_colors` is `true`.
20+
- ``lcsc_link`` :index:`: <pair: output - bom - options - json; lcsc_link>` [:ref:`boolean <boolean>` | :ref:`string <string>` | :ref:`list(string) <list(string)>`] (default: ``''``) [:ref:`case insensitive <no_case>`]Column/s containing LCSC part numbers, will be linked to web page.
21+
Use **true** to copy the value indicated by the `field_lcsc_part` global option.
22+
23+
- ``logo_width`` :index:`: <pair: output - bom - options - json; logo_width>` [:ref:`number <number>`] (default: ``370``) Used when the logo is an SVG image. This width is used to render the SVG image.
24+
- ``mouser_link`` :index:`: <pair: output - bom - options - json; mouser_link>` [:ref:`string <string>` | :ref:`list(string) <list(string)>`] (default: ``''``) [:ref:`case insensitive <no_case>`]Column/s containing Mouser part numbers, will be linked to web page.
25+
26+

docs/source/configuration/outputs/BoMOptions.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ BoMOptions parameters
1212
this will be replaced by the list from KiCad. |br|
1313
In addition to all user defined fields you have various special columns, consult :ref:`bom_columns`.
1414
- **csv** :index:`: <pair: output - bom - options; csv>` [:ref:`BoMCSV parameters <BoMCSV>`] [:ref:`dict <dict>`] (default: empty dict, default values used) Options for the CSV, TXT and TSV formats.
15-
- **format** :index:`: <pair: output - bom - options; format>` [:ref:`string <string>`] (default: ``'Auto'``) (choices: "HTML", "CSV", "TXT", "TSV", "XML", "XLSX", "HRTXT", "KICAD", "Auto") format for the BoM.
15+
- **format** :index:`: <pair: output - bom - options; format>` [:ref:`string <string>`] (default: ``'Auto'``) (choices: "HTML", "CSV", "TXT", "TSV", "XML", "XLSX", "HRTXT", "KICAD", "JSON", "Auto") format for the BoM.
1616
`Auto` defaults to CSV or a guess according to the options. |br|
1717
HRTXT stands for Human Readable TeXT. |br|
1818
KICAD is used to get the options from KiCad project. In KiCad you can configure CSV like options.
@@ -31,9 +31,10 @@ BoMOptions parameters
3131
If empty: ['Part', 'Part Lib', 'Value', 'Footprint', 'Footprint Lib',
3232
'Voltage', 'Tolerance', 'Current', 'Power'] is used.
3333

34-
- **hrtxt** :index:`: <pair: output - bom - options; hrtxt>` [:ref:`BoMTXT parameters <BoMTXT>`] [:ref:`dict <dict>`] (default: empty dict, default values used) Options for the HRTXT formats.
34+
- **hrtxt** :index:`: <pair: output - bom - options; hrtxt>` [:ref:`BoMTXT parameters <BoMTXT>`] [:ref:`dict <dict>`] (default: empty dict, default values used) Options for the HRTXT format.
3535
- **html** :index:`: <pair: output - bom - options; html>` [:ref:`BoMHTML parameters <BoMHTML>`] [:ref:`dict <dict>`] (default: empty dict, default values used) Options for the HTML format.
3636
- **ignore_dnf** :index:`: <pair: output - bom - options; ignore_dnf>` [:ref:`boolean <boolean>`] (default: ``true``) Exclude DNF (Do Not Fit) components.
37+
- **json** :index:`: <pair: output - bom - options; json>` [:ref:`BoMLinkableSimple parameters <BoMLinkableSimple>`] [:ref:`dict <dict>`] (default: empty dict, default values used) Options for the JSON format.
3738
- **normalize_values** :index:`: <pair: output - bom - options; normalize_values>` [:ref:`boolean <boolean>`] (default: ``false``) Try to normalize the R, L and C values, producing uniform units and prefixes.
3839
- **number** :index:`: <pair: output - bom - options; number>` [:ref:`number <number>`] (default: ``1``) Number of boards to build (components multiplier).
3940
- **output** :index:`: <pair: output - bom - options; output>` [:ref:`string <string>`] (default: ``'%f-%i%I%v.%x'``) filename for the output (%i=bom). The extension depends on the selected format.
@@ -166,5 +167,6 @@ Used dicts
166167
- :ref:`BoMCSV parameters <BoMCSV>`
167168
- :ref:`BoMColumns parameters <BoMColumns>`
168169
- :ref:`BoMHTML parameters <BoMHTML>`
170+
- :ref:`BoMLinkableSimple parameters <BoMLinkableSimple>`
169171
- :ref:`BoMTXT parameters <BoMTXT>`
170172
- :ref:`BoMXLSX parameters <BoMXLSX>`

kibot/bom/bom_writer.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright (c) 2020-2024 Salvador E. Tropea
3-
# Copyright (c) 2020-2024 Instituto Nacional de Tecnología Industrial
2+
# Copyright (c) 2020-2026 Salvador E. Tropea
3+
# Copyright (c) 2020-2026 Instituto Nacional de Tecnología Industrial
44
# Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat)
55
# License: MIT
66
# Project: KiBot (formerly KiPlot)
@@ -11,12 +11,14 @@
1111
This is just a hub that calls the real BoM writer:
1212
- csv_writer.py
1313
- html_writer.py
14+
- json_writer.py
1415
- kicad_writer.py
1516
- xml_writer.py
1617
- xlsx_writer.py
1718
"""
1819
from .csv_writer import write_csv
1920
from .html_writer import write_html
21+
from .json_writer import write_json
2022
from .xml_writer import write_xml
2123
from .. import log
2224
from .. import error
@@ -41,6 +43,8 @@ def write_bom(filename, ext, groups, headings, cfg):
4143
result = write_csv(filename, ext, groups, headings, head_names, cfg)
4244
elif ext in ["htm", "html"]:
4345
result = write_html(filename, groups, headings, head_names, cfg)
46+
elif ext == "json":
47+
result = write_json(filename, groups, headings, head_names, cfg)
4448
elif ext == "xml":
4549
result = write_xml(filename, groups, headings, head_names, cfg)
4650
elif ext == "xlsx":

kibot/bom/json_writer.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) 2026 Salvador E. Tropea
3+
# Copyright (c) 2026 Instituto Nacional de Tecnología Industrial
4+
# License: AGPL
5+
# Project: KiBot (formerly KiPlot)
6+
"""
7+
JSON Writer: Generates a JSON BoM file.
8+
"""
9+
import json
10+
import urllib.parse
11+
12+
from .columnlist import ColumnList
13+
from .html_writer import cell_class
14+
from .. import log
15+
logger = log.get_logger()
16+
17+
18+
def write_stats(data, cfg):
19+
multi = len(cfg.aggregate) > 1
20+
stats = {}
21+
stats['variant'] = cfg.variant.name if cfg.variant else 'Default'
22+
stats['kicad_version'] = cfg.kicad_version
23+
stats['component_groups'] = cfg.n_groups
24+
stats['component_count'] = cfg.total_str
25+
stats['fitted_components'] = cfg.fitted_str
26+
stats['number_of_pcbs'] = cfg.number
27+
stats['total_components'] = cfg.n_build
28+
prjs = []
29+
for prj in cfg.aggregate:
30+
d = {}
31+
d['schematic'] = prj.name
32+
d['revision'] = prj.sch.revision
33+
d['date'] = prj.sch.date
34+
if prj.sch.company:
35+
d['company'] = prj.sch.company
36+
if prj.ref_id:
37+
d['id'] = prj.ref_id
38+
if multi:
39+
d['component_groups'] = prj.comp_groups
40+
d['component_count'] = prj.total_str
41+
d['fitted_components'] = prj.fitted_str
42+
d['number_of_pcbs'] = prj.number
43+
d['total_components'] = prj.comp_build
44+
prjs.append(d)
45+
stats['projects'] = prjs
46+
data['stats'] = stats
47+
48+
49+
def write_json(filename, groups, headings, head_names, cfg):
50+
"""
51+
Write BoM out to a JSON file
52+
filename = path to output file (should be a .json)
53+
groups = [list of ComponentGroup groups]
54+
headings = [list of headings to search for data in the BoM file]
55+
head_names = [list of headings to display in the BoM file]
56+
cfg = BoMOptions object with all the configuration
57+
"""
58+
# TODO: Copiar el logo si es que no esta dentro del "output_dir"
59+
# Si esta vacio generar el nuestro y ponerle ese nombre
60+
link_datasheet = -1
61+
if cfg.json.datasheet_as_link and cfg.json.datasheet_as_link in headings:
62+
link_datasheet = headings.index(cfg.json.datasheet_as_link)
63+
link_digikey = cfg.json.digikey_link
64+
link_mouser = cfg.json.mouser_link
65+
link_lcsc = cfg.json.lcsc_link
66+
hl_empty = cfg.json.highlight_empty
67+
68+
data = {'title': cfg.json.title, 'logo': cfg.json.logo, 'logo_width': cfg.json.logo_width}
69+
write_stats(data, cfg)
70+
71+
# Headings and where the data came from
72+
data['headings'] = [{'name': h, 'class': cell_class(f)} for h, f in zip(head_names, headings)]
73+
74+
# User defined strings
75+
if cfg.json.extra_info:
76+
data['extra_info'] = cfg.json.extra_info
77+
78+
# We use this code twice (regular + DNF) so I encapsulated it in a function ... the Pythonic way
79+
def do_groups(dnf=False):
80+
rows = []
81+
for group in groups:
82+
if (cfg.ignore_dnf and not group.is_fitted()) != dnf:
83+
continue
84+
row = group.get_row(headings)
85+
if link_datasheet != -1:
86+
datasheet = group.get_field(ColumnList.COL_DATASHEET_L)
87+
cells = []
88+
for n, r in enumerate(row):
89+
cell = {'value': r}
90+
field = headings[n]
91+
#
92+
# Solve any link
93+
#
94+
# A link to Digi-Key?
95+
if link_digikey and field in link_digikey and r:
96+
cell['link'] = 'https://www.digikey.com/en/products/result?keywords=' + urllib.parse.quote(r)
97+
if link_mouser and field in link_mouser and r:
98+
cell['link'] = 'https://www.mouser.com/ProductDetail/' + r
99+
if link_lcsc and field in link_lcsc and r:
100+
cell['link'] = 'https://www.lcsc.com/product-detail/' + r + '.html'
101+
# Link this column to the datasheet?
102+
if link_datasheet == n and datasheet.startswith('http') and r:
103+
cell['link'] = datasheet
104+
#
105+
# Color hint
106+
#
107+
# Empty cell?
108+
if hl_empty and ((len(r) == 0 and field not in group.fields_with_tilde) or r.strip() == "~"):
109+
cell['empty'] = True
110+
# Add the cell
111+
cells.append(cell)
112+
rows.append(cells)
113+
return rows
114+
# End of helper function "do_groups"
115+
116+
data['rows'] = do_groups()
117+
# DNF component groups
118+
if cfg.json.generate_dnf and cfg.n_total != cfg.n_fitted:
119+
data['rows_dnf'] = do_groups(True)
120+
121+
with open(filename, "wt") as output:
122+
text = json.dumps(data, sort_keys=True, indent=2)
123+
output.write(text)
124+
125+
return True

0 commit comments

Comments
 (0)