diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index 9b13dbf1ed..22b98d031d 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -1606,12 +1606,19 @@ class BuildItem(InvenTree.models.InvenTreeMetadataModel): 'quantity': _(f'Allocated quantity ({q}) must not exceed available stock quantity ({a})') }) - # Allocated quantity cannot cause the stock item to be over-allocated + # Ensure that we do not 'over allocate' a stock item available = decimal.Decimal(self.stock_item.quantity) - allocated = decimal.Decimal(self.stock_item.allocation_count()) quantity = decimal.Decimal(self.quantity) + build_allocation_count = decimal.Decimal(self.stock_item.build_allocation_count( + exclude_allocations={'pk': self.pk} + )) + sales_allocation_count = decimal.Decimal(self.stock_item.sales_order_allocation_count()) - if available - allocated + quantity < quantity: + total_allocation = ( + build_allocation_count + sales_allocation_count + quantity + ) + + if total_allocation > available: raise ValidationError({ 'quantity': _('Stock item is over-allocated') }) diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index 83af0a7f1d..f3feee051a 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -993,6 +993,65 @@ class BuildAllocationTest(BuildAPITest): expected_code=201, ) +class BuildItemTest(BuildAPITest): + """Unit tests for build items. + + For this test, we will be using Build ID=1; + + - This points to Part 100 (see fixture data in part.yaml) + - This Part already has a BOM with 4 items (see fixture data in bom.yaml) + - There are no BomItem objects yet created for this build + """ + + def setUp(self): + """Basic operation as part of test suite setup""" + super().setUp() + + self.assignRole('build.add') + self.assignRole('build.change') + + self.build = Build.objects.get(pk=1) + + # Regenerate BuildLine objects + self.build.create_build_line_items() + + # Record number of build items which exist at the start of each test + self.n = BuildItem.objects.count() + + def test_update_overallocated(self): + """Test update of overallocated stock items.""" + + si = StockItem.objects.get(pk=2) + + # Find line item + line = self.build.build_lines.all().filter(bom_item__sub_part=si.part).first() + + # Set initial stock item quantity + si.quantity = 100 + si.save() + + # Create build item + bi = BuildItem( + build_line=line, + stock_item=si, + quantity=100 + ) + bi.save() + + # Reduce stock item quantity + si.quantity = 50 + si.save() + + # Reduce build item quantity + url = reverse('api-build-item-detail', kwargs={'pk': bi.pk}) + + self.patch( + url, + { + "quantity": 50, + }, + expected_code=200, + ) class BuildOverallocationTest(BuildAPITest): """Unit tests for over allocation of stock items against a build order. diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index efe335fd25..b4eb7e523c 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -1190,9 +1190,17 @@ class StockItem( return self.sales_order_allocations.count() > 0 - def build_allocation_count(self): - """Return the total quantity allocated to builds.""" - query = self.allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0))) + def build_allocation_count(self, **kwargs): + """Return the total quantity allocated to builds, with optional filters.""" + query = self.allocations.all() + + if filter_allocations := kwargs.get('filter_allocations'): + query = query.filter(**filter_allocations) + + if exclude_allocations := kwargs.get('exclude_allocations'): + query = query.exclude(**exclude_allocations) + + query = query.aggregate(q=Coalesce(Sum('quantity'), Decimal(0))) total = query['q']