diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 6c89ff7265..3725d79c7b 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -52,6 +52,10 @@ font-size: 12px; } +.glyphicon-right { + float: right; +} + .starred-part { color: #ffbb00; } diff --git a/InvenTree/InvenTree/static/script/inventree/bom.js b/InvenTree/InvenTree/static/script/inventree/bom.js index 8d0accb83b..1b74088abf 100644 --- a/InvenTree/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/InvenTree/static/script/inventree/bom.js @@ -133,7 +133,14 @@ function loadBomTable(table, options) { title: 'Part', sortable: true, formatter: function(value, row, index, field) { - return imageHoverIcon(row.sub_part_detail.image_url) + renderLink(row.sub_part_detail.full_name, row.sub_part_detail.url); + var html = imageHoverIcon(row.sub_part_detail.image_url) + renderLink(row.sub_part_detail.full_name, row.sub_part_detail.url); + + // Display an extra icon if this part is an assembly + if (row.sub_part_detail.assembly) { + html += ""; + } + + return html; } } ); diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 20ad569a12..6f9d2369e5 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -129,15 +129,17 @@ class PartStarAdmin(admin.ModelAdmin): class BomItemResource(ModelResource): """ Class for managing BomItem data import/export """ + level = Field(attribute='level', readonly=True) + part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) - part_name = Field(attribute='part__full_name', readonly=True) + parent_part_name = Field(attribute='part__full_name', readonly=True) - sub_part = Field(attribute='sub_part', widget=widgets.ForeignKeyWidget(Part)) + id = Field(attribute='sub_part', widget=widgets.ForeignKeyWidget(Part)) sub_part_name = Field(attribute='sub_part__full_name', readonly=True) - stock = Field(attribute='sub_part__total_stock', readonly=True) + sub_assembly = Field(attribute='sub_part__assembly', readonly=True) class Meta: model = BomItem @@ -145,7 +147,7 @@ class BomItemResource(ModelResource): report_skipped = False clean_model_instances = True - exclude = ('checksum') + exclude = ['checksum', ] class BomItemAdmin(ImportExportModelAdmin): diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 394314d461..a1f8b5ec6a 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -40,16 +40,43 @@ def MakeBomTemplate(fmt): return DownloadFile(data, filename) -def ExportBom(part, fmt='csv'): +def ExportBom(part, fmt='csv', cascade=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 = part.bom_items.all().order_by('id') + bom_items = [] - dataset = BomItemResource().export(queryset=bom_items) + def add_items(items, level): + # Add items at a given layer + for item in items: + + item.level = '-' * level + + bom_items.append(item) + + if item.sub_part.assembly: + add_items(item.sub_part.bom_items.all().order_by('id'), level + 1) + + if cascade: + # Cascading (multi-level) BOM + + # Start with the top level + items_to_process = part.bom_items.all().order_by('id') + + add_items(items_to_process, 1) + + else: + # No cascading needed - just the top-level items + bom_items = [item for item in part.bom_items.all().order_by('id')] + + dataset = BomItemResource().export(queryset=bom_items, cascade=cascade) data = dataset.export(fmt) filename = '{n}_BOM.{fmt}'.format(n=part.full_name, fmt=fmt) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 7a4eecd583..5adf1e08dd 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -6,6 +6,7 @@ Django Forms for interacting with Part objects from __future__ import unicode_literals from InvenTree.forms import HelperForm +from InvenTree.helpers import GetExportFormats from mptt.fields import TreeNodeChoiceField from django import forms @@ -28,6 +29,26 @@ class PartImageForm(HelperForm): ] +class BomExportForm(forms.Form): + """ Simple form to let user set BOM export options, + before exporting a BOM (bill of materials) file. + """ + + file_format = forms.ChoiceField(label=_("File Format"), help_text=_("Select output file format")) + + cascading = forms.BooleanField(label=_("Cascading"), required=False, initial=False, help_text=_("Download cascading / multi-level BOM")) + + def get_choices(self): + """ BOM export format choices """ + + return [(x, x.upper()) for x in GetExportFormats()] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields['file_format'].choices = self.get_choices() + + class BomValidateForm(HelperForm): """ Simple confirmation form for BOM validation. User is presented with a single checkbox input, diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 3e1ed6949a..c23298a441 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -65,6 +65,8 @@ class PartBriefSerializer(InvenTreeModelSerializer): 'available_stock', 'image_url', 'active', + 'assembly', + 'virtual', ] diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 3d61e24e2a..374acc94fd 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -44,16 +44,7 @@ {% if part.is_bom_valid == False %} {% endif %} -