""" Functionality for Bill of Material (BOM) management. Primarily BOM upload tools. """ from collections import OrderedDict from django.utils.translation import gettext as _ from InvenTree.helpers import DownloadFile, GetExportFormats, normalize from .admin import BomItemResource from .models import BomItem from company.models import ManufacturerPart, SupplierPart def IsValidBOMFormat(fmt): """ Test if a file format specifier is in the valid list of BOM file formats """ return fmt.strip().lower() in GetExportFormats() def MakeBomTemplate(fmt): """ Generate a Bill of Materials upload template file (for user download) """ fmt = fmt.strip().lower() if not IsValidBOMFormat(fmt): fmt = 'csv' # Create an "empty" queryset, essentially. # This will then export just the row headers! query = BomItem.objects.filter(pk=None) dataset = BomItemResource().export( queryset=query, importing=True ) data = dataset.export(fmt) filename = 'InvenTree_BOM_Template.' + fmt return DownloadFile(data, filename) def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False, manufacturer_data=False): """ Export a BOM (Bill of Materials) for a given part. Args: fmt: File format (default = 'csv') cascade: If True, multi-level BOM output is supported. Otherwise, a flat top-level-only BOM is exported. """ if not IsValidBOMFormat(fmt): fmt = 'csv' bom_items = [] uids = [] def add_items(items, level, cascade=True): # Add items at a given layer for item in items: item.level = str(int(level)) # Avoid circular BOM references if item.pk in uids: continue bom_items.append(item) if cascade and item.sub_part.assembly: if max_levels is None or level < max_levels: add_items(item.sub_part.bom_items.all().order_by('id'), level + 1) top_level_items = part.get_bom_items().order_by('id') add_items(top_level_items, 1, cascade) dataset = BomItemResource().export(queryset=bom_items, cascade=cascade) def add_columns_to_dataset(columns, column_size): try: for header, column_dict in columns.items(): # Construct column tuple col = tuple(column_dict.get(c_idx, '') for c_idx in range(column_size)) # Add column to dataset dataset.append_col(col, header=header) except AttributeError: pass if parameter_data: """ If requested, add extra columns for each PartParameter associated with each line item """ parameter_cols = {} for b_idx, bom_item in enumerate(bom_items): # Get part parameters parameters = bom_item.sub_part.get_parameters() # Add parameters to columns if parameters: for parameter in parameters: name = parameter.template.name value = parameter.data try: parameter_cols[name].update({b_idx: value}) except KeyError: parameter_cols[name] = {b_idx: value} # Add parameter columns to dataset parameter_cols_ordered = OrderedDict(sorted(parameter_cols.items(), key=lambda x: x[0])) add_columns_to_dataset(parameter_cols_ordered, len(bom_items)) if stock_data: """ If requested, add extra columns for stock data associated with each line item """ stock_headers = [ _('Default Location'), _('Total Stock'), _('Available Stock'), _('On Order'), ] stock_cols = {} for b_idx, bom_item in enumerate(bom_items): stock_data = [] sub_part = bom_item.sub_part # Get part default location try: loc = sub_part.get_default_location() if loc is not None: stock_data.append(str(loc.name)) else: stock_data.append('') except AttributeError: stock_data.append('') # Total "in stock" quantity for this part stock_data.append( str(normalize(sub_part.total_stock)) ) # Total "available stock" quantity for this part stock_data.append( str(normalize(sub_part.available_stock)) ) # Total "on order" quantity for this part stock_data.append( str(normalize(sub_part.on_order)) ) for s_idx, header in enumerate(stock_headers): try: stock_cols[header].update({b_idx: stock_data[s_idx]}) except KeyError: stock_cols[header] = {b_idx: stock_data[s_idx]} # Add stock columns to dataset add_columns_to_dataset(stock_cols, len(bom_items)) if manufacturer_data or supplier_data: """ If requested, add extra columns for each SupplierPart and ManufacturerPart associated with each line item """ # Keep track of the supplier parts we have already exported supplier_parts_used = set() manufacturer_cols = {} for bom_idx, bom_item in enumerate(bom_items): # Get part instance b_part = bom_item.sub_part # Include manufacturer data for each BOM item if manufacturer_data: # Filter manufacturer parts manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk).prefetch_related('supplier_parts') for mp_idx, mp_part in enumerate(manufacturer_parts): # Extract the "name" field of the Manufacturer (Company) if mp_part and mp_part.manufacturer: manufacturer_name = mp_part.manufacturer.name else: manufacturer_name = '' # Extract the "MPN" field from the Manufacturer Part if mp_part: manufacturer_mpn = mp_part.MPN else: manufacturer_mpn = '' # Generate a column name for this manufacturer k_man = f'{_("Manufacturer")}_{mp_idx}' k_mpn = f'{_("MPN")}_{mp_idx}' try: manufacturer_cols[k_man].update({bom_idx: manufacturer_name}) manufacturer_cols[k_mpn].update({bom_idx: manufacturer_mpn}) except KeyError: manufacturer_cols[k_man] = {bom_idx: manufacturer_name} manufacturer_cols[k_mpn] = {bom_idx: manufacturer_mpn} # We wish to include supplier data for this manufacturer part if supplier_data: for sp_idx, sp_part in enumerate(mp_part.supplier_parts.all()): supplier_parts_used.add(sp_part) if sp_part.supplier: supplier_name = sp_part.supplier.name else: supplier_name = '' if sp_part: supplier_sku = sp_part.SKU else: supplier_sku = '' # Generate column names for this supplier k_sup = str(_("Supplier")) + "_" + str(mp_idx) + "_" + str(sp_idx) k_sku = str(_("SKU")) + "_" + str(mp_idx) + "_" + str(sp_idx) try: manufacturer_cols[k_sup].update({bom_idx: supplier_name}) manufacturer_cols[k_sku].update({bom_idx: supplier_sku}) except KeyError: manufacturer_cols[k_sup] = {bom_idx: supplier_name} manufacturer_cols[k_sku] = {bom_idx: supplier_sku} if supplier_data: # Add in any extra supplier parts, which are not associated with a manufacturer part for sp_idx, sp_part in enumerate(SupplierPart.objects.filter(part__pk=b_part.pk)): if sp_part in supplier_parts_used: continue supplier_parts_used.add(sp_part) if sp_part.supplier: supplier_name = sp_part.supplier.name else: supplier_name = '' supplier_sku = sp_part.SKU # Generate column names for this supplier k_sup = str(_("Supplier")) + "_" + str(sp_idx) k_sku = str(_("SKU")) + "_" + str(sp_idx) try: manufacturer_cols[k_sup].update({bom_idx: supplier_name}) manufacturer_cols[k_sku].update({bom_idx: supplier_sku}) except KeyError: manufacturer_cols[k_sup] = {bom_idx: supplier_name} manufacturer_cols[k_sku] = {bom_idx: supplier_sku} # Add supplier columns to dataset add_columns_to_dataset(manufacturer_cols, len(bom_items)) data = dataset.export(fmt) filename = f"{part.full_name}_BOM.{fmt}" return DownloadFile(data, filename)