From 9f715337ec940cfb57bfba876abdddda660d987b Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 15 Jul 2025 00:00:38 +1000 Subject: [PATCH] Stock serialize tweaks (#10017) * Better item extraction * Improve query efficiency * Further queryset improvements * Return correct data format * Test with hugh number of serials * Improve serialization UX * Revert changes to unit tests --- src/backend/InvenTree/build/api.py | 3 ++- src/backend/InvenTree/build/models.py | 3 ++- src/backend/InvenTree/stock/api.py | 9 +++++-- src/backend/InvenTree/stock/serializers.py | 27 ++++++++++++++++---- src/frontend/src/hooks/UseInstance.tsx | 9 +++++++ src/frontend/src/pages/stock/StockDetail.tsx | 14 +++++----- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 00e9fa606b..42e5f4f1b2 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -659,7 +659,8 @@ class BuildOutputCreate(BuildOrderContextMixin, CreateAPI): # Create the build output(s) outputs = serializer.save() - response = stock.serializers.StockItemSerializer(outputs, many=True) + queryset = stock.serializers.StockItemSerializer.annotate_queryset(outputs) + response = stock.serializers.StockItemSerializer(queryset, many=True) # Return the created outputs return Response(response.data, status=status.HTTP_201_CREATED) diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index 43411f2e9d..da2bc233bf 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -1009,7 +1009,8 @@ class Build( }, ) - outputs = [output] + # Ensure we return a QuerySet object here, too + outputs = stock.models.StockItem.objects.filter(pk=output.pk) if self.status == BuildStatus.PENDING: self.status = BuildStatus.PRODUCTION.value diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 7ad96e62df..5407e76408 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -129,8 +129,10 @@ class StockItemSerialize(StockItemContextMixin, CreateAPI): # Perform the actual serialization step items = serializer.save() + queryset = StockSerializers.StockItemSerializer.annotate_queryset(items) + response = StockSerializers.StockItemSerializer( - items, many=True, context=self.get_serializer_context() + queryset, many=True, context=self.get_serializer_context() ) return Response(response.data, status=status.HTTP_201_CREATED) @@ -1151,8 +1153,11 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView): StockItemTracking.objects.bulk_create(tracking) + # Annotate the stock items with part information + queryset = StockSerializers.StockItemSerializer.annotate_queryset(items) + response = StockSerializers.StockItemSerializer( - items, many=True, context=self.get_serializer_context() + queryset, many=True, context=self.get_serializer_context() ) response_data = response.data diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 55c14640e2..4fea859db9 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -696,7 +696,10 @@ class SerializeStockItemSerializer(serializers.Serializer): def validate_quantity(self, quantity): """Validate that the quantity value is correct.""" - item = self.context['item'] + item = self.context.get('item') + + if not item: + raise ValidationError(_('No stock item provided')) if quantity < 0: raise ValidationError(_('Quantity must be greater than zero')) @@ -736,7 +739,10 @@ class SerializeStockItemSerializer(serializers.Serializer): """Check that the supplied serial numbers are valid.""" data = super().validate(data) - item = self.context['item'] + item = self.context.get('item') + + if not item: + raise ValidationError(_('No stock item provided')) if not item.part.trackable: raise ValidationError(_('Serial numbers cannot be assigned to this part')) @@ -771,7 +777,11 @@ class SerializeStockItemSerializer(serializers.Serializer): Returns: A list of StockItem objects that were created as a result of the serialization. """ - item = self.context['item'] + item = self.context.get('item') + + if not item: + raise ValidationError(_('No stock item provided')) + request = self.context.get('request') user = request.user if request else None @@ -905,7 +915,10 @@ class UninstallStockItemSerializer(serializers.Serializer): def save(self): """Uninstall stock item.""" - item = self.context['item'] + item = self.context.get('item') + + if not item: + raise ValidationError(_('No stock item provided')) data = self.validated_data request = self.context['request'] @@ -1035,7 +1048,11 @@ class ReturnStockItemSerializer(serializers.Serializer): def save(self): """Save the serializer to return the item into stock.""" - item = self.context['item'] + item = self.context.get('item') + + if not item: + raise ValidationError(_('No stock item provided')) + request = self.context['request'] data = self.validated_data diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx index 34a766f921..9caf729aea 100644 --- a/src/frontend/src/hooks/UseInstance.tsx +++ b/src/frontend/src/hooks/UseInstance.tsx @@ -66,6 +66,15 @@ export function useInstance({ JSON.stringify(pathParams), disabled ], + retry: (failureCount, error: any) => { + // If it's a 404, don't retry + if (error.response?.status == 404) { + return false; + } + + // Otherwise, retry up to 3 times + return failureCount < 3; + }, queryFn: async () => { if (disabled) { return defaultValue; diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 98f7c4573f..c8fb9d50be 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -744,12 +744,14 @@ export default function StockDetail() { quantity: stockitem.quantity, destination: stockitem.location ?? stockitem.part_detail?.default_location }, - onFormSuccess: () => { - const partId = stockitem.part; - refreshInstancePromise().catch(() => { - // Part may have been deleted - redirect to the part detail page - navigate(getDetailUrl(ModelType.part, partId)); - }); + onFormSuccess: (response: any) => { + if (response.length >= stockitem.quantity) { + // Entire item was serialized + // Navigate to the first result + navigate(getDetailUrl(ModelType.stockitem, response[0].pk)); + } else { + refreshInstance(); + } }, successMessage: t`Stock item serialized` });