2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-13 14:11:37 +00:00

Bom total quantity (#10142)

* Add 'total quantity' field to BOM exporter

* Extend unit test

* Update docs image
This commit is contained in:
Oliver
2025-08-07 10:09:51 +10:00
committed by GitHub
parent 0411b8ca58
commit c4011d0be3
3 changed files with 41 additions and 7 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -2809,7 +2809,7 @@ class BomItemTest(InvenTreeAPITestCase):
) )
# Check that the correct exporter plugin has been used # 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 # Next, download BOM data for a specific sub-assembly, and use the BOM exporter
with self.export_data( with self.export_data(

View File

@@ -1,9 +1,13 @@
"""Multi-level BOM exporter plugin.""" """Multi-level BOM exporter plugin."""
from decimal import Decimal
from typing import Optional
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import rest_framework.serializers as serializers import rest_framework.serializers as serializers
from InvenTree.helpers import normalize
from part.models import BomItem from part.models import BomItem
from plugin import InvenTreePlugin from plugin import InvenTreePlugin
from plugin.mixins import DataExportMixin from plugin.mixins import DataExportMixin
@@ -13,12 +17,20 @@ class BomExporterOptionsSerializer(serializers.Serializer):
"""Custom export options for the BOM exporter plugin.""" """Custom export options for the BOM exporter plugin."""
export_levels = serializers.IntegerField( export_levels = serializers.IntegerField(
default=1, default=0,
label=_('Levels'), 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, 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( export_stock_data = serializers.BooleanField(
default=True, label=_('Stock Data'), help_text=_('Include part stock data') default=True, label=_('Stock Data'), help_text=_('Include part stock data')
) )
@@ -57,7 +69,7 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin):
SLUG = 'bom-exporter' SLUG = 'bom-exporter'
TITLE = _('Multi-Level BOM Exporter') TITLE = _('Multi-Level BOM Exporter')
DESCRIPTION = _('Provides support for exporting multi-level BOMs') DESCRIPTION = _('Provides support for exporting multi-level BOMs')
VERSION = '1.0.0' VERSION = '1.1.0'
AUTHOR = _('InvenTree contributors') AUTHOR = _('InvenTree contributors')
ExportOptionsSerializer = BomExporterOptionsSerializer ExportOptionsSerializer = BomExporterOptionsSerializer
@@ -68,6 +80,8 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin):
def update_headers(self, headers, context, **kwargs): def update_headers(self, headers, context, **kwargs):
"""Update headers for the BOM export.""" """Update headers for the BOM export."""
export_total_quantity = context.get('export_total_quantity', True)
if not self.export_stock_data: if not self.export_stock_data:
# Remove stock data from the headers # Remove stock data from the headers
for field in [ for field in [
@@ -95,6 +109,10 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin):
# Append a "BOM Level" field # Append a "BOM Level" field
headers['level'] = _('BOM Level') headers['level'] = _('BOM Level')
if export_total_quantity:
# Append a 'total quantity' field
headers['total_quantity'] = _('Total Quantity')
# Append variant part columns # Append variant part columns
if self.export_substitute_data and self.n_substitute_cols > 0: if self.export_substitute_data and self.n_substitute_cols > 0:
for idx in range(self.n_substitute_cols): 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_manufacturer_data = context.get('export_manufacturer_data', True)
self.export_substitute_data = context.get('export_substitute_data', True) self.export_substitute_data = context.get('export_substitute_data', True)
self.export_parameter_data = context.get('export_parameter_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 # Pre-fetch related data to reduce database queries
queryset = self.prefetch_queryset(queryset) queryset = self.prefetch_queryset(queryset)
@@ -179,18 +198,23 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin):
return self.bom_data 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. """Process a single BOM row.
Arguments: Arguments:
bom_item: The BomItem object to process bom_item: The BomItem object to process
level: The current level of export level: The current level of export
multiplier: The multiplier for the quantity (used for recursive calls)
""" """
# Add this row to the output dataset # Add this row to the output dataset
row = self.serializer_class(bom_item, exporting=True).data row = self.serializer_class(bom_item, exporting=True).data
row['level'] = level row['level'] = level
if multiplier is None:
multiplier = Decimal(1)
# Extend with additional data # Extend with additional data
if self.export_substitute_data: if self.export_substitute_data:
@@ -205,6 +229,11 @@ class BomExporterPlugin(DataExportMixin, InvenTreePlugin):
if self.export_parameter_data: if self.export_parameter_data:
row.update(self.get_parameter_data(bom_item)) 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) self.bom_data.append(row)
# If we have reached the maximum export level, return just this bom item # 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) sub_items = self.prefetch_queryset(sub_items)
for item in sub_items.all(): 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: def get_substitute_data(self, bom_item: BomItem) -> dict:
"""Return substitute part data for a BomItem.""" """Return substitute part data for a BomItem."""