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)