From aa9958bf11a59911e8a8312fa4655ef955cf39ed Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 15 Nov 2025 07:42:48 +1100 Subject: [PATCH] [bug] State change fixes (#10832) * Fix for setting custom status * Fix for setting custom status when receiving stock items * Allow caching for set_status * Updated code and unit tests --- .../InvenTree/generic/states/states.py | 16 +++++++++++--- src/backend/InvenTree/order/models.py | 21 +++++++++++++------ src/backend/InvenTree/order/test_api.py | 10 ++++++++- src/backend/InvenTree/stock/models.py | 2 +- src/backend/InvenTree/stock/serializers.py | 13 +++++++++--- src/backend/InvenTree/stock/test_api.py | 14 +++++++++---- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/backend/InvenTree/generic/states/states.py b/src/backend/InvenTree/generic/states/states.py index 3984947888..d94793e748 100644 --- a/src/backend/InvenTree/generic/states/states.py +++ b/src/backend/InvenTree/generic/states/states.py @@ -309,13 +309,23 @@ class StatusCodeMixin: return status is not None and status == self.get_custom_status() - def set_status(self, status: int) -> bool: - """Set the status code for this object.""" + def set_status(self, status: int, custom_values=None) -> bool: + """Set the status code for this object. + + Arguments: + status: The status code to set + custom_values: Optional list of custom values to consider (can be used to avoid DB queries) + """ if not self.status_class: raise NotImplementedError('Status class not defined') base_values = self.status_class.values() - custom_value_set = self.status_class.custom_values() + + custom_value_set = ( + self.status_class.custom_values() + if custom_values is None + else custom_values + ) custom_field = f'{self.STATUS_FIELD}_custom_key' diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 97584b91b0..81f5878f8d 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -976,6 +976,9 @@ class PurchaseOrder(TotalPriceMixin, Order): # Prefetch line item objects for DB efficiency line_items_ids = [item['line_item'].pk for item in items] + # Cache the custom status options for the StockItem model + custom_stock_status_values = stock.models.StockItem.STATUS_CLASS.custom_values() + line_items = PurchaseOrderLineItem.objects.filter( pk__in=line_items_ids ).prefetch_related('part', 'part__part', 'order') @@ -1050,7 +1053,6 @@ class PurchaseOrder(TotalPriceMixin, Order): 'supplier_part': supplier_part, 'purchase_order': self, 'purchase_price': purchase_price, - 'status': item.get('status', StockStatus.OK.value), 'location': stock_location, 'quantity': 1 if serialize else stock_quantity, 'batch': item.get('batch_code', ''), @@ -1059,6 +1061,9 @@ class PurchaseOrder(TotalPriceMixin, Order): 'packaging': item.get('packaging') or supplier_part.packaging, } + # Extract the "status" field + status = item.get('status', StockStatus.OK.value) + # Check linked build order # This is for receiving against an *external* build order if build_order := line.build_order: @@ -1099,11 +1104,14 @@ class PurchaseOrder(TotalPriceMixin, Order): # Now, create the new stock items if serialize: - stock_items.extend( - stock.models.StockItem._create_serial_numbers( - serials=serials, **stock_data - ) + new_items = stock.models.StockItem._create_serial_numbers( + serials=serials, **stock_data ) + + for item in new_items: + item.set_status(status, custom_values=custom_stock_status_values) + stock_items.append(item) + else: new_item = stock.models.StockItem( **stock_data, @@ -1115,10 +1123,11 @@ class PurchaseOrder(TotalPriceMixin, Order): rght=2, ) + new_item.set_status(status, custom_values=custom_stock_status_values) + if barcode: new_item.assign_barcode(barcode_data=barcode, save=False) - # new_item.save() bulk_create_items.append(new_item) # Update the line item quantity diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 9442b09e78..93610e80ed 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -1226,6 +1226,8 @@ class PurchaseOrderReceiveTest(OrderTest): def test_receive_large_quantity(self): """Test receipt of a large number of items.""" + from stock.status_codes import StockStatus + sp = SupplierPart.objects.first() # Create a new order @@ -1256,7 +1258,12 @@ class PurchaseOrderReceiveTest(OrderTest): url, { 'items': [ - {'line_item': line.pk, 'quantity': line.quantity} for line in lines + { + 'line_item': line.pk, + 'quantity': line.quantity, + 'status': StockStatus.QUARANTINED.value, + } + for line in lines ], 'location': location.pk, }, @@ -1269,6 +1276,7 @@ class PurchaseOrderReceiveTest(OrderTest): for item in response: self.assertEqual(item['purchase_order'], po.pk) + self.assertEqual(item['status'], StockStatus.QUARANTINED) # Check that the order has been completed po.refresh_from_db() diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index a8519de9ec..7629fb7036 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -827,7 +827,7 @@ class StockItem( super().save(*args, **kwargs) # If user information is provided, and no existing note exists, create one! - if user and add_note and self.tracking_info.count() == 0: + if add_note and self.tracking_info.count() == 0: tracking_info = {'status': self.status} self.add_tracking_entry( diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 4154c533e6..421d943cf2 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -1060,12 +1060,19 @@ class StockChangeStatusSerializer(serializers.Serializer): # Instead of performing database updates for each item, # perform bulk database updates (much more efficient) + # Pre-cache the custom status values (to reduce DB hits) + custom_status_codes = StockItem.STATUS_CLASS.custom_values() + for item in items: # Ignore items which are already in the desired status - if item.compare_status(status): - continue - item.set_status(status) + # Careful check for custom status codes also + if item.compare_status(status): + custom_status = item.get_custom_status() + if status == custom_status or custom_status is None: + continue + + item.set_status(status, custom_values=custom_status_codes) item.save(add_note=False) # Create a new transaction note for each item diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 0d298cec6e..f3973676ef 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -1701,12 +1701,18 @@ class StockItemTest(StockAPITestCase): prt = Part.objects.first() + # Number of items to create + N_ITEMS = 10 + # Create a bunch of items - items = [StockItem.objects.create(part=prt, quantity=10) for _ in range(10)] + items = [ + StockItem.objects.create(part=prt, quantity=10) for _ in range(N_ITEMS) + ] for item in items: item.refresh_from_db() self.assertEqual(item.status, StockStatus.OK.value) + self.assertEqual(item.tracking_info.count(), 1) data = { 'items': [item.pk for item in items], @@ -1719,10 +1725,10 @@ class StockItemTest(StockAPITestCase): for item in items: item.refresh_from_db() self.assertEqual(item.status, StockStatus.DAMAGED.value) - self.assertEqual(item.tracking_info.count(), 1) + self.assertEqual(item.tracking_info.count(), 2) # Same test, but with one item unchanged - items[0].status = StockStatus.ATTENTION.value + items[0].set_status(StockStatus.ATTENTION.value) items[0].save() data['status'] = StockStatus.ATTENTION.value @@ -1732,7 +1738,7 @@ class StockItemTest(StockAPITestCase): for item in items: item.refresh_from_db() self.assertEqual(item.status, StockStatus.ATTENTION.value) - self.assertEqual(item.tracking_info.count(), 2) + self.assertEqual(item.tracking_info.count(), 3) tracking = item.tracking_info.last() self.assertEqual(tracking.tracking_type, StockHistoryCode.EDITED.value)