From 81fbf52c60cd6261e4d51907f63fad162fab2fc4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 May 2026 16:14:49 +1000 Subject: [PATCH] Delete output check (#11881) * Allow deletion of serialized build output - If calling "delete_output" on a BuildOrder - Only will delete an in_production output - Ignore the global setting in this case Co-authored-by: Copilot * Add unit test Co-authored-by: Copilot --------- Co-authored-by: Copilot --- src/backend/InvenTree/build/models.py | 4 ++- src/backend/InvenTree/build/test_api.py | 39 +++++++++++++++++++++++++ src/backend/InvenTree/stock/models.py | 12 ++++++-- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index 24b3914209..9bd92609eb 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -1033,7 +1033,9 @@ class Build( self.deallocate_stock(output=output) # Remove the build output from the database - output.delete() + # This is a special case where serialized stock can be deleted, + # independedent of the global setting which normally prevents deletion of serialized stock items + output.delete(ignore_serial_check=True) @transaction.atomic def trim_allocated_stock(self): diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index b7aa4a9cbd..706332e3fd 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -9,6 +9,7 @@ from rest_framework import status from build.models import Build, BuildItem, BuildLine from build.status_codes import BuildStatus +from common.settings import set_global_setting from InvenTree.unit_test import InvenTreeAPITestCase from part.models import BomItem, Part from stock.models import StockItem @@ -1548,6 +1549,44 @@ class BuildOutputScrapTest(BuildAPITest): self.assertFalse(completed_output.is_building) +class BuildOutputCancelTest(BuildAPITest): + """Test cancellation of build outputs.""" + + def test_cancel_output(self): + """Test cancellation of a build output.""" + build = Build.objects.get(pk=1) + build.part.trackable = True + build.part.save() + + N = build.build_outputs.count() + + # Create outputs + outputs = build.create_build_output(2, serials=['101', '202']) + self.assertEqual(outputs.count(), 2) + self.assertEqual(build.build_outputs.count(), N + 2) + + output_ids = list(outputs.values_list('pk', flat=True)) + + # Let's cancel one of the outputs + set_global_setting('STOCK_ALLOW_DELETE_SERIALIZED', True) + url = reverse('api-build-output-delete', kwargs={'pk': build.pk}) + + self.post(url, data={'outputs': [{'output': output_ids[0]}]}, expected_code=201) + + # Prevent deletion of serialized stock items, and try again + # Note that this should still succeed, independent of the global setting + set_global_setting('STOCK_ALLOW_DELETE_SERIALIZED', False) + + self.post(url, data={'outputs': [{'output': output_ids[1]}]}, expected_code=201) + + # The outputs should have been scrapped + self.assertEqual(build.build_outputs.count(), N) + + for pk in output_ids: + self.assertFalse(StockItem.objects.filter(pk=pk).exists()) + self.get(reverse('api-stock-detail', kwargs={'pk': pk}), expected_code=404) + + class BuildLineTests(BuildAPITest): """Unit tests for the BuildLine API endpoints.""" diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index e841cf554e..2c8e4d48bb 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -449,9 +449,15 @@ class StockItem( order_insertion_by = ['part'] - def delete(self, **kwargs): - """Custom delete method for StockItem model.""" - if not get_global_setting('STOCK_ALLOW_DELETE_SERIALIZED', cache=False): + def delete(self, ignore_serial_check: bool = False, **kwargs): + """Custom delete method for StockItem model. + + Arguments: + ignore_serial_check: If True, allow deletion of serialized stock items regardless of global setting + """ + if not ignore_serial_check and not get_global_setting( + 'STOCK_ALLOW_DELETE_SERIALIZED', cache=False + ): if self.serialized: raise ValidationError(_('Serialized stock items cannot be deleted'))