From e30b81a822d56a47e7b6ebb618a746534b9d9992 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Mon, 28 Sep 2015 08:27:09 +0200 Subject: wip --- kicad_bom_cmd.py | 304 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 kicad_bom_cmd.py (limited to 'kicad_bom_cmd.py') 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 + +# +# +# +# 1k +# Resistors_SMD:R_1210_HandSoldering +# +# 1470030RL +# +# +# +# 55FEEA8B + +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) -- cgit v1.2.3