2020-09-14

KiCad - Custom BOM Output

After experimenting various methods to create BOM in KiCad, I found the best method is to use the Python script plugins. KiCad has provided a netlist reader (kicad_netlist_reader.py) and several sample scripts. But none of the scripts meet my need. So, I decide to create one for myself.

I have defined several custom fields for symbols. For capacitor, field "Voltage" is a necessary e.g. 16V. But the regular field "Value" should only have capacitance e.g. 10u. It will be better to show "10u 16V" in BOM. If the capacitor is X7R then will be better to show "10u 16V X7R".

Originally only Value and Footprint are checked to see if two components are the same part. But if capacitance is equal but voltage is different then they should be considered as different parts. So, I check Value, Voltage, Current, Power, Tolerance and Type.

The calculation of total cost is also helpful. It is considered, too. Here is the final result:


bom.py:

"""
@package
Generate a comma delimited list (csv file type).
Components are sorted by ref and grouped by value with same footprint.
The value is dynamically composed by Value, Voltage, Current, Power, 
    Tolerance and Type.

Command line: python "bom_rcfocus.py" "%I" "%O.csv"
"""

# Import the KiCad python helper module and the csv formatter
import kicad_netlist_reader
import csv
import sys

# Override the equal-compare method
def myEqu(self, other):
result = False
if self.getValue() == other.getValue():
if self.getField("Voltage") == other.getField("Voltage"):
if self.getField("Current") == other.getField("Current"):
if self.getField("Power") == other.getField("Power"):
if self.getField("Tolerance") == other.getField("Tolerance"):
      if self.getField("Type") == other.getField("Type"):
     if self.getFootprint() == other.getFootprint():
    result = True
return result

kicad_netlist_reader.comp.__eq__ = myEqu

# Generate an instance of a generic netlist, and load the netlist tree from
# the command line option. If the file doesn't exist, execution will stop
net = kicad_netlist_reader.netlist(sys.argv[1])

# Open a file to write to, if the file cannot be opened output to stdout
# instead
try:
f = open(sys.argv[2], 'w')
except IOError:
e = "Can't open output file for writing: " + sys.argv[2]
print(__file__, ":", e, sys.stderr)
f = sys.stdout

# Create a new csv writer object to use as the output formatter
out = csv.writer(f, lineterminator='\n', delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL)

# Header
out.writerow(['Ref', 'Part', 'Value', 'Footprint', 'Description', 'Manufacturer', 'Price', 'Qty', 'Totoal'])

# Get interested components in groups of matching parts + values
# (see ky_generic_netlist_reader.py)
#grouped = net.groupComponents(net.getInterestingComponents())
grouped = net.groupComponents()

# Output
total_qty = 0
amount = 0

for group in grouped:
qty = len(group)

# Add the reference of every component in the group and keep a reference
# to the component so that the other data can be filled in once per group
refs = ""
for i in range(qty):
component = group[i]
refs += component.getRef()
if i < qty - 1:
refs += ", "
c = component

# If field "Installed" is "NU" then skip it.
if c.getField("Installed") == "NU":
continue

# Skip MountHole
if c.getValue() == "MountHole":
continue

# Skip TestPoint
if c.getValue() == "TestPoint":
continue

# If specific part number (PN) is assigned then use it to replace part name.
part = c.getPartName()
if c.getField("PN"):
part = c.getField("PN")

# Value = Value + Voltage + Current + Power + Tolerance + Type
value = c.getValue()
if c.getField("Voltage"):
value += " " + c.getField("Voltage")
if c.getField("Current"):
value += " " + c.getField("Current")
if c.getField("Power"):
value += " " + c.getField("Power")
if c.getField("Tolerance"):
value += " " + c.getField("Tolerance")
if c.getField("Type"):
value += " " + c.getField("Type")

# If Part is equal to Value then don't show Value.
# This will make the final BOM table looks more clear.
if part == value:
value = ""

# Footprint: trim libary name
fp = c.getFootprint()
semi_pos = fp.find(':')
footprint = fp[semi_pos+1:]

# Others
total_qty += qty

try:
t = qty * float(c.getField("Price"))
amount += t
total = "{:.2f}".format(t) # convert to string
except ValueError:
total = ''

out.writerow([refs, part, value, footprint, c.getDescription(),
c.getField("Manufacturer"), c.getField("Price"), qty, total])

# Tailer
out.writerow([''])
out.writerow(['Summary:', '', '', '', '', '', '', total_qty, "{:.1f}".format(amount)])