diff --git a/docs/docs/assets/images/plugin/builtin/bom_export_options.png b/docs/docs/assets/images/plugin/builtin/bom_export_options.png index 2da4aa44b0..51ff9fbc18 100644 Binary files a/docs/docs/assets/images/plugin/builtin/bom_export_options.png and b/docs/docs/assets/images/plugin/builtin/bom_export_options.png differ diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index c702fb47d8..2eb9c704ae 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -2809,7 +2809,7 @@ class BomItemTest(InvenTreeAPITestCase): ) # Check that the correct exporter plugin has been used - required_cols.extend(['BOM Level']) + required_cols.extend(['BOM Level', 'Total Quantity']) # Next, download BOM data for a specific sub-assembly, and use the BOM exporter with self.export_data( diff --git a/src/backend/InvenTree/plugin/builtin/exporter/bom_exporter.py b/src/backend/InvenTree/plugin/builtin/exporter/bom_exporter.py index d5fbc385bd..3e793361dd 100644 --- a/src/backend/InvenTree/plugin/builtin/exporter/bom_exporter.py +++ b/src/backend/InvenTree/plugin/builtin/exporter/bom_exporter.py @@ -1,9 +1,13 @@ """Multi-level BOM exporter plugin.""" +from decimal import Decimal +from typing import Optional + from django.utils.translation import gettext_lazy as _ import rest_framework.serializers as serializers +from InvenTree.helpers import normalize from part.models import BomItem from plugin import InvenTreePlugin from plugin.mixins import DataExportMixin @@ -13,12 +17,20 @@ class BomExporterOptionsSerializer(serializers.Serializer): """Custom export options for the BOM exporter plugin.""" export_levels = serializers.IntegerField( - default=1, + default=0, label=_('Levels'), - help_text=_('Number of levels to export'), + help_text=_( + 'Number of levels to export - set to zero to export all BOM levels' + ), min_value=0, ) + export_total_quantity = serializers.BooleanField( + default=True, + label=_('Total Quantity'), + help_text=_('Include total quantity of each part in the BOM'), + ) + export_stock_data = serializers.BooleanField( default=True, label=_('Stock Data'), help_text=_('Include part stock data') ) @@ -57,7 +69,7 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin): SLUG = 'bom-exporter' TITLE = _('Multi-Level BOM Exporter') DESCRIPTION = _('Provides support for exporting multi-level BOMs') - VERSION = '1.0.0' + VERSION = '1.1.0' AUTHOR = _('InvenTree contributors') ExportOptionsSerializer = BomExporterOptionsSerializer @@ -68,6 +80,8 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin): def update_headers(self, headers, context, **kwargs): """Update headers for the BOM export.""" + export_total_quantity = context.get('export_total_quantity', True) + if not self.export_stock_data: # Remove stock data from the headers for field in [ @@ -95,6 +109,10 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin): # Append a "BOM Level" field headers['level'] = _('BOM Level') + if export_total_quantity: + # Append a 'total quantity' field + headers['total_quantity'] = _('Total Quantity') + # Append variant part columns if self.export_substitute_data and self.n_substitute_cols > 0: for idx in range(self.n_substitute_cols): @@ -167,6 +185,7 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin): self.export_manufacturer_data = context.get('export_manufacturer_data', True) self.export_substitute_data = context.get('export_substitute_data', True) self.export_parameter_data = context.get('export_parameter_data', True) + self.export_total_quantity = context.get('export_total_quantity', True) # Pre-fetch related data to reduce database queries queryset = self.prefetch_queryset(queryset) @@ -179,18 +198,23 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin): return self.bom_data - def process_bom_row(self, bom_item, level, **kwargs) -> list: + def process_bom_row( + self, bom_item, level: int = 1, multiplier: Optional[Decimal] = None, **kwargs + ) -> list: """Process a single BOM row. Arguments: bom_item: The BomItem object to process level: The current level of export - + multiplier: The multiplier for the quantity (used for recursive calls) """ # Add this row to the output dataset row = self.serializer_class(bom_item, exporting=True).data row['level'] = level + if multiplier is None: + multiplier = Decimal(1) + # Extend with additional data if self.export_substitute_data: @@ -205,6 +229,11 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin): if self.export_parameter_data: row.update(self.get_parameter_data(bom_item)) + if self.export_total_quantity: + # Calculate the total quantity for this BOM item + total_quantity = Decimal(bom_item.quantity) * multiplier + row['total_quantity'] = normalize(total_quantity) + self.bom_data.append(row) # If we have reached the maximum export level, return just this bom item @@ -215,7 +244,12 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin): sub_items = self.prefetch_queryset(sub_items) for item in sub_items.all(): - self.process_bom_row(item, level + 1, **kwargs) + self.process_bom_row( + item, + level=level + 1, + multiplier=multiplier * bom_item.quantity, + **kwargs, + ) def get_substitute_data(self, bom_item: BomItem) -> dict: """Return substitute part data for a BomItem."""