From d024a3fe2fb9535a309b3f575a3019f9d18d82d8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 1 Aug 2026 20:35:11 +1000 Subject: [PATCH] Fix for N+1 issues in stock item serializing (#12534) --- src/backend/InvenTree/stock/api.py | 10 +++++++++- src/backend/InvenTree/stock/models.py | 14 +++++++++++++- src/backend/InvenTree/stock/test_api.py | 3 +-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 529f7a4f13..994d039c85 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -143,8 +143,16 @@ class StockItemSerialize(StockItemContextMixin, CreateAPI): queryset = StockSerializers.StockItemSerializer.annotate_queryset(items) + # Apply any additional prefetching required by optional fields which end up + # included in this response (e.g. 'tags', 'tests') - mirrors what + # OutputOptionsMixin.get_queryset() does for a normal list/retrieve request, + # which this manually-constructed response bypasses + context = self.get_serializer_context() + probe_serializer = StockSerializers.StockItemSerializer(context=context) + queryset = probe_serializer.prefetch_queryset(queryset) + response = StockSerializers.StockItemSerializer( - queryset, many=True, context=self.get_serializer_context() + queryset, many=True, context=context ) return Response(response.data, status=status.HTTP_201_CREATED) diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 5db2235d21..03820804ae 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -2489,7 +2489,18 @@ class StockItem( # Create a new tracking entry for each item history_items = [] + # Every new item shares this exact part - avoid a redundant per-item + # part_id -> Part lookup (bulk_create_and_fetch() does not preserve the + # cached FK) by seeding each item's cache with the instance we already have + part = self.part + + # Check (once) whether there are any test results to copy across, rather + # than re-querying an identical (and usually empty) result set per item + has_test_results = self.test_results.exists() + for item in items: + item.part = part + # Construct tracking entries for the new StockItem if entry := item.add_tracking_entry( StockHistoryCode.SPLIT_FROM_PARENT, @@ -2512,7 +2523,8 @@ class StockItem( history_items.append(entry) # Copy any test results from this item to the new one - item.copyTestResultsFrom(self) + if has_test_results: + item.copyTestResultsFrom(self) StockItemTracking.objects.bulk_create(history_items, batch_size=250) diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 2b270ffe91..2de3825a25 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -1702,9 +1702,8 @@ class StockItemTest(StockAPITestCase): with self.settings( PLUGIN_TESTING_EVENTS=True, PLUGIN_TESTING_EVENTS_ASYNC=True ): - # TODO: 2026-07-12 : Refactor this API call response = self.post( - url, data, max_query_count=1300, benchmark=True, format='json' + url, data, max_query_count=150, benchmark=True, format='json' ) self.assertEqual(response.status_code, 201)