Merge branch 'master' of https://github.com/SchrodingersGat/KiBoM
This commit is contained in:
commit
7f402f9c1f
|
|
@ -2,4 +2,6 @@ import columns
|
|||
import netlist_reader
|
||||
import units
|
||||
import component
|
||||
import sort
|
||||
import sort
|
||||
import preferences
|
||||
import bom_writer
|
||||
|
|
@ -12,6 +12,14 @@ class ColumnList:
|
|||
|
||||
#default columns for groups
|
||||
COL_GRP_QUANTITY = 'Quantity'
|
||||
COL_GRP_TOTAL_COST = 'Total Cost' #Total cost based on quantity
|
||||
COL_GRP_BUILD_QUANTITY = 'Build Quantity'
|
||||
|
||||
#generated columns
|
||||
_COLUMNS_GEN = [
|
||||
COL_GRP_QUANTITY,
|
||||
COL_GRP_BUILD_QUANTITY,
|
||||
]
|
||||
|
||||
#default columns
|
||||
_COLUMNS_DEFAULT = [
|
||||
|
|
@ -23,6 +31,7 @@ class ColumnList:
|
|||
COL_FP,
|
||||
COL_FP_LIB,
|
||||
COL_GRP_QUANTITY,
|
||||
COL_GRP_BUILD_QUANTITY,
|
||||
COL_DATASHEET
|
||||
]
|
||||
|
||||
|
|
@ -71,7 +80,7 @@ class ColumnList:
|
|||
def RemoveColumnByName(self, name):
|
||||
|
||||
#first check if this is in an immutable colum
|
||||
if name in self._COLUMNS_DEFAULT:
|
||||
if name in self._COLUMNS_PROTECTED:
|
||||
return
|
||||
|
||||
#column does not exist, return
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ class ComponentGroup():
|
|||
def getField(self, field):
|
||||
if not field in self.fields.keys(): return ""
|
||||
if not self.fields[field]: return ""
|
||||
return str(self.fields[field])
|
||||
return u''.join((self.fields[field]))
|
||||
|
||||
def getCount(self):
|
||||
return len(self.components)
|
||||
|
|
@ -255,9 +255,12 @@ class ComponentGroup():
|
|||
|
||||
#update 'global' fields
|
||||
self.fields[ColumnList.COL_REFERENCE] = self.getRefs()
|
||||
|
||||
q = self.getCount()
|
||||
self.fields[ColumnList.COL_GRP_QUANTITY] = "{n}{dnf}".format(
|
||||
n = self.getCount(),
|
||||
n=q,
|
||||
dnf = " (DNF)" if not self.isFitted() else "")
|
||||
self.fields[ColumnList.COL_GRP_BUILD_QUANTITY] = str(q * self.prefs.buildNumber) if self.isFitted() else "0"
|
||||
self.fields[ColumnList.COL_VALUE] = self.components[0].getValue()
|
||||
self.fields[ColumnList.COL_PART] = self.components[0].getPartName()
|
||||
self.fields[ColumnList.COL_PART_LIB] = self.components[0].getLibName()
|
||||
|
|
@ -303,7 +306,18 @@ class ComponentGroup():
|
|||
|
||||
|
||||
#return a dict of the KiCAD data based on the supplied columns
|
||||
#NOW WITH UNICODE SUPPORT!
|
||||
def getRow(self, columns):
|
||||
row = [self.getField(key) for key in columns]
|
||||
#print(row)
|
||||
row = []
|
||||
for key in columns:
|
||||
val = self.getField(key)
|
||||
|
||||
if val is None:
|
||||
val = ""
|
||||
else:
|
||||
val = u'' + val
|
||||
val = val.encode('utf-8')
|
||||
|
||||
row.append(val)
|
||||
|
||||
return row
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
# _*_ coding:latin-1 _*_
|
||||
|
||||
import csv
|
||||
import columns
|
||||
from component import *
|
||||
|
|
@ -25,6 +27,11 @@ def WriteCSV(filename, groups, net, headings, prefs):
|
|||
else:
|
||||
return False
|
||||
|
||||
nGroups = len(groups)
|
||||
nTotal = sum([g.getCount() for g in groups])
|
||||
nFitted = sum([g.getCount() for g in groups if g.isFitted()])
|
||||
nBuild = nFitted * prefs.buildNumber
|
||||
|
||||
with open(filename, "w") as f:
|
||||
|
||||
writer = csv.writer(f, delimiter=delimiter, lineterminator="\n")
|
||||
|
|
@ -43,8 +50,10 @@ def WriteCSV(filename, groups, net, headings, prefs):
|
|||
row = group.getRow(headings)
|
||||
|
||||
if prefs.numberRows:
|
||||
row = [rowCount] + row
|
||||
row = [str(rowCount)] + row
|
||||
|
||||
#deal with unicode characters
|
||||
#row = [el.decode('latin-1') for el in row]
|
||||
writer.writerow(row)
|
||||
|
||||
try:
|
||||
|
|
@ -58,8 +67,12 @@ def WriteCSV(filename, groups, net, headings, prefs):
|
|||
for i in range(5):
|
||||
writer.writerow([])
|
||||
|
||||
writer.writerow(["Component Count:",sum([g.getCount() for g in groups])])
|
||||
writer.writerow(["Component Groups:",len(groups)])
|
||||
writer.writerow(["Component Groups:",nGroups])
|
||||
writer.writerow(["Component Count:",nTotal])
|
||||
writer.writerow(["Fitted Components:", nFitted])
|
||||
if prefs.buildNumber > 0:
|
||||
writer.writerow(["Number of PCBs:",prefs.buildNumber])
|
||||
writer.writerow(["Total components:", nBuild])
|
||||
writer.writerow(["Schematic Version:",net.getVersion()])
|
||||
writer.writerow(["Schematic Date:",net.getSheetDate()])
|
||||
writer.writerow(["BoM Date:",net.getDate()])
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
import columns
|
||||
from component import *
|
||||
import os
|
||||
|
|
@ -9,7 +10,7 @@ BG_USER = "#E6F9FF"
|
|||
#return a background color for a given column title
|
||||
def bgColor(col):
|
||||
#auto-generated columns
|
||||
if col == ColumnList.COL_GRP_QUANTITY:
|
||||
if col in ColumnList._COLUMNS_GEN:
|
||||
return BG_GEN
|
||||
#kicad protected columns
|
||||
elif col in ColumnList._COLUMNS_PROTECTED:
|
||||
|
|
@ -19,7 +20,7 @@ def bgColor(col):
|
|||
return BG_USER
|
||||
|
||||
def link(text):
|
||||
text = str(text)
|
||||
|
||||
for t in ["http","https","ftp","www"]:
|
||||
if text.startswith(t):
|
||||
return '<a href="{t}">{t}</a>'.format(t=text)
|
||||
|
|
@ -41,12 +42,21 @@ def WriteHTML(filename, groups, net, headings, prefs):
|
|||
print("{fn} is not a valid html file".format(fn=filename))
|
||||
return False
|
||||
|
||||
nGroups = len(groups)
|
||||
nTotal = sum([g.getCount() for g in groups])
|
||||
nFitted = sum([g.getCount() for g in groups if g.isFitted()])
|
||||
nBuild = nFitted * prefs.buildNumber
|
||||
|
||||
with open(filename,"w") as html:
|
||||
|
||||
#header
|
||||
html.write("<html>\n")
|
||||
html.write("<head>\n")
|
||||
html.write('\t<meta charset="ISO-8859-1">\n') #UTF-8 encoding for unicode support
|
||||
html.write("</head>\n")
|
||||
html.write("<body>\n")
|
||||
|
||||
|
||||
#PCB info
|
||||
html.write("<h2>KiBoM PCB Bill of Materials</h2>\n")
|
||||
html.write('<table border="1">\n')
|
||||
|
|
@ -55,8 +65,12 @@ def WriteHTML(filename, groups, net, headings, prefs):
|
|||
html.write("<tr><td>Schematic Version</td><td>{version}</td></tr>\n".format(version=net.getVersion()))
|
||||
html.write("<tr><td>Schematic Date</td><td>{date}</td></tr>\n".format(date=net.getSheetDate()))
|
||||
html.write("<tr><td>KiCad Version</td><td>{version}</td></tr>\n".format(version=net.getTool()))
|
||||
html.write("<tr><td>Total Components</td><td>{n}</td></tr>\n".format(n = sum([g.getCount() for g in groups])))
|
||||
html.write("<tr><td>Component Groups</td><td>{n}</td></tr>\n".format(n=len(groups)))
|
||||
html.write("<tr><td>Component Groups</td><td>{n}</td></tr>\n".format(n=nGroups))
|
||||
html.write("<tr><td>Component Count (per PCB)</td><td>{n}</td></tr>\n".format(n=nTotal))
|
||||
html.write("<tr><td>Fitted Components (per PCB)</td><td>{n}</td></tr>\n".format(n=nFitted))
|
||||
if prefs.buildNumber > 0:
|
||||
html.write("<tr><td>Number of PCBs</td><td>{n}</td></tr>\n".format(n=prefs.buildNumber))
|
||||
html.write("<tr><td>Total Component Count<br>(for {n} PCBs)</td><td>{t}</td></tr>\n".format(n=prefs.buildNumber, t=nBuild))
|
||||
html.write("</table>\n")
|
||||
html.write("<br>\n")
|
||||
html.write("<h2>Component Groups</h2>\n")
|
||||
|
|
@ -93,7 +107,7 @@ def WriteHTML(filename, groups, net, headings, prefs):
|
|||
html.write("<tr>\n")
|
||||
|
||||
if prefs.numberRows:
|
||||
html.write('\t<td align="center">{n}</td>'.format(n=rowCount))
|
||||
html.write('\t<td align="center">{n}</td>\n'.format(n=rowCount))
|
||||
|
||||
for n, r in enumerate(row):
|
||||
bg = bgColor(headings[n])
|
||||
|
|
|
|||
|
|
@ -18,12 +18,15 @@ class BomPref:
|
|||
SECTION_EXCLUDE_PART = "EXCLUDE_COMPONENT_PART"
|
||||
SECTION_EXCLUDE_DESC = "EXCLUDE_COMPONENT_DESC"
|
||||
SECTION_ALIASES = "COMPONENT_ALIASES"
|
||||
SECTION_CONFIGURATIONS = "PCB_CONFIGURATIONS"
|
||||
|
||||
OPT_IGNORE_DNF = "ignore_dnf"
|
||||
OPT_NUMBER_ROWS = "number_rows"
|
||||
OPT_GROUP_CONN = "group_connectors"
|
||||
OPT_USE_REGEX = "test_regex"
|
||||
OPT_COMP_FP = "compare_footprints"
|
||||
OPT_INC_PRICE = "calculate_price"
|
||||
OPT_BUILD_NUMBER = 'build_quantity'
|
||||
|
||||
#list of columns which we can use regex on
|
||||
COL_REG_EX = [
|
||||
|
|
@ -46,6 +49,9 @@ class BomPref:
|
|||
self.groupConnectors = True #group connectors and ignore component value
|
||||
self.useRegex = True #Test various columns with regex
|
||||
self.compareFootprints = True #test footprints when comparing components
|
||||
self.buildNumber = 0
|
||||
self.verbose = False #by default, is not verbose
|
||||
self.configurations = [] #list of various configurations
|
||||
|
||||
#default reference exclusions
|
||||
self.excluded_references = [
|
||||
|
|
@ -71,7 +77,9 @@ class BomPref:
|
|||
["c", "c_small", "cap", "capacitor"],
|
||||
["r", "r_small", "res", "resistor"],
|
||||
["sw", "switch"],
|
||||
["l", "l_small", "inductor"]
|
||||
["l", "l_small", "inductor"],
|
||||
["zener","zenersmall"],
|
||||
["d","diode","d_small"]
|
||||
]
|
||||
|
||||
#dictionary of possible regex expressions for ignoring component row(s)
|
||||
|
|
@ -97,6 +105,13 @@ class BomPref:
|
|||
def columnToGroup(self, col):
|
||||
return "REGEXCLUDE_" + col.upper().replace(" ","_")
|
||||
|
||||
#check an option within the SECTION_GENERAL group
|
||||
def checkOption(self, parser, opt, default=False):
|
||||
if parser.has_option(self.SECTION_GENERAL, opt):
|
||||
return parser.get(self.SECTION_GENERAL, opt).lower() in ["1","true","yes"]
|
||||
else:
|
||||
return default
|
||||
|
||||
#read KiBOM preferences from file
|
||||
def Read(self, file, verbose=False):
|
||||
file = os.path.abspath(file)
|
||||
|
|
@ -111,16 +126,23 @@ class BomPref:
|
|||
|
||||
#read general options
|
||||
if self.SECTION_GENERAL in cf.sections():
|
||||
if cf.has_option(self.SECTION_GENERAL, self.OPT_IGNORE_DNF):
|
||||
self.ignoreDNF = cf.get(self.SECTION_GENERAL, self.OPT_IGNORE_DNF) == "1"
|
||||
if cf.has_option(self.SECTION_GENERAL, self.OPT_NUMBER_ROWS):
|
||||
self.numberRows = cf.get(self.SECTION_GENERAL, self.OPT_NUMBER_ROWS) == "1"
|
||||
if cf.has_option(self.SECTION_GENERAL, self.OPT_GROUP_CONN):
|
||||
self.groupConnectors = cf.get(self.SECTION_GENERAL, self.OPT_GROUP_CONN) == "1"
|
||||
if cf.has_option(self.SECTION_GENERAL, self.OPT_USE_REGEX):
|
||||
self.useRegex = cf.get(self.SECTION_GENERAL, self.OPT_USE_REGEX) == "1"
|
||||
if cf.has_option(self.SECTION_GENERAL, self.OPT_COMP_FP):
|
||||
self.compareFootprints = cf.get(self.SECTION_GENERAL, self.OPT_COMP_FP) == "1"
|
||||
self.ignoreDNF = self.checkOption(cf, self.OPT_IGNORE_DNF, default=True)
|
||||
self.numberRows = self.checkOption(cf, self.OPT_NUMBER_ROWS, default=True)
|
||||
self.groupConnectors = self.checkOption(cf, self.OPT_GROUP_CONN, default=True)
|
||||
self.useRegex = self.checkOption(cf, self.OPT_USE_REGEX, default=True)
|
||||
self.compareFootprints = self.checkOption(cf, self.OPT_COMP_FP, default=True)
|
||||
|
||||
if cf.has_option(self.SECTION_GENERAL, self.OPT_BUILD_NUMBER):
|
||||
try:
|
||||
self.buildNumber = int(cf.get(self.SECTION_GENERAL, self.OPT_BUILD_NUMBER))
|
||||
if self.buildNumber < 1:
|
||||
self.buildNumber = 0
|
||||
except:
|
||||
pass
|
||||
|
||||
#read out configurations
|
||||
if self.SECTION_CONFIGURATIONS in cf.sections():
|
||||
self.configurations = [i for i in cf.options(self.SECTION_CONFIGURATIONS)]
|
||||
|
||||
#read out ignored-rows
|
||||
if self.SECTION_IGNORE in cf.sections():
|
||||
|
|
@ -136,7 +158,14 @@ class BomPref:
|
|||
if section in cf.sections():
|
||||
self.regex[key] = [r for r in cf.options(section)]
|
||||
|
||||
|
||||
|
||||
#add an option to the SECTION_GENRAL group
|
||||
def addOption(self, parser, opt, value, comment=None):
|
||||
if comment:
|
||||
if not comment.startswith(";"):
|
||||
comment = "; " + comment
|
||||
parser.set(self.SECTION_GENERAL, comment)
|
||||
parser.set(self.SECTION_GENERAL, opt, "1" if value else "0")
|
||||
|
||||
#write KiBOM preferences to file
|
||||
def Write(self, file):
|
||||
|
|
@ -146,16 +175,12 @@ class BomPref:
|
|||
|
||||
cf.add_section(self.SECTION_GENERAL)
|
||||
cf.set(self.SECTION_GENERAL, "; General BoM options here")
|
||||
cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, rows that are not to be fitted on the PCB will not be written to the BoM file".format(opt=self.OPT_IGNORE_DNF))
|
||||
cf.set(self.SECTION_GENERAL, self.OPT_IGNORE_DNF, 1 if self.ignoreDNF else 0)
|
||||
cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, each row in the BoM will be prepended with an incrementing row number".format(opt=self.OPT_NUMBER_ROWS))
|
||||
cf.set(self.SECTION_GENERAL, self.OPT_NUMBER_ROWS, 1 if self.numberRows else 0)
|
||||
cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector".format(opt=self.OPT_GROUP_CONN))
|
||||
cf.set(self.SECTION_GENERAL, self.OPT_GROUP_CONN, 1 if self.groupConnectors else 0)
|
||||
cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, each component group will be tested against a number of regular-expressions (specified, per column, below). If any matches are found, the row is ignored in the output file".format(opt=self.OPT_USE_REGEX))
|
||||
cf.set(self.SECTION_GENERAL, self.OPT_USE_REGEX, 1 if self.useRegex else 0)
|
||||
cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, two components must have the same footprint to be grouped together. If '{opt}' is not set, then footprint comparison is ignored.".format(opt=self.OPT_COMP_FP))
|
||||
cf.set(self.SECTION_GENERAL, self.OPT_COMP_FP, 1 if self.compareFootprints else 0)
|
||||
self.addOption(cf, self.OPT_IGNORE_DNF, self.ignoreDNF, comment="If '{opt}' option is set to 1, rows that are not to be fitted on the PCB will not be written to the BoM file".format(opt=self.OPT_IGNORE_DNF))
|
||||
self.addOption(cf, self.OPT_NUMBER_ROWS, self.numberRows, comment="If '{opt}' option is set to 1, each row in the BoM will be prepended with an incrementing row number".format(opt=self.OPT_NUMBER_ROWS))
|
||||
self.addOption(cf, self.OPT_GROUP_CONN, self.groupConnectors, comment="If '{opt}' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector".format(opt=self.OPT_GROUP_CONN))
|
||||
self.addOption(cf, self.OPT_USE_REGEX, self.useRegex, comment="If '{opt}' option is set to 1, each component group will be tested against a number of regular-expressions (specified, per column, below). If any matches are found, the row is ignored in the output file".format(opt=self.OPT_USE_REGEX))
|
||||
self.addOption(cf, self.OPT_COMP_FP, self.compareFootprints, comment="If '{opt}' option is set to 1, two components must have the same footprint to be grouped together. If '{opt}' is not set, then footprint comparison is ignored.".format(opt=self.OPT_COMP_FP))
|
||||
self.addOption(cf, self.OPT_BUILD_NUMBER, self.buildNumber, comment="; '{opt}' is the number of boards to build, which is used to calculate total parts quantity. If this is set to zero (0) then it is ignored".format(opt=self.OPT_BUILD_NUMBER))
|
||||
|
||||
cf.add_section(self.SECTION_IGNORE)
|
||||
cf.set(self.SECTION_IGNORE, "; Any column heading that appears here will be excluded from the Generated BoM")
|
||||
|
|
@ -164,6 +189,11 @@ class BomPref:
|
|||
for i in self.ignore:
|
||||
cf.set(self.SECTION_IGNORE, i)
|
||||
|
||||
cf.add_section(self.SECTION_CONFIGURATIONS)
|
||||
cf.set(self.SECTION_CONFIGURATION, '; List of PCB configuration parameters')
|
||||
for i in self.configurations:
|
||||
cf.set(self.SECTION_CONFIGURATIONS, i)
|
||||
|
||||
cf.add_section(self.SECTION_ALIASES)
|
||||
cf.set(self.SECTION_ALIASES, "; A series of values which are considered to be equivalent for the part name")
|
||||
cf.set(self.SECTION_ALIASES, "; Each line represents a space-separated list of equivalent component name values")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# _*_ coding:utf-8 _*_
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -18,19 +18,32 @@ def WriteXML(filename, groups, net, headings, prefs):
|
|||
if not filename.endswith(".xml"):
|
||||
return False
|
||||
|
||||
xml = ElementTree.Element('KiCAD_BOM', attrib = {
|
||||
'Schematic_Source' : net.getSource(),
|
||||
'Schematic_Version' : net.getVersion(),
|
||||
'Schematic_Date' : net.getSheetDate(),
|
||||
'BOM_Date' : net.getDate(),
|
||||
'KiCad_Version' : net.getTool(),
|
||||
'groups' : str(len(groups)),
|
||||
'components' : str(sum([group.getCount() for group in groups]))
|
||||
})
|
||||
nGroups = len(groups)
|
||||
nTotal = sum([g.getCount() for g in groups])
|
||||
nFitted = sum([g.getCount() for g in groups if g.isFitted()])
|
||||
nBuild = nFitted * prefs.buildNumber
|
||||
|
||||
attrib = {}
|
||||
|
||||
attrib['Schematic_Source'] = net.getSource()
|
||||
attrib['Schematic_Version'] = net.getVersion()
|
||||
attrib['Schematic_Date'] = net.getSheetDate()
|
||||
attrib['BOM_Date'] = net.getDate()
|
||||
attrib['KiCad_Version'] = net.getTool()
|
||||
attrib['Component_Groups'] = str(nGroups)
|
||||
attrib['Component_Count'] = str(nTotal)
|
||||
attrib['Fitted_Components'] = str(nFitted)
|
||||
|
||||
if prefs.buildNumber > 0:
|
||||
attrib['Number_of_PCBs'] = str(prefs.buildNumber)
|
||||
attrib['Total_Components'] = str(nBuild)
|
||||
|
||||
xml = ElementTree.Element('KiCAD_BOM', attrib = attrib, encoding='utf-8')
|
||||
|
||||
for group in groups:
|
||||
if prefs.ignoreDNF and not group.isFitted():
|
||||
continue
|
||||
|
||||
row = group.getRow(headings)
|
||||
|
||||
attrib = {}
|
||||
|
|
@ -40,13 +53,12 @@ def WriteXML(filename, groups, net, headings, prefs):
|
|||
h = h.replace('"','')
|
||||
h = h.replace("'",'')
|
||||
|
||||
attrib[h] = row[i]
|
||||
attrib[h] = str(row[i]).decode('ascii',errors='ignore')
|
||||
|
||||
sub = ElementTree.SubElement(xml, "group", attrib=attrib)
|
||||
|
||||
with open(filename,"w") as output:
|
||||
out = ElementTree.tostring(xml, 'utf-8')
|
||||
|
||||
out = ElementTree.tostring(xml, encoding="utf-8")
|
||||
output.write(minidom.parseString(out).toprettyxml(indent="\t"))
|
||||
|
||||
return True
|
||||
|
|
|
|||
98
KiBOM_CLI.py
98
KiBOM_CLI.py
|
|
@ -6,67 +6,91 @@ import sys
|
|||
import os
|
||||
import shutil
|
||||
|
||||
import argparse
|
||||
|
||||
here = os.path.abspath(os.path.dirname(sys.argv[0]))
|
||||
|
||||
sys.path.append(here)
|
||||
sys.path.append(os.path.join(here,"KiBOM"))
|
||||
|
||||
from KiBOM.columns import ColumnList
|
||||
from KiBOM.netlist_reader import *
|
||||
from KiBOM.bom_writer import *
|
||||
from KiBOM.preferences import BomPref
|
||||
|
||||
verbose = False
|
||||
|
||||
def close(*arg):
|
||||
print(*arg)
|
||||
sys.exit(0)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
close("No input file supplied")
|
||||
def say(*arg):
|
||||
if verbose:
|
||||
print(*arg)
|
||||
|
||||
input_file = sys.argv[1].replace("\\",os.path.sep).replace("/",os.path.sep)
|
||||
parser = argparse.ArgumentParser(description="KiBOM Bill of Materials generator script")
|
||||
|
||||
input_file = os.path.abspath(input_file)
|
||||
parser.add_argument("netlist", help='xml netlist file. Use "%%I" when running from within KiCad')
|
||||
parser.add_argument("--output", "-o", help='BoM output file name.\nUse "%%O" when running from within KiCad to use the default output name (csv file).\nFor e.g. HTML output, use "%%O.html"\r\nIf output file is unspecified, default output filename (csv format) will be used', default=None)
|
||||
|
||||
parser.add_argument("-v", "--verbose", help="Enable verbose output", action='count')
|
||||
parser.add_argument("--cfg", help="BoM config file (script will try to use 'bom.ini' if not specified here)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
input_file = args.netlist
|
||||
|
||||
if not input_file.endswith(".xml"):
|
||||
close("Supplied file is not .xml")
|
||||
|
||||
#work out an output file
|
||||
ext = ".csv"
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
#no output file supplied, assume .csv
|
||||
output_file = input_file.replace(".xml",".csv")
|
||||
else:
|
||||
output_file = sys.argv[2].replace("\\",os.path.sep).replace("/",os.path.sep)
|
||||
close("{i} is not a valid xml file".format(i=input_file))
|
||||
|
||||
valid = False
|
||||
verbose = args.verbose is not None
|
||||
|
||||
for e in [".xml",".csv",".txt",".tsv",".html"]:
|
||||
if output_file.endswith(e):
|
||||
valid = True
|
||||
ext = e
|
||||
break
|
||||
if not valid:
|
||||
output_file += ext
|
||||
|
||||
output_file = os.path.abspath(output_file)
|
||||
|
||||
print("Input File: " + input_file)
|
||||
print("Output File: " + output_file)
|
||||
input_file = os.path.abspath(input_file)
|
||||
|
||||
#preferences
|
||||
ignore = []
|
||||
ignoreDNF = False
|
||||
numberRows = True
|
||||
say("Input:",input_file)
|
||||
|
||||
#Look for a '.bom' preference file
|
||||
pref_file = os.path.join(os.path.dirname(input_file) , "bom.ini")
|
||||
output_file = args.output
|
||||
|
||||
if output_file is None:
|
||||
output_file = input_file.replace(".xml","_bom.csv")
|
||||
|
||||
#enfore a proper extension
|
||||
valid = False
|
||||
extensions = [".xml",".csv",".txt",".tsv",".html"]
|
||||
for e in extensions:
|
||||
if output_file.endswith(e):
|
||||
valid = True
|
||||
break
|
||||
if not valid:
|
||||
close("Extension must be one of",extensions)
|
||||
|
||||
output_file = os.path.abspath(output_file)
|
||||
|
||||
say("Output:",output_file)
|
||||
|
||||
#look for a config file!
|
||||
#bom.ini by default
|
||||
ini = os.path.abspath(os.path.join(os.path.dirname(input_file), "bom.ini"))
|
||||
|
||||
config_file = ini #default value
|
||||
#user can overwrite with a specific config file
|
||||
if args.cfg:
|
||||
config_file = args.cfg
|
||||
|
||||
#read preferences from file. If file does not exists, default preferences will be used
|
||||
pref = BomPref()
|
||||
pref.Read(pref_file)
|
||||
|
||||
#verbosity options
|
||||
pref.verbose = verbose
|
||||
|
||||
if os.path.exists(config_file):
|
||||
pref.Read(config_file)
|
||||
say("Config:",config_file)
|
||||
|
||||
#write preference file back out (first run will generate a file with default preferences)
|
||||
pref.Write(pref_file)
|
||||
if not os.path.exists(ini):
|
||||
pref.Write(ini)
|
||||
say("Writing preferences file bom.ini")
|
||||
|
||||
#individual components
|
||||
components = []
|
||||
|
|
@ -90,6 +114,10 @@ for g in groups:
|
|||
for f in g.fields:
|
||||
columns.AddColumn(f)
|
||||
|
||||
if pref.buildNumber < 1:
|
||||
columns.RemoveColumn(ColumnList.COL_GRP_BUILD_QUANTITY)
|
||||
say("Removing:",ColumnList.COL_GRP_BUILD_QUANTITY)
|
||||
|
||||
#Finally, write the BoM out to file
|
||||
result = WriteBoM(output_file, groups, net, columns.columns, pref)
|
||||
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -9,6 +9,26 @@ KiBoM intelligently groups components based on multiple factors, and can generat
|
|||
|
||||
BoM options are user-configurable in a per-project configuration file.
|
||||
|
||||
## Usage
|
||||
|
||||
The *KiBOM_CLI* script can be run directly from KiCad or from the command line. For command help, run the script with the *-h* flag e.g.
|
||||
|
||||
python KiBOM_CLI.py -h
|
||||
|
||||

|
||||
|
||||
**netlist** The netlist must be provided to the script. When running from KiCAD use "%I"
|
||||
|
||||
**--output / -o** If provided, this is the path to the BoM output. If not provided, the script will use the same name as the input file, with the suffix "_bom.csv"
|
||||
|
||||
**--cfg** If provided, this is the BOM config file that will be used. If not provided, options will be loaded from "bom.ini"
|
||||
|
||||
**--verbose / -v** Enable extra debugging information
|
||||
|
||||
To run from KiCad, simply add the same command line in the *Bill of Materials* script window. e.g. to generate a HTML output:
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
### Intelligent Component Grouping
|
||||
|
|
@ -107,7 +127,9 @@ Hit the "Generate" button, and the output window should show that the BoM genera
|
|||
### HTML Output
|
||||
The output HTML file is generated as follows:
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
Here the components are correctly grouped, with links to datasheets where appropriate, and fields color-coded.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
|
||||
import sys
|
||||
sys.path.append(".")
|
||||
import KiBOM
|
||||
from KiBOM import *
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Loading…
Reference in New Issue