Browse Source

add python BOM générator for kicad

remy 9 years ago
parent
commit
dd1f778e9c

+ 764 - 0
BOM/kicad_netlist_reader.py

@@ -0,0 +1,764 @@
+#
+# KiCad python module for interpreting generic netlists which can be used
+# to generate Bills of materials, etc.
+#
+# Remember these files use UTF8 encoding
+#
+# No string formatting is used on purpose as the only string formatting that
+# is current compatible with python 2.4+ to 3.0+ is the '%' method, and that
+# is due to be deprecated in 3.0+ soon
+#
+
+"""
+    @package
+    Helper module for interpreting generic netlist and build custom
+    bom generators or netlists in foreign format
+"""
+
+
+from __future__ import print_function
+import sys
+import xml.sax as sax
+import re
+import pdb
+
+#-----<Configure>----------------------------------------------------------------
+
+# excluded_fields is a list of regular expressions.  If any one matches a field
+# from either a component or a libpart, then that will not be included as a
+# column in the BOM.  Otherwise all columns from all used libparts and components
+# will be unionized and will appear.  Some fields are impossible to blacklist, such
+# as Ref, Value, Footprint, and Datasheet.  Additionally Qty and Item are supplied
+# unconditionally as columns, and may not be removed.
+excluded_fields = [
+    #'Price@1000'
+    ]
+
+
+# You may exlude components from the BOM by either:
+#
+# 1) adding a custom field named "Installed" to your components and filling it
+# with a value of "NU" (Normally Uninstalled).
+# See netlist.getInterestingComponents(), or
+#
+# 2) blacklisting it in any of the three following lists:
+
+
+# regular expressions which match component 'Reference' fields of components that
+# are to be excluded from the BOM.
+excluded_references = [
+    'TP[0-9]+'              # all test points
+    ]
+
+
+# regular expressions which match component 'Value' fields of components that
+# are to be excluded from the BOM.
+excluded_values = [
+    'MOUNTHOLE',
+    'SCOPETEST',
+    'MOUNT_HOLE',
+    'SOLDER_BRIDGE.*'
+    ]
+
+
+# regular expressions which match component 'Footprint' fields of components that
+# are to be excluded from the BOM.
+excluded_footprints = [
+    #'MOUNTHOLE'
+    ]
+
+#-----</Configure>---------------------------------------------------------------
+
+
+class xmlElement():
+    """xml element which can represent all nodes of the netlist tree.  It can be
+    used to easily generate various output formats by propogating format
+    requests to children recursively.
+    """
+    def __init__(self, name, parent=None):
+        self.name = name
+        self.attributes = {}
+        self.parent = parent
+        self.chars = ""
+        self.children = []
+
+    def __str__(self):
+        """String representation of this netlist element
+
+        """
+        return self.name + "[" + self.chars + "]" + " attr_count:" + str(len(self.attributes))
+
+    def formatXML(self, nestLevel=0, amChild=False):
+        """Return this element formatted as XML
+
+        Keywords:
+        nestLevel -- increases by one for each level of nesting.
+        amChild -- If set to True, the start of document is not returned.
+
+        """
+        s = ""
+
+        indent = ""
+        for i in range(nestLevel):
+            indent += "    "
+
+        if not amChild:
+            s = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+
+        s += indent + "<" + self.name
+        for a in self.attributes:
+            s += " " + a + "=\"" + self.attributes[a] + "\""
+
+        if (len(self.chars) == 0) and (len(self.children) == 0):
+            s += "/>"
+        else:
+            s += ">" + self.chars
+
+        for c in self.children:
+            s += "\n"
+            s += c.formatXML(nestLevel+1, True)
+
+        if (len(self.children) > 0):
+            s += "\n" + indent
+
+        if (len(self.children) > 0) or (len(self.chars) > 0):
+            s += "</" + self.name + ">"
+
+        return s
+
+    def formatHTML(self, amChild=False):
+        """Return this element formatted as HTML
+
+        Keywords:
+        amChild -- If set to True, the start of document is not returned
+
+        """
+        s = ""
+
+        if not amChild:
+            s = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+                "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+                <html xmlns="http://www.w3.org/1999/xhtml">
+                <head>
+                <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+                <title></title>
+                </head>
+                <body>
+                <table>
+                """
+
+        s += "<tr><td><b>" + self.name + "</b><br>" + self.chars + "</td><td><ul>"
+        for a in self.attributes:
+            s += "<li>" + a + " = " + self.attributes[a] + "</li>"
+
+        s += "</ul></td></tr>\n"
+
+        for c in self.children:
+            s += c.formatHTML(True)
+
+        if not amChild:
+            s += """</table>
+                </body>
+                </html>"""
+
+        return s
+
+    def addAttribute(self, attr, value):
+        """Add an attribute to this element"""
+        if type(value) != str: value = value.encode('utf-8')
+        self.attributes[attr] = value
+
+    def setAttribute(self, attr, value):
+        """Set an attributes value - in fact does the same thing as add
+        attribute
+
+        """
+        self.attributes[attr] = value
+
+    def setChars(self, chars):
+        """Set the characters for this element"""
+        self.chars = chars
+
+    def addChars(self, chars):
+        """Add characters (textual value) to this element"""
+        self.chars += chars
+
+    def addChild(self, child):
+        """Add a child element to this element"""
+        self.children.append(child)
+        return self.children[len(self.children) - 1]
+
+    def getParent(self):
+        """Get the parent of this element (Could be None)"""
+        return self.parent
+
+    def getChild(self, name):
+        """Returns the first child element named 'name'
+
+        Keywords:
+        name -- The name of the child element to return"""
+        for child in self.children:
+            if child.name == name:
+                return child
+        return None
+
+    def getChildren(self, name=None):
+        if name:
+            # return _all_ children named "name"
+            ret = []
+            for child in self.children:
+                if child.name == name:
+                    ret.append(child)
+            return ret
+        else:
+            return self.children
+
+    def get(self, elemName, attribute="", attrmatch=""):
+        """Return the text data for either an attribute or an xmlElement
+        """
+        if (self.name == elemName):
+            if attribute != "":
+                try:
+                    if attrmatch != "":
+                        if self.attributes[attribute] == attrmatch:
+                            ret = self.chars
+                            if type(ret) != str: ret = ret.encode('utf-8')
+                            return ret
+                    else:
+                        ret = self.attributes[attribute]
+                        if type(ret) != str: ret = ret.encode('utf-8')
+                        return ret
+                except AttributeError:
+                    ret = ""
+                    if type(ret) != str: ret = ret.encode('utf-8')
+                    return ret
+            else:
+                ret = self.chars
+                if type(ret) != str: ret = ret.encode('utf-8')
+                return ret
+
+        for child in self.children:
+            ret = child.get(elemName, attribute, attrmatch)
+            if ret != "":
+                if type(ret) != str: ret = ret.encode('utf-8')
+                return ret
+
+        ret = ""
+        if type(ret) != str: ret = ret.encode('utf-8')
+        return ret
+
+
+
+class libpart():
+    """Class for a library part, aka 'libpart' in the xml netlist file.
+    (Components in eeschema are instantiated from library parts.)
+    This part class is implemented by wrapping an xmlElement with accessors.
+    This xmlElement instance is held in field 'element'.
+    """
+    def __init__(self, xml_element):
+        #
+        self.element = xml_element
+
+    #def __str__(self):
+        # simply print the xmlElement associated with this part
+        #return str(self.element)
+
+    def getLibName(self):
+        return self.element.get("libpart", "lib")
+
+    def getPartName(self):
+        return self.element.get("libpart", "part")
+
+    def getDescription(self):
+        return self.element.get("description")
+
+    def getField(self, name):
+        return self.element.get("field", "name", name)
+
+    def getFieldNames(self):
+        """Return a list of field names in play for this libpart.
+        """
+        fieldNames = []
+        fields = self.element.getChild('fields')
+        if fields:
+            for f in fields.getChildren():
+                fieldNames.append( f.get('field','name') )
+        return fieldNames
+
+    def getDatasheet(self):
+        return self.getField("Datasheet")
+
+    def getFootprint(self):
+        return self.getField("Footprint")
+
+    def getAliases(self):
+        """Return a list of aliases or None"""
+        aliases = self.element.getChild("aliases")
+        if aliases:
+            ret = []
+            children = aliases.getChildren()
+            # grab the text out of each child:
+            for child in children:
+                ret.append( child.get("alias") )
+            return ret
+        return None
+
+
+class comp():
+    """Class for a component, aka 'comp' in the xml netlist file.
+    This component class is implemented by wrapping an xmlElement instance
+    with accessors.  The xmlElement is held in field 'element'.
+    """
+
+    def __init__(self, xml_element):
+        self.element = xml_element
+        self.libpart = None
+
+        # Set to true when this component is included in a component group
+        self.grouped = False
+
+    def __eq__(self, other):
+        """ Equivalency operator, remember this can be easily overloaded
+            2 components are equivalent ( i.e. can be grouped
+            if they have same value and same footprint
+
+            Override the component equivalence operator must be done before
+            loading the netlist, otherwise all components will have the original
+            equivalency operator.
+
+            You have to define a comparison module (for instance named myEqu)
+            and add the line;
+                kicad_netlist_reader.comp.__eq__ = myEqu
+            in your bom generator script before calling the netliste reader by something like:
+                net = kicad_netlist_reader.netlist(sys.argv[1])
+        """
+        result = False
+        if self.getValue() == other.getValue():
+            if self.getFootprint() == other.getFootprint():
+                result = True
+        return result
+
+    def setLibPart(self, part):
+        self.libpart = part
+
+    def getLibPart(self):
+        return self.libpart
+
+    def getPartName(self):
+        return self.element.get("libsource", "part")
+
+    def getLibName(self):
+        return self.element.get("libsource", "lib")
+
+    def setValue(self, value):
+        """Set the value of this component"""
+        v = self.element.getChild("value")
+        if v:
+            v.setChars(value)
+
+    def getValue(self):
+        return self.element.get("value")
+
+    def getField(self, name, libraryToo=True):
+        """Return the value of a field named name. The component is first
+        checked for the field, and then the components library part is checked
+        for the field. If the field doesn't exist in either, an empty string is
+        returned
+
+        Keywords:
+        name -- The name of the field to return the value for
+        libraryToo --   look in the libpart's fields for the same name if not found
+                        in component itself
+        """
+
+        field = self.element.get("field", "name", name)
+        if field == "" and libraryToo:
+            field = self.libpart.getField(name)
+        return field
+
+    def getFieldNames(self):
+        """Return a list of field names in play for this component.  Mandatory
+        fields are not included, and they are: Value, Footprint, Datasheet, Ref.
+        The netlist format only includes fields with non-empty values.  So if a field
+        is empty, it will not be present in the returned list.
+        """
+        fieldNames = []
+        fields = self.element.getChild('fields')
+        if fields:
+            for f in fields.getChildren():
+                fieldNames.append( f.get('field','name') )
+        return fieldNames
+
+    def getRef(self):
+        return self.element.get("comp", "ref")
+
+    def getFootprint(self, libraryToo=True):
+        ret = self.element.get("footprint")
+        if ret =="" and libraryToo:
+            ret = self.libpart.getFootprint()
+        return ret
+
+    def getDatasheet(self, libraryToo=True):
+        ret = self.element.get("datasheet")
+        if ret == '' and libraryToo:
+            ret = self.libpart.getDatasheet()
+        return ret
+
+    def getTimestamp(self):
+        return self.element.get("tstamp")
+
+    def getDescription(self):
+        return self.libpart.getDescription()
+
+
+class netlist():
+    """ Kicad generic netlist class. Generally loaded from a kicad generic
+    netlist file. Includes several helper functions to ease BOM creating
+    scripts
+
+    """
+    def __init__(self, fname=""):
+        """Initialiser for the genericNetlist class
+
+        Keywords:
+        fname -- The name of the generic netlist file to open (Optional)
+
+        """
+        self.design = None
+        self.components = []
+        self.libparts = []
+        self.libraries = []
+        self.nets = []
+
+        # The entire tree is loaded into self.tree
+        self.tree = []
+
+        self._curr_element = None
+
+        # component blacklist regexs, made from exluded_* above.
+        self.excluded_references = []
+        self.excluded_values = []
+        self.excluded_footprints = []
+
+        if fname != "":
+            self.load(fname)
+
+    def addChars(self, content):
+        """Add characters to the current element"""
+        self._curr_element.addChars(content)
+
+    def addElement(self, name):
+        """Add a new kicad generic element to the list"""
+        if self._curr_element == None:
+            self.tree = xmlElement(name)
+            self._curr_element = self.tree
+        else:
+            self._curr_element = self._curr_element.addChild(
+                xmlElement(name, self._curr_element))
+
+        # If this element is a component, add it to the components list
+        if self._curr_element.name == "comp":
+            self.components.append(comp(self._curr_element))
+
+        # Assign the design element
+        if self._curr_element.name == "design":
+            self.design = self._curr_element
+
+        # If this element is a library part, add it to the parts list
+        if self._curr_element.name == "libpart":
+            self.libparts.append(libpart(self._curr_element))
+
+        # If this element is a net, add it to the nets list
+        if self._curr_element.name == "net":
+            self.nets.append(self._curr_element)
+
+        # If this element is a library, add it to the libraries list
+        if self._curr_element.name == "library":
+            self.libraries.append(self._curr_element)
+
+        return self._curr_element
+
+    def endDocument(self):
+        """Called when the netlist document has been fully parsed"""
+        # When the document is complete, the library parts must be linked to
+        # the components as they are seperate in the tree so as not to
+        # duplicate library part information for every component
+        for c in self.components:
+            for p in self.libparts:
+                if p.getLibName() == c.getLibName():
+                    if p.getPartName() == c.getPartName():
+                        c.setLibPart(p)
+                        break
+                    else:
+                        aliases = p.getAliases()
+                        if aliases and self.aliasMatch( c.getPartName(), aliases ):
+                            c.setLibPart(p)
+                            break;
+
+            if not c.getLibPart():
+                print( 'missing libpart for ref:', c.getRef(), c.getPartName(), c.getLibName() )
+
+
+    def aliasMatch(self, partName, aliasList):
+        for alias in aliasList:
+            if partName == alias:
+                return True
+        return False
+
+    def endElement(self):
+        """End the current element and switch to its parent"""
+        self._curr_element = self._curr_element.getParent()
+
+    def getDate(self):
+        """Return the date + time string generated by the tree creation tool"""
+        return self.design.get("date")
+
+    def getSource(self):
+        """Return the source string for the design"""
+        return self.design.get("source")
+
+    def getTool(self):
+        """Return the tool string which was used to create the netlist tree"""
+        return self.design.get("tool")
+
+    def gatherComponentFieldUnion(self, components=None):
+        """Gather the complete 'set' of unique component fields, fields found in any component.
+        """
+        if not components:
+            components=self.components
+
+        s = set()
+        for c in components:
+            s.update( c.getFieldNames() )
+
+        # omit anything matching any regex in excluded_fields
+        ret = set()
+        for field in s:
+            exclude = False
+            for rex in excluded_fields:
+                if re.match( rex, field ):
+                    exclude = True
+                    break
+            if not exclude:
+                ret.add(field)
+
+        return ret       # this is a python 'set'
+
+    def gatherLibPartFieldUnion(self):
+        """Gather the complete 'set' of part fields, fields found in any part.
+        """
+        s = set()
+        for p in self.libparts:
+            s.update( p.getFieldNames() )
+
+        # omit anything matching any regex in excluded_fields
+        ret = set()
+        for field in s:
+            exclude = False
+            for rex in excluded_fields:
+                if re.match( rex, field ):
+                    exclude = True
+                    break
+            if not exclude:
+                ret.add(field)
+
+        return ret       # this is a python 'set'
+
+    def getInterestingComponents(self):
+        """Return a subset of all components, those that should show up in the BOM.
+        Omit those that should not, by consulting the blacklists:
+        excluded_values, excluded_refs, and excluded_footprints, which hold one
+        or more regular expressions.  If any of the the regular expressions match
+        the corresponding field's value in a component, then the component is exluded.
+        """
+
+        # pre-compile all the regex expressions:
+        del self.excluded_references[:]
+        del self.excluded_values[:]
+        del self.excluded_footprints[:]
+
+        for rex in excluded_references:
+            self.excluded_references.append( re.compile( rex ) )
+
+        for rex in excluded_values:
+            self.excluded_values.append( re.compile( rex ) )
+
+        for rex in excluded_footprints:
+            self.excluded_footprints.append( re.compile( rex ) )
+
+        # the subset of components to return, considered as "interesting".
+        ret = []
+
+        # run each component thru a series of tests, if it passes all, then add it
+        # to the interesting list 'ret'.
+        for c in self.components:
+            exclude = False
+            if not exclude:
+                for refs in self.excluded_references:
+                    if refs.match(c.getRef()):
+                        exclude = True
+                        break;
+            if not exclude:
+                for vals in self.excluded_values:
+                    if vals.match(c.getValue()):
+                        exclude = True
+                        break;
+            if not exclude:
+                for mods in self.excluded_footprints:
+                    if mods.match(c.getFootprint()):
+                        exclude = True
+                        break;
+
+            if not exclude:
+                # This is a fairly personal way to flag DNS (Do Not Stuff).  NU for
+                # me means Normally Uninstalled.  You can 'or in' another expression here.
+                if c.getField( "Installed" ) == 'NU':
+                    exclude = True
+
+            if not exclude:
+                ret.append(c)
+
+        # Sort first by ref as this makes for easier to read BOM's
+        def f(v):
+            return re.sub(r'([A-z]+)[0-9]+', r'\1', v) + '%08i' % int(re.sub(r'[A-z]+([0-9]+)', r'\1', v))
+        ret.sort(key=lambda g: f(g.getRef()))
+
+        return ret
+
+
+    def groupComponents(self, components = None):
+        """Return a list of component lists. Components are grouped together
+        when the value, library and part identifiers match.
+
+        Keywords:
+        components -- is a list of components, typically an interesting subset
+        of all components, or None.  If None, then all components are looked at.
+        """
+        if not components:
+            components = self.components
+
+        groups = []
+
+        # Make sure to start off will all components ungrouped to begin with
+        for c in components:
+            c.grouped = False
+
+        # Group components based on the value, library and part identifiers
+        for c in components:
+            if c.grouped == False:
+                c.grouped = True
+                newgroup = []
+                newgroup.append(c)
+
+                # Check every other ungrouped component against this component
+                # and add to the group as necessary
+                for ci in components:
+                    if ci.grouped == False and ci == c:
+                        newgroup.append(ci)
+                        ci.grouped = True
+
+                # Add the new component group to the groups list
+                groups.append(newgroup)
+
+        # Each group is a list of components, we need to sort each list first
+        # to get them in order as this makes for easier to read BOM's
+        def f(v):
+            return re.sub(r'([A-z]+)[0-9]+', r'\1', v) + '%08i' % int(re.sub(r'[A-z]+([0-9]+)', r'\1', v))
+        for g in groups:
+            g = sorted(g, key=lambda g: f(g.getRef()))
+
+        # Finally, sort the groups to order the references alphabetically
+        groups = sorted(groups, key=lambda group: f(group[0].getRef()))
+
+        return groups
+
+    def getGroupField(self, group, field):
+        """Return the whatever is known about the given field by consulting each
+        component in the group.  If any of them know something about the property/field,
+        then return that first non-blank value.
+        """
+        for c in group:
+            ret = c.getField(field, False)
+            if ret != '':
+                return ret
+        return group[0].getLibPart().getField(field)
+
+    def getGroupFootprint(self, group):
+        """Return the whatever is known about the Footprint by consulting each
+        component in the group.  If any of them know something about the Footprint,
+        then return that first non-blank value.
+        """
+        for c in group:
+            ret = c.getFootprint()
+            if ret != "":
+                return ret
+        return group[0].getLibPart().getFootprint()
+
+    def getGroupDatasheet(self, group):
+        """Return the whatever is known about the Datasheet by consulting each
+        component in the group.  If any of them know something about the Datasheet,
+        then return that first non-blank value.
+        """
+        for c in group:
+            ret = c.getDatasheet()
+            if ret != "":
+                return ret
+
+        if len(group) > 0:
+            return group[0].getLibPart().getDatasheet()
+        else:
+            print("NULL!")
+        return ''
+
+    def formatXML(self):
+        """Return the whole netlist formatted in XML"""
+        return self.tree.formatXML()
+
+    def formatHTML(self):
+        """Return the whole netlist formatted in HTML"""
+        return self.tree.formatHTML()
+
+    def load(self, fname):
+        """Load a kicad generic netlist
+
+        Keywords:
+        fname -- The name of the generic netlist file to open
+
+        """
+        try:
+            self._reader = sax.make_parser()
+            self._reader.setContentHandler(_gNetReader(self))
+            self._reader.parse(fname)
+        except IOError as e:
+            print( __file__, ":", e, file=sys.stderr )
+            sys.exit(-1)
+
+
+
+class _gNetReader(sax.handler.ContentHandler):
+    """SAX kicad generic netlist content handler - passes most of the work back
+    to the 'netlist' class which builds a complete tree in RAM for the design
+
+    """
+    def __init__(self, aParent):
+        self.parent = aParent
+
+    def startElement(self, name, attrs):
+        """Start of a new XML element event"""
+        element = self.parent.addElement(name)
+
+        for name in attrs.getNames():
+            element.addAttribute(name, attrs.getValue(name))
+
+    def endElement(self, name):
+        self.parent.endElement()
+
+    def characters(self, content):
+        # Ignore erroneous white space - ignoreableWhitespace does not get rid
+        # of the need for this!
+        if not content.isspace():
+            self.parent.addChars(content)
+
+    def endDocument(self):
+        """End of the XML document event"""
+        self.parent.endDocument()

BIN
BOM/kicad_netlist_reader.pyc


+ 175 - 0
BOM/net.py

@@ -0,0 +1,175 @@
+#
+# Example python script to generate a BOM from a KiCad generic netlist
+#
+# Example: Sorted and Grouped CSV BOM
+#
+"""
+    @package
+    Generate a csv BOM list.
+    Components are sorted by ref and grouped by value
+    Fields are (if exist)
+    Item, Qty, Reference(s), Value, LibPart, Footprint, Datasheet
+"""
+
+from __future__ import print_function
+
+# Import the KiCad python helper module and the csv formatter
+import kicad_netlist_reader
+import csv
+import sys
+
+def myEqu(self, other):
+    """myEqu is a more advanced equivalence function for components which is
+    used by component grouping. Normal operation is to group components based
+    on their value and footprint.
+
+    In this example of a custom equivalency operator we compare the
+    value, the part name and the footprint.
+
+    """
+    result = True
+    if self.getValue() != other.getValue():
+        result = False
+    elif self.getPartName() != other.getPartName():
+        result = False
+    elif self.getFootprint() != other.getFootprint():
+        result = False
+
+    return result
+
+# Override the component equivalence operator - it is important to do this
+# before loading the netlist, otherwise all components will have the original
+# equivalency operator.
+kicad_netlist_reader.comp.__eq__ = myEqu
+
+if len(sys.argv) != 3:
+    print("Usage ", __file__, "<generic_netlist.xml> <output.csv>", file=sys.stderr)
+    sys.exit(1)
+
+
+# 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
+
+# subset the components to those wanted in the BOM, controlled
+# by <configure> block in kicad_netlist_reader.py
+components = net.getInterestingComponents()
+
+compfields = net.gatherComponentFieldUnion(components)
+partfields = net.gatherLibPartFieldUnion()
+
+# remove Reference, Value, Datasheet, and Footprint, they will come from 'columns' below
+partfields -= set( ['Reference', 'Value', 'Datasheet', 'Footprint'] )
+
+columnset = compfields | partfields     # union
+
+# prepend an initial 'hard coded' list and put the enchillada into list 'columns'
+columnsIndividuals = ['Reference(s)', 'Value', 'Footprint'] + sorted(list(columnset))
+columnsCollected = ['Item', 'Qty', 'Reference(s)', 'Value', 'Footprint'] + sorted(list(columnset))
+# Create a new csv writer object to use as the output formatter
+out = csv.writer( f, lineterminator='\n', delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL )
+
+# override csv.writer's writerow() to support encoding conversion (initial encoding is utf8):
+def writerow( acsvwriter, columns ):
+    utf8row = []
+    for col in columns:
+        utf8row.append( str(col) )  # currently, no change
+    acsvwriter.writerow( utf8row )
+
+# Output a set of rows as a header providing general information
+#~ writerow( out, ['Source:', net.getSource()] )
+#~ writerow( out, ['Date:', net.getDate()] )
+#~ writerow( out, ['Tool:', net.getTool()] )
+#~ writerow( out, ['Generator:', sys.argv[0]] )
+writerow( out, ['Component Count:', len(components)] )
+writerow( out, [] )
+writerow( out, ['Individual Components:'] )
+writerow( out, [] )                        # blank line
+writerow( out, columnsIndividuals )
+
+# Output all the interesting components individually first:
+row = []
+for c in components:
+    del row[:]
+    #~ row.append('')                                      # item is blank in individual table
+    #~ row.append('')                                      # Qty is always 1, why print it
+    row.append( c.getRef() )                            # Reference
+    row.append( c.getValue() )                          # Value
+    #~ row.append( c.getLibName() + ":" + c.getPartName() ) # LibPart
+    #row.append( c.getDescription() )
+    row.append( c.getFootprint() )
+    #~ row.append( c.getDatasheet() )
+
+    # from column 7 upwards, use the fieldnames to grab the data
+    for field in columnsIndividuals[3:]:
+		val = c.getField( field )
+		if val == "Value":
+			row.append('');
+		else :
+			row.append(val); 
+        
+
+    writerow( out, row )
+
+
+writerow( out, [] )                        # blank line
+writerow( out, [] )                        # blank line
+writerow( out, [] )                        # blank line
+
+writerow( out, ['Collated Components:'] )
+writerow( out, [] )                        # blank line
+writerow( out, columnsCollected )                   # reuse same columns
+
+
+
+# Get all of the components in groups of matching parts + values
+# (see kicad_netlist_reader.py)
+grouped = net.groupComponents(components)
+
+
+# Output component information organized by group, aka as collated:
+item = 0
+for group in grouped:
+    del row[:]
+    refs = ""
+
+    # 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
+    for component in group:
+        if len(refs) > 0:
+            refs += ", "
+        refs += component.getRef()
+        c = component
+
+    # Fill in the component groups common data
+    # columns = ['Item', 'Qty', 'Reference(s)', 'Value', 'LibPart', 'Footprint', 'Datasheet'] + sorted(list(columnset))
+    item += 1
+    row.append( item )
+    row.append( len(group) )
+    row.append( refs );
+    row.append( c.getValue() )
+    #~ row.append( c.getLibName() + ":" + c.getPartName() )
+    row.append( net.getGroupFootprint(group) )
+    #~ row.append( net.getGroupDatasheet(group) )
+
+    # from column 7 upwards, use the fieldnames to grab the data
+    for field in columnsCollected[5:]:
+		val = net.getGroupField(group, field)
+		if val == "Value":
+			row.append('');
+		else :
+			row.append(val);
+
+
+    writerow( out, row  )
+
+f.close()

+ 0 - 14
footprint/Dialight.pretty/598-xxxx-107F.kicad_mod

@@ -1,14 +0,0 @@
-(module 0603 (layer F.Cu) (tedit 57EA34D7)
-  (fp_text reference REF** (at 0 -0.635) (layer F.SilkS)
-    (effects (font (size 0.3 0.3) (thickness 0.075)))
-  )
-  (fp_text value 0603 (at 0 0.635) (layer F.Fab)
-    (effects (font (size 0.25 0.25) (thickness 0.0625)))
-  )
-  (fp_line (start -0.254 0) (end 0.254 0.381) (layer F.SilkS) (width 0.15))
-  (fp_line (start -0.254 0) (end 0.254 -0.381) (layer F.SilkS) (width 0.15))
-  (fp_line (start 0.254 0.381) (end 0.254 -0.381) (layer F.SilkS) (width 0.15))
-  (fp_line (start -0.254 -0.381) (end -0.254 0.381) (layer F.SilkS) (width 0.15))
-  (pad 1 smd rect (at -0.575 0) (size 0.45 0.7) (layers F.Cu F.Paste F.Mask))
-  (pad 2 smd rect (at 0.575 0) (size 0.45 0.7) (layers F.Cu F.Paste F.Mask))
-)