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:
Binary file not shown.
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 43 KiB |
@@ -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(
|
||||||
|
@@ -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."""
|
||||||
|
Reference in New Issue
Block a user