From b64182b565e5de9a602a4efe839613425b78afdb Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 13 Jul 2026 20:21:56 +1000 Subject: [PATCH] Uninstall fix (#12378) * Prevent uninstall into structural location * Regression tests --- src/backend/InvenTree/stock/models.py | 5 ++++ src/backend/InvenTree/stock/serializers.py | 9 ++++++ src/backend/InvenTree/stock/test_api.py | 14 ++++++++++ src/backend/InvenTree/stock/tests.py | 32 ++++++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 029a871f2f..73042aeacf 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -1760,6 +1760,11 @@ class StockItem( if self.belongs_to is None: return False + if location and location.structural: + raise ValidationError({ + 'location': _('Cannot assign stock to structural location') + }) + # Add a transaction note to the parent item self.belongs_to.add_tracking_entry( StockHistoryCode.REMOVED_CHILD_ITEM, diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 03764b8cf2..1d5f4fc029 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -938,6 +938,15 @@ class UninstallStockItemSerializer(serializers.Serializer): allow_blank=True, ) + def validate_location(self, location): + """Validate the provided location.""" + if location and location.structural: + raise ValidationError( + _('Structural locations cannot be assigned stock items') + ) + + return location + def save(self): """Uninstall stock item.""" item = self.context.get('item') diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index e869f438ea..6fbcbd650b 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -1977,6 +1977,20 @@ class StockItemTest(StockAPITestCase): url = reverse('api-stock-item-uninstall', kwargs={'pk': sub_item.pk}) + # An uninstalled item cannot be moved into a structural location + structural = StockLocation.objects.create( + name='Structural location', structural=True + ) + + response = self.post(url, {'location': structural.pk}, expected_code=400) + + self.assertIn( + 'Structural locations cannot be assigned stock items', str(response.data) + ) + + sub_item.refresh_from_db() + self.assertEqual(sub_item.belongs_to, item) + self.post(url, {'location': 1}, expected_code=201) sub_item.refresh_from_db() diff --git a/src/backend/InvenTree/stock/tests.py b/src/backend/InvenTree/stock/tests.py index b79d32ba9b..63ed32321a 100644 --- a/src/backend/InvenTree/stock/tests.py +++ b/src/backend/InvenTree/stock/tests.py @@ -351,6 +351,38 @@ class StockTest(StockTestBase): stock.splitStock(stock.quantity, None, self.user) self.assertEqual(StockItem.objects.filter(part=3).count(), n + 1) + def test_uninstall_into_structural_location(self): + """Test that an item cannot be uninstalled into a structural location.""" + parent = StockItem.objects.get(pk=1) + + item = StockItem.objects.get(pk=2) + item.belongs_to = parent + item.save() + + n_entries = item.tracking_info.count() + n_parent_entries = parent.tracking_info.count() + + structural = StockLocation.objects.create( + name='Structural location', structural=True + ) + + with self.assertRaises(ValidationError): + item.uninstall_into_location(structural, self.user, 'Uninstalling') + + # The item remains installed, with no location change or tracking entries + item.refresh_from_db() + self.assertEqual(item.belongs_to, parent) + self.assertNotEqual(item.location, structural) + self.assertEqual(item.tracking_info.count(), n_entries) + self.assertEqual(parent.tracking_info.count(), n_parent_entries) + + # Uninstalling into a non-structural location is still permitted + item.uninstall_into_location(self.drawer2, self.user, 'Uninstalling') + + item.refresh_from_db() + self.assertIsNone(item.belongs_to) + self.assertEqual(item.location, self.drawer2) + def test_child_items(self): """Test the 'children' reverse relation and 'child_count' property.