Source code for sr.tools.bom.bom

"""Routines for extracting BOMs from schematics."""
from __future__ import print_function

from decimal import Decimal
import os

from sr.tools.bom import schem
from sr.tools.bom.threadpool import ThreadPool


STOCK_OUT = 0
STOCK_OK = 1
STOCK_UNKNOWN = 2

NUM_THREADS = 4


[docs]class PartGroup(list): """ A set of parts. One might call this a "BOM line". :param part: The part. :param str name: The name of the group. :param list designators: A list of designators """ def __init__(self, part, name="", designators=[]): """Create a new part group.""" list.__init__(self) for x in designators: self.append((name, designators)) self.part = part self.name = name
[docs] def stockcheck(self): """ Check the distributor has enough parts in stock. :returns: ``None`` if the result cannot be determined, ``True`` if the distributor has enough parts, otherwise ``False``. :rtype: None or bool """ s = self.part.stockcheck() if s is None: return None if s is True: # There are some in stock, but we don't know how many return None if s < self.order_num(): return False return True
[docs] def order_num(self): """ Get the number of parts to order from a distributor. For example, if we need 5002 components from a 5000 component reel, this will return 2. :returns: The number of parts to order. :rtype: int """ if self.part.stockcheck() is None: # unable to discover details from distributor... # assume one part per distributor unit return len(self) if self.part.get_dist_units() is None: # Same as above return len(self) n = len(self) if n == 0: return 0 # change n to be in distributor units, rather than component units # (e.g. number of reels rather than number of components) d = n / self.part.get_dist_units() if n % self.part.get_dist_units() > 0: d = d + 1 n = d if n < self.part.get_min_order(): # round up to minimum order n = self.part.get_min_order() elif (n % self.part.get_increments()) != 0: n = n + (self.part.get_increments() - (n % self.part.get_increments())) # Some (hopefully) sane assertions assert n % self.part.get_increments() == 0 assert n >= self.part.get_min_order() return n
[docs] def get_price(self): """ Returns the price of the group. :returns: The price. :rtype: decimal.Decimal """ n = self.order_num() p = self.part.get_price(n) if p is None: print("Warning: couldn't get price for %s (%s)" % (self.part["sr-code"], self.part["supplier"])) return Decimal(0) return p * Decimal(n)
[docs]class Bom(dict): """A bill of materials."""
[docs] def stockcheck(self): """ Check that all items in the schematic are in stock. Returns list of things that aren't in stock. :returns: An iterator containing pairs of ``STOCK_UNKNOWN``, ``STOCK_OUT``, ``STOCK_OK`` and the part. :rtype: iterator of tuples """ for pg in self.values(): a = pg.stockcheck() if a is None: yield (STOCK_UNKNOWN, pg.part) elif not a: yield (STOCK_OUT, pg.part) else: yield (STOCK_OK, pg.part)
[docs] def get_price(self): """ Get total price of all the items. :returns: The total price. :rtype: decimal.Decimal """ tot = Decimal(0) for pg in self.values(): tot = tot + pg.get_price() return tot
[docs]class BoardBom(Bom): """ BOM object. Groups parts with the same asset code into PartGroups. Dictionary keys are asset codes. :param db: A parts DB instance. :param fname: The schematic to load from. :param name: The name to give the schematic. """ def __init__(self, db, fname, name): """Create a new ``BoardBom`` object.""" Bom.__init__(self) self.db = db self.name = name s = schem.open_schem(fname) for des, srcode in s.items(): if srcode == "unknown": print("No value set for %s" % des) continue if srcode not in self: self[srcode] = PartGroup(db[srcode], name) self[srcode].append((name, des))
[docs]class MultiBoardBom(Bom): """ A bill of materials with multiple boards. :param db: A parts DB instance.""" def __init__(self, db): """Create multiple board BOM.""" Bom.__init__(self) self.db = db # Array of 2-entry lists # 0: Number of boards # 1: Board self.boards = []
[docs] def load_boards_args(self, args, allow_multipliers=True): """ Load the BOM from board arguments, which is a list of arguments where each item is a string either starting with a '-' and then a number, meaning it is a multiplier, or just a string which contains the schematic. :param args: The board arguments. :param bool allow_multipliers: Whether or not to allow multipliers in the board arguments. """ mul = 1 for arg in args: if arg[0] == '-' and allow_multipliers: mul = int(arg[1:]) else: board = BoardBom(self.db, arg, os.path.basename(arg)) self.add_boards(board, mul)
[docs] def add_boards(self, board, num): """ Add boards to the collection. :param BoardCom board: The board to add. :param int num: The number of times to add it. """ # already part of this collection? found = False for n in range(len(self.boards)): t = self.boards[n] if t[1] == board: t[0] = t[0] + num found = True break if not found: self.boards.append([num, board]) # update our PartGroup dictionary self.clear() for num, board in self.boards: # Mmmmm. Horrible. for i in range(num): for srcode, bpg in board.items(): if srcode not in self: self[srcode] = PartGroup(bpg.part) self[srcode] += bpg
[docs] def prime_cache(self): """ Ensures that the webpage cache is filled in the quickest time possible by making many requests in parallel. """ print("Getting data for parts from suppliers' websites") pool = ThreadPool(NUM_THREADS) for srcode, pg in self.items(): print(srcode) pool.add_task(pg.get_price) pool.wait_completion()