diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index c7c53258cd..a662e06346 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -4316,11 +4316,11 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel): return int(Decimal(available_stock) / n) - def get_required_quantity(self, build_quantity: float) -> float: + def get_required_quantity(self, build_quantity: Decimal | float | int) -> Decimal: """Calculate the required part quantity, based on the supplied build_quantity. Arguments: - build_quantity: Number of assemblies to build + build_quantity: Number of assemblies to build (converted to Decimal) Returns: Production quantity required for this component @@ -4330,6 +4330,10 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel): and piece_count indicates how many pieces are needed. Total material = quantity x piece_count x build_quantity. """ + # Convert via str to avoid binary float artefacts (e.g. 0.1 -> 0.1000000000000000055...) + if not isinstance(build_quantity, Decimal): + build_quantity = Decimal(str(build_quantity)) + # Base quantity requirement (quantity is per-piece, piece_count is number of pieces) required = self.quantity * self.piece_count * build_quantity @@ -4356,8 +4360,7 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel): try: round_up = Decimal(self.rounding_multiple) value = Decimal(required) - value = math.ceil(value / round_up) * round_up - required = float(value) + required = math.ceil(value / round_up) * round_up except InvalidOperation: log_error('bom_item.get_required_quantity') diff --git a/src/backend/InvenTree/part/test_bom_item.py b/src/backend/InvenTree/part/test_bom_item.py index a894c4870c..4b37f90af6 100644 --- a/src/backend/InvenTree/part/test_bom_item.py +++ b/src/backend/InvenTree/part/test_bom_item.py @@ -1,6 +1,7 @@ """Unit tests for the BomItem model.""" import hashlib +from decimal import Decimal from unittest import mock import django.core.exceptions as django_exceptions @@ -112,6 +113,20 @@ class BomItemTest(TestCase): self.assertEqual(item.get_required_quantity(1), 8) # 3 + 5 = 8 self.assertEqual(item.get_required_quantity(10), 35) # 30 + 5 = 35 + def test_required_quantity_input_types(self): + """Test that get_required_quantity accepts int, float and Decimal inputs.""" + item = BomItem.objects.get(pk=4) + + for value, expected in [ + (10, Decimal('30')), + (Decimal('2.5'), Decimal('7.5')), + (2.5, Decimal('7.5')), + (0.1, Decimal('0.3')), + ]: + result = item.get_required_quantity(value) + self.assertIsInstance(result, Decimal) + self.assertEqual(result, expected) + def test_round_up(self): """Test the 'rounding_multiple' attribute.""" item = BomItem.objects.get(pk=4) @@ -143,6 +158,9 @@ class BomItemTest(TestCase): item.get_required_quantity(100), 306 ) # 3 x 100 = 300, rounded up to nearest multiple of 17 + # The rounded value must remain a Decimal (not a float) + self.assertIsInstance(item.get_required_quantity(10), Decimal) + # Next, let's create a new Build order bo = build.models.Build.objects.create( part=item.part, quantity=21, reference='BO-9999', title='Test Build Order'