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'))