Additional unit tests

This commit is contained in:
Oliver Walters 2026-07-03 02:10:30 +00:00
parent 98dc285ebf
commit 268b638a04
3 changed files with 149 additions and 0 deletions

View File

@ -1887,6 +1887,77 @@ class BuildLineTests(BuildAPITest):
self.assertSetEqual(false_ids, expected_false)
self.assertSetEqual(true_ids | false_ids, {line.pk for line in lines})
def test_filter_consumable_via_part(self):
"""Filter BuildLine objects by 'consumable' status, accounting for the underlying part.
A BuildLine should be treated as 'consumable' if either the BOM line
itself is marked as consumable, or the underlying part is marked as consumable.
"""
assembly = Part.objects.create(
name='Consumable Filter Assembly',
description='Assembly for consumable filter tests',
assembly=True,
)
plain = Part.objects.create(
name='Consumable Filter Plain Component',
description='A regular component',
component=True,
)
consumable_part = Part.objects.create(
name='Consumable Filter Consumable Part',
description='A part marked as consumable',
component=True,
consumable=True,
)
consumable_line_part = Part.objects.create(
name='Consumable Filter Consumable BOM Line',
description='A part which is consumable only via its BOM line',
component=True,
)
bom_item_plain = BomItem.objects.create(
part=assembly, sub_part=plain, quantity=1
)
bom_item_part_consumable = BomItem.objects.create(
part=assembly, sub_part=consumable_part, quantity=1
)
bom_item_line_consumable = BomItem.objects.create(
part=assembly, sub_part=consumable_line_part, quantity=1, consumable=True
)
build = Build.objects.create(
part=assembly,
reference='BO-9997',
quantity=1,
title='Consumable Filter Build',
)
url = reverse('api-build-line-list')
response = self.get(url, data={'build': build.pk, 'consumable': True})
returned_bom_items = {item['bom_item'] for item in response.data}
self.assertIn(bom_item_part_consumable.pk, returned_bom_items)
self.assertIn(bom_item_line_consumable.pk, returned_bom_items)
self.assertNotIn(bom_item_plain.pk, returned_bom_items)
response = self.get(url, data={'build': build.pk, 'consumable': False})
returned_bom_items = {item['bom_item'] for item in response.data}
self.assertIn(bom_item_plain.pk, returned_bom_items)
self.assertNotIn(bom_item_part_consumable.pk, returned_bom_items)
self.assertNotIn(bom_item_line_consumable.pk, returned_bom_items)
# Check that the serialized 'consumable' field reflects the combined status
response = self.get(
url, data={'build': build.pk, 'bom_item': bom_item_part_consumable.pk}
)
self.assertEqual(len(response.data), 1)
self.assertTrue(response.data[0]['consumable'])
def test_output_options(self):
"""Test output options for the BuildLine endpoint."""
self.run_output_test(

View File

@ -998,6 +998,38 @@ class AutoAllocationTests(BuildTestBase):
self.assertEqual(self.build.allocated_stock.count(), N - 8)
def test_consumable_via_part(self):
"""A BOM line should be treated as consumable if the underlying part is consumable.
Even though 'bom_item_1' itself is not marked as consumable, marking
'sub_part_1' as consumable should have the same effect as marking the
BOM line itself as consumable.
"""
self.sub_part_1.consumable = True
self.sub_part_1.save()
self.assertFalse(self.bom_item_1.consumable)
self.assertTrue(self.bom_item_1.is_consumable)
# The BuildLine should be treated as fully allocated, without any stock allocated
self.assertEqual(self.line_1.allocated_quantity(), 0)
self.assertTrue(self.line_1.is_fully_allocated())
# The build should not consider this line when checking for unallocated lines
unallocated_lines = self.build.unallocated_lines(tracked=False)
self.assertNotIn(self.line_1, unallocated_lines)
# Auto-allocation should skip this line, even though stock exists
self.build.auto_allocate_stock(
interchangeable=True, substitutes=True, optional_items=True
)
self.assertEqual(self.line_1.allocated_quantity(), 0)
self.assertEqual(BuildItem.objects.filter(build_line=self.line_1).count(), 0)
# The other (non-consumable) line should still be allocated as normal
self.assertEqual(self.line_2.allocated_quantity(), 30)
class ExternalBuildTest(InvenTreeAPITestCase):
"""Unit tests for external build order functionality."""

View File

@ -277,6 +277,52 @@ class BomItemTest(TestCase):
self.assertEqual(assembly.can_build, 20)
# Mark 'c4' as consumable at the *part* level (not the BOM line itself)
c4.consumable = True
c4.save()
bom_item_c4 = BomItem.objects.create(part=assembly, sub_part=c4, quantity=200)
# The raw BomItem field is unset, but the part is marked as consumable
self.assertFalse(bom_item_c4.consumable)
self.assertTrue(bom_item_c4.is_consumable)
# A BomItem which is consumable via its part does not alter the can_build calculation
self.assertEqual(assembly.can_build, 20)
def test_consumable_filter(self):
"""Tests for the BomItem.consumable_filter() helper method."""
assembly = Part.objects.create(
name='Another assembly', description='Made with parts', assembly=True
)
c1 = Part.objects.create(name='D1', description='Not consumable')
c2 = Part.objects.create(name='D2', description='Consumable BOM line')
c3 = Part.objects.create(
name='D3', description='Consumable part', consumable=True
)
bom_item_1 = BomItem.objects.create(part=assembly, sub_part=c1, quantity=1)
bom_item_2 = BomItem.objects.create(
part=assembly, sub_part=c2, quantity=1, consumable=True
)
bom_item_3 = BomItem.objects.create(part=assembly, sub_part=c3, quantity=1)
consumable_items = set(
BomItem.objects.filter(BomItem.consumable_filter(consumable=True))
)
non_consumable_items = set(
BomItem.objects.filter(BomItem.consumable_filter(consumable=False))
)
self.assertIn(bom_item_2, consumable_items)
self.assertIn(bom_item_3, consumable_items)
self.assertNotIn(bom_item_1, consumable_items)
self.assertIn(bom_item_1, non_consumable_items)
self.assertNotIn(bom_item_2, non_consumable_items)
self.assertNotIn(bom_item_3, non_consumable_items)
def test_metadata(self):
"""Unit tests for the metadata field."""
for model in [BomItem]: