kicad-bom-gen

A collection of scripts to generate BOMs in various formats from KiCad's Eeschema (Schematic Editor).

"""
    @package
Generate BOM's using Jinja2 templates.
    Output: Template Specific (Markdown, AsciiDoc, plaintext, etc.)
    Fields: Template Specific
    Sort By: -g, --group [group_by] (default 'value')

    Command Line:
    python bom-gen.py "%I" -o "%O<.ext>" -t template<.ext> [-g [value]]

    Included Templates:
      * table.txt - Generate a formatted plain text table.
      * github.md - Generate a github flavoured Markdown table.
      * checklist.adoc - Generate a assembly BOM checklist in asciidoc format.
          Can be used to generate a PDF with asciidoctor-pdf.
"""

import os
import argparse
import xml.etree.ElementTree as ET
from jinja2 import Template


# Parse arguments
parser = argparse.ArgumentParser()
# Paths to needed files
parser.add_argument('infile', help='Path to KiCad netlist xml file to parse.')
parser.add_argument('-o','--output', dest='outfile', help='Path to output rendered BOM. Includes extension.')
parser.add_argument('-t','--template', help='Path to template file, or name of installed template.')

# Options
parser.add_argument('-g', '--group', nargs='?', const='value', help="Group components in rows by [group] (default 'value').")
parser.add_argument('--no-clean', action='store_false', default=True, dest='clean', help='Do not delete input xml file')

args = parser.parse_args()

template_vars = {'components': []}

# Parse input .xml file
print("Parsing input file: %s..." % args.infile)
xml_data = ET.parse(args.infile).getroot()

# Extract general information about the project
print("Extracting general info about project...")
design_xml = xml_data.find('design')
sheet1_xml = design_xml.find('sheet').find('title_block')
template_vars['name'] = sheet1_xml.find('title').text
template_vars['version'] = sheet1_xml.find('rev').text
template_vars['time'] = design_xml.find('date').text
template_vars['source'] = sheet1_xml.find('source').text

# Extract component values
print("Extracting component values...")
components = xml_data.find('components')
for component in components.findall('comp'):

    # Every component is required to have a ref and val
    comp_temp = {
        'ref': component.attrib['ref'],
        'value': component.find('value').text
    }

    # Footprint
    comp_temp['footprint'] = component.find('footprint').text

    # Lib details
    comp_lib = component.find('libsource')
    comp_temp['lib'] = comp_lib.attrib['lib']
    comp_temp['part'] = comp_lib.attrib['part']
    comp_temp['description'] = comp_lib.attrib['description']

    # Add all user defined fields
    user_fields = component.find('fields')
    if user_fields:
        for field in user_fields.findall('field'):
            comp_temp[field.attrib['name']] = field.text

    # Check for duplicates
    duplicate = None
    for added_comp in template_vars['components']:
        if args.group in added_comp and args.group in comp_temp \
                and comp_temp[args.group] == added_comp[args.group]:
            duplicate = added_comp
            break
    
    # If the grouping flag was used, and the field was duplicated
    if (args.group and duplicate is not None):
        # Stick that bad boy on the end of the Ref string and hope everything else is the same
        duplicate['ref'] = duplicate['ref'] + ', ' + comp_temp['ref']

    # Create a new BOM entry if none of the same group exist or grouping is off
    else:
        # Add component to list
        template_vars['components'].append(comp_temp)

if args.template:
    # Render template file with extracted values
    print("Rendering template: %s..." % args.template)
    try:
        # Attempt to read file as full path
        with open(args.template, 'r') as template_file:
            bom_template = Template(template_file.read(), trim_blocks=True)
    except:
        # If the file is not found attempt to load it from the templates/ directory
        with open(os.path.abspath(os.path.dirname(__file__))+'/templates/'+args.template, 'r') as template_file:
            bom_template = Template(template_file.read(), trim_blocks=True)

    rendered_bom = bom_template.render(template_vars)
else:
    print("No template defined, outputting raw values...")
    # Don't use a template
    rendered_bom = template_vars

# Remove input file
if args.clean:
    print("Cleaning up build files...")
    os.remove(args.infile)

# Output rendered BOM
if args.outfile:
    print("Writing output file: %s..." % args.outfile)
    with open(args.outfile, 'w') as f:
        f.write(rendered_bom)
    print('Done.')
else:
    print()
    print(rendered_bom)