summaryrefslogtreecommitdiff
path: root/kicad_bom_cmd.py
diff options
context:
space:
mode:
authorTrygve Laugstøl <trygvis@inamo.no>2015-09-28 08:27:09 +0200
committerTrygve Laugstøl <trygvis@inamo.no>2015-09-28 08:27:09 +0200
commite30b81a822d56a47e7b6ebb618a746534b9d9992 (patch)
tree0b4cef3e729751f326b468906d7248b2a23f93d9 /kicad_bom_cmd.py
parente91f02df33da10e7f18ac5c582f08134e9bea3c8 (diff)
downloadoctopart-stuff-master.tar.gz
octopart-stuff-master.tar.bz2
octopart-stuff-master.tar.xz
octopart-stuff-master.zip
Diffstat (limited to 'kicad_bom_cmd.py')
-rw-r--r--kicad_bom_cmd.py304
1 files changed, 304 insertions, 0 deletions
diff --git a/kicad_bom_cmd.py b/kicad_bom_cmd.py
new file mode 100644
index 0000000..5c555ff
--- /dev/null
+++ b/kicad_bom_cmd.py
@@ -0,0 +1,304 @@
+import xmltodict
+import sys
+import json
+
+import octopart
+
+# <export>
+# <components>
+# <comp ref="R1">
+# <value>1k</value>
+# <footprint>Resistors_SMD:R_1210_HandSoldering</footprint>
+# <fields>
+# <field name="Farnell">1470030RL</field>
+# </fields>
+# <libsource lib="device" part="R"/>
+# <sheetpath names="/" tstamps="/"/>
+# <tstamp>55FEEA8B</tstamp>
+
+settings = {
+ 'resistor': {
+ 'tolerance': '10',
+ 'power_rating': '0.25'
+ },
+ 'currency': 'NOK'
+}
+
+settings['seller'] = 'farnell'
+
+def apply_settings(search):
+ search['seller'] = settings.get('seller', None)
+
+# Thank you Wikipedia: https://en.wikipedia.org/wiki/Surface-mount_technology#Two-terminal_packages
+def find_case(footprint):
+ footprint = footprint.lower()
+
+ metric = ['0402', '0603', '1005', '1608', '2012', '2520', '3216', '3225', '4516', '4532', '4564', '5025', '6332']
+ imperial = ['01005', '0201', '0402', '0603', '0805', '1008', '1206', '1210', '1806', '1812', '1825', '2010', '2512', '2920']
+
+ for s in metric:
+ if s in footprint:
+ return s
+
+ for s in imperial:
+ if s in footprint:
+ return s
+
+ return None
+
+# TODO: check for power_rating and tolerance
+def resistor(comp):
+ ref = comp['@ref']
+ value = parse_value(comp['value'])
+ footprint = comp.get('footprint', None)
+ tolerance = None or settings['resistor']['tolerance']
+ power_rating = None or settings['resistor']['power_rating']
+
+ case = None
+ if footprint is not None:
+ case = find_case(footprint)
+
+ r = Resistor(value, tolerance, power_rating, case)
+ entry = bom.get_entry_for_component(r)
+ entry.add_reference(ref)
+
+def parse_value(v):
+ try:
+ factor = 1
+ s = v.strip().lower()
+ if s.endswith('k'):
+ s = s[0:-1]
+ factor = 1000
+ elif s.endswith('m'):
+ s = s[0:-1]
+ factor = 1000 * 1000
+
+ return float(s) * factor
+ except ValueError:
+ raise Exception('Bad value: ' + s)
+
+class Resistor(object):
+ def __init__(self, resistance, tolerance, power_rating, case):
+ self.resistance = resistance
+ self.tolerance = tolerance
+ self.power_rating = power_rating
+ self.case = case
+
+ def __eq__(self, other):
+ return type(self) == type(other) and \
+ self.resistance == other.resistance and \
+ self.tolerance == other.tolerance and \
+ self.power_rating == other.power_rating and \
+ self.case == other.case
+
+ def __str__(self):
+ return "Resistor: resistance={}, tolerance={}, power_rating={}, case={}".format(self.resistance, self.tolerance, self.power_rating, self.case)
+
+ def resolve_parts(self):
+ search = octopart.ResistorSearch()
+ search['resistance'] = self.resistance
+ search['power_rating'] = self.power_rating
+ search['tolerance'] = self.tolerance
+ search['case'] = self.case
+ apply_settings(search)
+ return octopart.resistor_search(search)
+
+class GenericComponent(object):
+ def __init__(self, ref, value):
+ self.ref = ref
+ self.value = value
+
+ def __str__(self):
+ return "Generic: ref={}, value={}".format(self.ref, self.value)
+
+ def __eq__(self, other):
+ return type(self) == type(other) and self.ref == other.ref and self.value == other.value
+
+ def resolve_parts(self):
+ # TODO: to a query with the value
+ return octopart.SearchResponse.empty()
+
+class Bom(object):
+ def __init__(self):
+ self.entries = []
+
+ def get_entry_for_component(self, component):
+ for e in self.entries:
+ if e.component == component:
+ return e
+
+ e = BomEntry(component)
+ self.entries.append(e)
+ return e
+
+ def resolve(self):
+ for entry in self.entries:
+ entry.resolve_parts()
+
+class BomOption(object):
+ def __init__(self, part, offer, prices):
+# print "BomOption, part=" + str(part) + ", prices=" + str(prices)
+ self.offer = offer
+ self.sku = offer.sku
+ self.seller = offer.seller
+ assert prices is not None
+ self.prices = prices
+
+ def price_for_quantity(self, quantity):
+ cost = None
+ self.bom_quantity = max(quantity, self.offer.moq)
+
+ for p in self.prices:
+ if self.bom_quantity < p.quantity:
+ continue
+ price = p.amount * self.bom_quantity
+ if cost is None or cost > price:
+ cost = price
+# print "price_for_quantity, SKU:{}, quantity={}, min={}".format(self.offer.sku, quantity, cost)
+ return cost
+
+class BomEntry(object):
+ def __init__(self, component):
+ self.component = component
+ self.references = []
+ self.search_response = None
+ self.options = None
+ self.octopart_mpn = None
+ self.octopart_url = None
+
+ def add_reference(self, reference):
+ self.references.append(reference)
+
+ def quantity(self):
+ return len(self.references)
+
+ def resolve_parts(self):
+ self.search_response = self.component.resolve_parts()
+ self.options = []
+
+ for search_result in self.search_response.results:
+ p = search_result.item
+ self.octopart_mpn = p.mpn
+ self.octopart_url = p.octopart_url
+
+ for offer in p.offers:
+ prices = BomEntry.adjust_prices_to_currency(offer)
+ if prices is None:
+ print 'Could not find prices for offer on SKU {} from {}'.format(offer.sku, offer.seller.name)
+ continue
+ self.options.append(BomOption(p, offer, prices))
+
+# self.total_price = offers[0]
+# self.price_per_part = self.total_price / self.quantity()
+# break
+
+ def __str__(self):
+ return "Bom entry.."
+
+ @staticmethod
+ def adjust_prices_to_currency(offer):
+# print 'adjust_prices_to_currency: SKU:{}, prices={}'.format(offer.sku, offer.prices)
+ prices = offer.prices
+ c = settings['currency'].upper()
+
+# print 'prices: ' + str(prices)
+
+ p = prices.get(c, None)
+
+ if p is not None:
+ print 'NOK, p=' + str(p)
+ return p
+
+ currencies = ['USD', 'EUR']
+
+ rates = octopart.get('http://api.fixer.io/latest?base=' + c)['rates']
+ for c in currencies:
+ p = prices.get(c, None)
+
+ if p is None:
+ continue
+
+ rate = rates.get(c, None)
+
+ if rate is None:
+ # TODO: warn
+ continue
+
+# print 'found, c={}, p={}'.format(c, p)
+
+ return p
+
+ return None
+
+if __name__ == '__main__':
+ bom = Bom()
+
+ with open(sys.argv[1]) as fd:
+ obj = xmltodict.parse(fd.read())
+ comp = json.dumps(obj['export']['components']['comp'])
+ for comp in obj['export']['components']['comp']:
+ ref = comp['@ref']
+ value = comp['value']
+
+ libsource = comp['libsource']
+ lib = libsource['@lib']
+ part = libsource['@part']
+
+ if lib == 'device' and part == 'R':
+ resistor(comp)
+ else:
+ c = GenericComponent(ref, value)
+ entry = bom.get_entry_for_component(c)
+ entry.add_reference(ref)
+
+ do_resolve = False
+ do_resolve = True
+
+ if do_resolve:
+# bom.entries = bom.entries[0:1]
+ bom.resolve()
+
+ print "BOM:"
+ i = 1
+ for entry in bom.entries:
+ print "{0:-3}: {1:5}".format(i, ', '.join(entry.references))
+ print " {}".format(entry.component)
+ i += 1
+
+ if do_resolve:
+ print ''
+ print ''
+ i = 1
+ print "Resolved BOM:"
+ for entry in bom.entries:
+ print "{0:-3}: {1:5}".format(i, ', '.join(entry.references))
+ print " Specification: {}".format(entry.component)
+ print " Octopart MPN: {}, URL: {}".format(entry.octopart_mpn, entry.octopart_url)
+
+ for o in entry.options:
+ quantity = entry.quantity()
+ price = o.price_for_quantity(quantity)
+ if price is None:
+ print " SKU: {0}, price: not available".format(o.sku, price)
+ else:
+ if quantity != o.bom_quantity:
+ extra = ' (adjusted quantity: {})'.format(o.bom_quantity)
+ else:
+ extra = ''
+ print " SKU: {0}, price: {1:.2f}{2}".format(o.sku, price, extra)
+
+ for search_result in entry.search_response.results:
+ p = search_result.item
+# print "{}: Snippet: {}".format(p.uid, search_result.snippet)
+ print " {}: len={}, {}".format(p.uid, len(p.offers), search_result.snippet)
+# for offer in part.offers:
+# print 'offer: ' + str(offer)
+ i+=1
+
+ print "CSV:"
+ print "references;mpn;quantity;specification"
+ for entry in bom.entries:
+ if entry.octopart_mpn is None:
+ continue
+
+ print "{};{};{}".format(','.join(entry.references),entry.octopart_mpn,entry.quantity(),entry.component)