From bc2dbfa0c28ff6f9f8bc653b6987d89ff7ef20fe Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 26 Jun 2025 07:42:06 +1000 Subject: [PATCH] [bug] Custom state fix (#9858) * Set status correctly when returning from customer * Fix for stock item status change - Reduced set of changes from #9781 * Handle API updates * Fix variable shadowing * More intelligent comparison * Remove debug statement --- .../InvenTree/generic/states/states.py | 7 ++++++ src/backend/InvenTree/stock/api.py | 15 ++++++++++- src/backend/InvenTree/stock/models.py | 15 +++++------ src/backend/InvenTree/stock/serializers.py | 25 +++++++++++-------- src/backend/InvenTree/stock/test_views.py | 6 ++--- 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/backend/InvenTree/generic/states/states.py b/src/backend/InvenTree/generic/states/states.py index 3304cc3e5c..e66971f393 100644 --- a/src/backend/InvenTree/generic/states/states.py +++ b/src/backend/InvenTree/generic/states/states.py @@ -301,6 +301,13 @@ class StatusCodeMixin: """Return the custom status code for this object.""" return getattr(self, f'{self.STATUS_FIELD}_custom_key', None) + def compare_status(self, status: int) -> bool: + """Determine if the current status matches the provided status code.""" + if status == self.get_status(): + return True + + 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.""" if not self.status_class: diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 175519b093..7ad96e62df 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -1067,6 +1067,11 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView): # Do this regardless of results above data.pop('use_pack_size', None) + # Extract 'status' flag from data + status_raw = data.pop('status', None) + status_custom = data.pop('status_custom_key', None) + status_value = status_custom or status_raw + # Assign serial numbers for a trackable part if serial_numbers: if not part.trackable: @@ -1130,10 +1135,14 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView): tracking = [] for item in items: + if status_value and not item.compare_status(status_value): + item.set_status(status_value) + item.save() + if entry := item.add_tracking_entry( StockHistoryCode.CREATED, user, - deltas={'status': item.status}, + deltas={'status': status_value}, location=location, quantity=float(item.quantity), commit=False, @@ -1152,6 +1161,10 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView): # Create a single StockItem object # Note: This automatically creates a tracking entry item = serializer.save() + + if status_value and not item.compare_status(status_value): + item.set_status(status_value) + item.save(user=user) response_data = serializer.data diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 9cbacada67..3b66f6ecfb 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -1332,9 +1332,10 @@ class StockItem( self.sales_order = None self.location = location - if status := kwargs.get('status'): - self.status = status - tracking_info['status'] = status + if status := kwargs.pop('status', None): + if not self.compare_status(status): + self.set_status(status) + tracking_info['status'] = status self.save() @@ -2240,7 +2241,7 @@ class StockItem( status = kwargs.pop('status', None) or kwargs.pop('status_custom_key', None) - if status and status != self.status: + if status and not self.compare_status(status): self.set_status(status) tracking_info['status'] = status @@ -2325,7 +2326,7 @@ class StockItem( status = kwargs.pop('status', None) or kwargs.pop('status_custom_key', None) - if status and status != self.status: + if status and not self.compare_status(status): self.set_status(status) tracking_info['status'] = status @@ -2388,7 +2389,7 @@ class StockItem( status = kwargs.pop('status', None) or kwargs.pop('status_custom_key', None) - if status and status != self.status: + if status and not self.compare_status(status): self.set_status(status) tracking_info['status'] = status @@ -2442,7 +2443,7 @@ class StockItem( status = kwargs.pop('status', None) or kwargs.pop('status_custom_key', None) - if status and status != self.status: + if status and not self.compare_status(status): self.set_status(status) deltas['status'] = status diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 58c1d0a22e..9c3947d53e 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -521,7 +521,17 @@ class StockItemSerializer( """Custom update method to pass the user information through to the instance.""" instance._user = self.context['user'] - return super().update(instance, validated_data) + status_custom_key = validated_data.pop('status_custom_key', None) + status = validated_data.pop('status', None) + + instance = super().update(instance, validated_data=validated_data) + + if status_code := status_custom_key or status: + if not instance.compare_status(status_code): + instance.set_status(status_code) + instance.save() + + return instance @staticmethod def annotate_queryset(queryset): @@ -1123,7 +1133,6 @@ class StockChangeStatusSerializer(serializers.Serializer): note = data.get('note', '') - items_to_update = [] transaction_notes = [] deltas = {'status': status} @@ -1135,12 +1144,11 @@ class StockChangeStatusSerializer(serializers.Serializer): for item in items: # Ignore items which are already in the desired status - if item.status == status: + if item.compare_status(status): continue - item.updated = now - item.status = status - items_to_update.append(item) + item.set_status(status) + item.save(add_note=False) # Create a new transaction note for each item transaction_notes.append( @@ -1154,10 +1162,7 @@ class StockChangeStatusSerializer(serializers.Serializer): ) ) - # Update status - StockItem.objects.bulk_update(items_to_update, ['status', 'updated']) - - # Create entries + # Create tracking entries StockItemTracking.objects.bulk_create(transaction_notes) diff --git a/src/backend/InvenTree/stock/test_views.py b/src/backend/InvenTree/stock/test_views.py index bd6fdb3af4..035349cb59 100644 --- a/src/backend/InvenTree/stock/test_views.py +++ b/src/backend/InvenTree/stock/test_views.py @@ -11,7 +11,7 @@ from users.models import Owner class StockViewTestCase(InvenTreeTestCase): - """Mixin for Stockview tests.""" + """Mixin for StockView tests.""" fixtures = ['category', 'part', 'company', 'location', 'supplier_part', 'stock'] @@ -46,7 +46,7 @@ class StockOwnershipTest(StockViewTestCase): """Helper function to get response to API change.""" return self.client.patch( reverse('api-stock-detail', args=(self.test_item_id,)), - {'status': StockStatus.DAMAGED.value}, + {'status_custom_key': StockStatus.DAMAGED.value}, content_type='application/json', ) @@ -96,7 +96,7 @@ class StockOwnershipTest(StockViewTestCase): self.assertTrue(location.check_ownership(self.user)) # Owner is group -> True self.assertContains( self.assert_api_change(), - f'"status":{StockStatus.DAMAGED.value}', + f'"status_custom_key":{StockStatus.DAMAGED.value}', status_code=200, )