From 321ecc39d490c03bbd7ac161e166bc449696cd11 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 13 Nov 2026 17:19:20 +1100 Subject: [PATCH] Mark build fields as read-only (#13265) --- .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/stock/serializers.py | 2 + src/backend/InvenTree/stock/test_api.py | 64 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index f7d8dab409..45540349b1 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 577 +INVENTREE_API_VERSION = 578 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v578 -> 2026-11-13 : https://github.com/inventree/InvenTree/pull/13265 + - The "is_building" and "build" fields on the StockItem API are now read-only - build outputs must be created, completed or scrapped via the build order endpoints + v577 -> 2026-11-12 : https://github.com/inventree/InvenTree/pull/13236 - non-function doc changes from allauth diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 77250e553a..9da237e2bb 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -401,7 +401,9 @@ class StockItemSerializer( read_only_fields = [ 'allocated', 'barcode_hash', + 'build', 'creation_date', + 'is_building', 'stocktake_date', 'stocktake_user', 'updated', diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 80aef67332..9d3c8cf39b 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -2215,6 +2215,70 @@ class StockItemTest(StockAPITestCase): data = self.get(url).data self.assertEqual(data['purchase_price_currency'], 'NZD') + def test_build_fields_read_only(self): + """The 'is_building' and 'build' fields cannot be changed via the generic stock item API. + + Build outputs must be completed (or scrapped) via the build order endpoints, + otherwise the build order 'completed' count, stock allocations and + tracking history are bypassed. + """ + assembly = Part.objects.filter(assembly=True).first() + self.assertIsNotNone(assembly) + + bo = build.models.Build.objects.create( + part=assembly, reference=build.models.Build.generate_reference(), quantity=5 + ) + + output = bo.create_build_output(5).first() + self.assertTrue(output.is_building) + + url = reverse('api-stock-detail', kwargs={'pk': output.pk}) + + # Attempting to mark the output as complete is silently ignored + data = self.patch(url, {'is_building': False}, expected_code=200).data + self.assertTrue(data['is_building']) + + output.refresh_from_db() + self.assertTrue(output.is_building) + + bo.refresh_from_db() + self.assertEqual(bo.completed, 0) + + # The source build cannot be changed or removed + other_bo = build.models.Build.objects.create( + part=assembly, reference=build.models.Build.generate_reference(), quantity=5 + ) + + for value in [other_bo.pk, None]: + data = self.patch(url, {'build': value}, expected_code=200).data + self.assertEqual(data['build'], bo.pk) + + output.refresh_from_db() + self.assertEqual(output.build, bo) + + # Nor can a new stock item be created as "in production" or against a build + response = self.post( + reverse('api-stock-list'), + { + 'part': assembly.pk, + 'quantity': 3, + 'location': 1, + 'is_building': True, + 'build': bo.pk, + }, + expected_code=201, + ) + + for item in response.data: + item = StockItem.objects.get(pk=item['pk']) + self.assertFalse(item.is_building) + self.assertIsNone(item.build) + + # And an existing stock item cannot be assigned to a build + item_url = reverse('api-stock-detail', kwargs={'pk': 1}) + data = self.patch(item_url, {'build': bo.pk}, expected_code=200).data + self.assertIsNone(data['build']) + def test_output_options(self): """Test the output options for StockItemt detail.""" self.assignRole('build.view')