From c3b63695b585f7a3dd98c2d1ca0c729c3b74b8ef Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Thu, 3 Oct 2024 23:20:16 +1300 Subject: [PATCH] Fix build item over-allocation checks --- src/backend/InvenTree/build/models.py | 13 ++++-- src/backend/InvenTree/build/test_api.py | 59 +++++++++++++++++++++++++ src/backend/InvenTree/stock/models.py | 14 ++++-- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index 9b13dbf1edfa..22b98d031db8 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -1606,12 +1606,19 @@ def clean(self): '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 83af0a7f1dae..f3feee051adc 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -993,6 +993,65 @@ def test_fractional_allocation(self): 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 efe335fd25de..b4eb7e523cd1 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -1190,9 +1190,17 @@ def is_allocated(self): 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']