1#
2# Example python script to generate a BOM from a KiCad generic netlist
3#
4# Example: Sorted and Grouped CSV BOM
5#
6
7"""
8    @package
9    Output: CSV (comma-separated)
10    Grouped By: Value
11    Sorted By: Ref
12    Fields: Item, Qty, Reference(s), Value, LibPart, Footprint, Datasheet, all additional symbol fields
13
14    Outputs ungrouped components first, then outputs grouped components.
15
16    Command line:
17    python "pathToFile/bom_csv_grouped_by_value.py" "%I" "%O.csv"
18"""
19
20from __future__ import print_function
21
22# Import the KiCad python helper module and the csv formatter
23import kicad_netlist_reader
24import kicad_utils
25import csv
26import sys
27
28# A helper function to convert a UTF8/Unicode/locale string read in netlist
29# for python2 or python3 (Windows/unix)
30def fromNetlistText( aText ):
31    currpage = sys.stdout.encoding      #the current code page. can be none
32    if currpage is None:
33        return aText
34    if currpage != 'utf-8':
35        try:
36            return aText.encode('utf-8').decode(currpage)
37        except UnicodeDecodeError:
38            return aText
39    else:
40        return aText
41
42def myEqu(self, other):
43    """myEqu is a more advanced equivalence function for components which is
44    used by component grouping. Normal operation is to group components based
45    on their value and footprint.
46
47    In this example of a custom equivalency operator we compare the
48    value, the part name and the footprint.
49    """
50    result = True
51    if self.getValue() != other.getValue():
52        result = False
53    elif self.getPartName() != other.getPartName():
54        result = False
55    elif self.getFootprint() != other.getFootprint():
56        result = False
57
58    return result
59
60# Override the component equivalence operator - it is important to do this
61# before loading the netlist, otherwise all components will have the original
62# equivalency operator.
63kicad_netlist_reader.comp.__eq__ = myEqu
64
65if len(sys.argv) != 3:
66    print("Usage ", __file__, "<generic_netlist.xml> <output.csv>", file=sys.stderr)
67    sys.exit(1)
68
69
70# Generate an instance of a generic netlist, and load the netlist tree from
71# the command line option. If the file doesn't exist, execution will stop
72net = kicad_netlist_reader.netlist(sys.argv[1])
73
74# Open a file to write to, if the file cannot be opened output to stdout
75# instead
76try:
77    f = kicad_utils.open_file_write(sys.argv[2], 'w')
78except IOError:
79    e = "Can't open output file for writing: " + sys.argv[2]
80    print( __file__, ":", e, sys.stderr )
81    f = sys.stdout
82
83# subset the components to those wanted in the BOM, controlled
84# by <configure> block in kicad_netlist_reader.py
85components = net.getInterestingComponents()
86
87compfields = net.gatherComponentFieldUnion(components)
88partfields = net.gatherLibPartFieldUnion()
89
90# remove Reference, Value, Datasheet, and Footprint, they will come from 'columns' below
91partfields -= set( ['Reference', 'Value', 'Datasheet', 'Footprint'] )
92
93columnset = compfields | partfields     # union
94
95# prepend an initial 'hard coded' list and put the enchillada into list 'columns'
96columns = ['Item', 'Qty', 'Reference(s)', 'Value', 'LibPart', 'Footprint', 'Datasheet'] + sorted(list(columnset))
97
98# Create a new csv writer object to use as the output formatter
99out = csv.writer( f, lineterminator='\n', delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL )
100
101# override csv.writer's writerow() to support encoding conversion (initial encoding is utf8):
102def writerow( acsvwriter, columns ):
103    utf8row = []
104    for col in columns:
105        utf8row.append( fromNetlistText( str(col) ) )
106    acsvwriter.writerow( utf8row )
107
108# Output a set of rows as a header providing general information
109writerow( out, ['Source:', net.getSource()] )
110writerow( out, ['Date:', net.getDate()] )
111writerow( out, ['Tool:', net.getTool()] )
112writerow( out, ['Generator:', sys.argv[0]] )
113writerow( out, ['Component Count:', len(components)] )
114writerow( out, [] )
115writerow( out, ['Individual Components:'] )
116writerow( out, [] )                        # blank line
117writerow( out, columns )
118
119# Output all the interesting components individually first:
120row = []
121for c in components:
122    del row[:]
123    row.append('')                                      # item is blank in individual table
124    row.append('')                                      # Qty is always 1, why print it
125    row.append( c.getRef() )                            # Reference
126    row.append( c.getValue() )                          # Value
127    row.append( c.getLibName() + ":" + c.getPartName() ) # LibPart
128    #row.append( c.getDescription() )
129    row.append( c.getFootprint() )
130    row.append( c.getDatasheet() )
131
132    # from column 7 upwards, use the fieldnames to grab the data
133    for field in columns[7:]:
134        row.append( c.getField( field ) );
135
136    writerow( out, row )
137
138
139writerow( out, [] )                        # blank line
140writerow( out, [] )                        # blank line
141writerow( out, [] )                        # blank line
142
143writerow( out, ['Collated Components:'] )
144writerow( out, [] )                        # blank line
145writerow( out, columns )                   # reuse same columns
146
147
148
149# Get all of the components in groups of matching parts + values
150# (see kicad_netlist_reader.py)
151grouped = net.groupComponents(components)
152
153
154# Output component information organized by group, aka as collated:
155item = 0
156for group in grouped:
157    del row[:]
158    refs = ""
159
160    # Add the reference of every component in the group and keep a reference
161    # to the component so that the other data can be filled in once per group
162    for component in group:
163        if len(refs) > 0:
164            refs += ", "
165        refs += component.getRef()
166        c = component
167
168    # Fill in the component groups common data
169    # columns = ['Item', 'Qty', 'Reference(s)', 'Value', 'LibPart', 'Footprint', 'Datasheet'] + sorted(list(columnset))
170    item += 1
171    row.append( item )
172    row.append( len(group) )
173    row.append( refs );
174    row.append( c.getValue() )
175    row.append( c.getLibName() + ":" + c.getPartName() )
176    row.append( net.getGroupFootprint(group) )
177    row.append( net.getGroupDatasheet(group) )
178
179    # from column 7 upwards, use the fieldnames to grab the data
180    for field in columns[7:]:
181        row.append( net.getGroupField(group, field) );
182
183    writerow( out, row  )
184
185f.close()
186