mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Merge pull request #3017 from SchrodingersGat/build-output-delete-fix
Bug Fix: Build output delete API endpoint
This commit is contained in:
		| @@ -282,6 +282,13 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView): | |||||||
|     API endpoint for deleting multiple build outputs |     API endpoint for deleting multiple build outputs | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     def get_serializer_context(self): | ||||||
|  |         ctx = super().get_serializer_context() | ||||||
|  |  | ||||||
|  |         ctx['to_complete'] = False | ||||||
|  |  | ||||||
|  |         return ctx | ||||||
|  |  | ||||||
|     queryset = Build.objects.none() |     queryset = Build.objects.none() | ||||||
|  |  | ||||||
|     serializer_class = build.serializers.BuildOutputDeleteSerializer |     serializer_class = build.serializers.BuildOutputDeleteSerializer | ||||||
|   | |||||||
| @@ -199,7 +199,7 @@ class BuildOutputCreateSerializer(serializers.Serializer): | |||||||
|  |  | ||||||
|     def validate_quantity(self, quantity): |     def validate_quantity(self, quantity): | ||||||
|  |  | ||||||
|         if quantity < 0: |         if quantity <= 0: | ||||||
|             raise ValidationError(_("Quantity must be greater than zero")) |             raise ValidationError(_("Quantity must be greater than zero")) | ||||||
|  |  | ||||||
|         part = self.get_part() |         part = self.get_part() | ||||||
| @@ -209,7 +209,7 @@ class BuildOutputCreateSerializer(serializers.Serializer): | |||||||
|             if part.trackable: |             if part.trackable: | ||||||
|                 raise ValidationError(_("Integer quantity required for trackable parts")) |                 raise ValidationError(_("Integer quantity required for trackable parts")) | ||||||
|  |  | ||||||
|             if part.has_trackable_parts(): |             if part.has_trackable_parts: | ||||||
|                 raise ValidationError(_("Integer quantity required, as the bill of materials contains trackable parts")) |                 raise ValidationError(_("Integer quantity required, as the bill of materials contains trackable parts")) | ||||||
|  |  | ||||||
|         return quantity |         return quantity | ||||||
| @@ -232,7 +232,6 @@ class BuildOutputCreateSerializer(serializers.Serializer): | |||||||
|  |  | ||||||
|         serial_numbers = serial_numbers.strip() |         serial_numbers = serial_numbers.strip() | ||||||
|  |  | ||||||
|         # TODO: Field level validation necessary here? |  | ||||||
|         return serial_numbers |         return serial_numbers | ||||||
|  |  | ||||||
|     auto_allocate = serializers.BooleanField( |     auto_allocate = serializers.BooleanField( | ||||||
|   | |||||||
| @@ -305,6 +305,215 @@ class BuildTest(BuildAPITest): | |||||||
|  |  | ||||||
|         self.assertEqual(bo.status, BuildStatus.CANCELLED) |         self.assertEqual(bo.status, BuildStatus.CANCELLED) | ||||||
|  |  | ||||||
|  |     def test_create_delete_output(self): | ||||||
|  |         """ | ||||||
|  |         Test that we can create and delete build outputs via the API | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         bo = Build.objects.get(pk=1) | ||||||
|  |  | ||||||
|  |         n_outputs = bo.output_count | ||||||
|  |  | ||||||
|  |         create_url = reverse('api-build-output-create', kwargs={'pk': 1}) | ||||||
|  |  | ||||||
|  |         # Attempt to create outputs with invalid data | ||||||
|  |         response = self.post( | ||||||
|  |             create_url, | ||||||
|  |             { | ||||||
|  |                 'quantity': 'not a number', | ||||||
|  |             }, | ||||||
|  |             expected_code=400 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertIn('A valid number is required', str(response.data)) | ||||||
|  |  | ||||||
|  |         for q in [-100, -10.3, 0]: | ||||||
|  |  | ||||||
|  |             response = self.post( | ||||||
|  |                 create_url, | ||||||
|  |                 { | ||||||
|  |                     'quantity': q, | ||||||
|  |                 }, | ||||||
|  |                 expected_code=400 | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             if q == 0: | ||||||
|  |                 self.assertIn('Quantity must be greater than zero', str(response.data)) | ||||||
|  |             else: | ||||||
|  |                 self.assertIn('Ensure this value is greater than or equal to 0', str(response.data)) | ||||||
|  |  | ||||||
|  |         # Mark the part being built as 'trackable' (requires integer quantity) | ||||||
|  |         bo.part.trackable = True | ||||||
|  |         bo.part.save() | ||||||
|  |  | ||||||
|  |         response = self.post( | ||||||
|  |             create_url, | ||||||
|  |             { | ||||||
|  |                 'quantity': 12.3, | ||||||
|  |             }, | ||||||
|  |             expected_code=400 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertIn('Integer quantity required for trackable parts', str(response.data)) | ||||||
|  |  | ||||||
|  |         # Erroneous serial numbers | ||||||
|  |         response = self.post( | ||||||
|  |             create_url, | ||||||
|  |             { | ||||||
|  |                 'quantity': 5, | ||||||
|  |                 'serial_numbers': '1, 2, 3, 4, 5, 6', | ||||||
|  |                 'batch': 'my-batch', | ||||||
|  |             }, | ||||||
|  |             expected_code=400 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertIn('Number of unique serial numbers (6) must match quantity (5)', str(response.data)) | ||||||
|  |  | ||||||
|  |         # At this point, no new build outputs should have been created | ||||||
|  |         self.assertEqual(n_outputs, bo.output_count) | ||||||
|  |  | ||||||
|  |         # Now, create with *good* data | ||||||
|  |         response = self.post( | ||||||
|  |             create_url, | ||||||
|  |             { | ||||||
|  |                 'quantity': 5, | ||||||
|  |                 'serial_numbers': '1, 2, 3, 4, 5', | ||||||
|  |                 'batch': 'my-batch', | ||||||
|  |             }, | ||||||
|  |             expected_code=201, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # 5 new outputs have been created | ||||||
|  |         self.assertEqual(n_outputs + 5, bo.output_count) | ||||||
|  |  | ||||||
|  |         # Attempt to create with identical serial numbers | ||||||
|  |         response = self.post( | ||||||
|  |             create_url, | ||||||
|  |             { | ||||||
|  |                 'quantity': 3, | ||||||
|  |                 'serial_numbers': '1-3', | ||||||
|  |             }, | ||||||
|  |             expected_code=400, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertIn('The following serial numbers already exist : 1,2,3', str(response.data)) | ||||||
|  |  | ||||||
|  |         # Double check no new outputs have been created | ||||||
|  |         self.assertEqual(n_outputs + 5, bo.output_count) | ||||||
|  |  | ||||||
|  |         # Now, let's delete each build output individually via the API | ||||||
|  |         outputs = bo.build_outputs.all() | ||||||
|  |  | ||||||
|  |         delete_url = reverse('api-build-output-delete', kwargs={'pk': 1}) | ||||||
|  |  | ||||||
|  |         response = self.post( | ||||||
|  |             delete_url, | ||||||
|  |             { | ||||||
|  |                 'outputs': [], | ||||||
|  |             }, | ||||||
|  |             expected_code=400 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertIn('A list of build outputs must be provided', str(response.data)) | ||||||
|  |  | ||||||
|  |         # Mark 1 build output as complete | ||||||
|  |         bo.complete_build_output(outputs[0], self.user) | ||||||
|  |  | ||||||
|  |         self.assertEqual(n_outputs + 5, bo.output_count) | ||||||
|  |         self.assertEqual(1, bo.complete_count) | ||||||
|  |  | ||||||
|  |         # Delete all outputs at once | ||||||
|  |         # Note: One has been completed, so this should fail! | ||||||
|  |         response = self.post( | ||||||
|  |             delete_url, | ||||||
|  |             { | ||||||
|  |                 'outputs': [ | ||||||
|  |                     { | ||||||
|  |                         'output': output.pk, | ||||||
|  |                     } for output in outputs | ||||||
|  |                 ] | ||||||
|  |             }, | ||||||
|  |             expected_code=400 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertIn('This build output has already been completed', str(response.data)) | ||||||
|  |  | ||||||
|  |         # No change to the build outputs | ||||||
|  |         self.assertEqual(n_outputs + 5, bo.output_count) | ||||||
|  |         self.assertEqual(1, bo.complete_count) | ||||||
|  |  | ||||||
|  |         # Let's delete 2 build outputs | ||||||
|  |         response = self.post( | ||||||
|  |             delete_url, | ||||||
|  |             { | ||||||
|  |                 'outputs': [ | ||||||
|  |                     { | ||||||
|  |                         'output': output.pk, | ||||||
|  |                     } for output in outputs[1:3] | ||||||
|  |                 ] | ||||||
|  |             }, | ||||||
|  |             expected_code=201 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Two build outputs have been removed | ||||||
|  |         self.assertEqual(n_outputs + 3, bo.output_count) | ||||||
|  |         self.assertEqual(1, bo.complete_count) | ||||||
|  |  | ||||||
|  |         # Tests for BuildOutputComplete serializer | ||||||
|  |         complete_url = reverse('api-build-output-complete', kwargs={'pk': 1}) | ||||||
|  |  | ||||||
|  |         # Let's mark the remaining outputs as complete | ||||||
|  |         response = self.post( | ||||||
|  |             complete_url, | ||||||
|  |             { | ||||||
|  |                 'outputs': [], | ||||||
|  |                 'location': 4, | ||||||
|  |             }, | ||||||
|  |             expected_code=400, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertIn('A list of build outputs must be provided', str(response.data)) | ||||||
|  |  | ||||||
|  |         for output in outputs[3:]: | ||||||
|  |             output.refresh_from_db() | ||||||
|  |             self.assertTrue(output.is_building) | ||||||
|  |  | ||||||
|  |         response = self.post( | ||||||
|  |             complete_url, | ||||||
|  |             { | ||||||
|  |                 'outputs': [ | ||||||
|  |                     { | ||||||
|  |                         'output': output.pk | ||||||
|  |                     } for output in outputs[3:] | ||||||
|  |                 ], | ||||||
|  |                 'location': 4, | ||||||
|  |             }, | ||||||
|  |             expected_code=201, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Check that the outputs have been completed | ||||||
|  |         self.assertEqual(3, bo.complete_count) | ||||||
|  |  | ||||||
|  |         for output in outputs[3:]: | ||||||
|  |             output.refresh_from_db() | ||||||
|  |             self.assertEqual(output.location.pk, 4) | ||||||
|  |             self.assertFalse(output.is_building) | ||||||
|  |  | ||||||
|  |         # Try again, with an output which has already been completed | ||||||
|  |         response = self.post( | ||||||
|  |             complete_url, | ||||||
|  |             { | ||||||
|  |                 'outputs': [ | ||||||
|  |                     { | ||||||
|  |                         'output': outputs.last().pk, | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             }, | ||||||
|  |             expected_code=400, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertIn('This build output has already been completed', str(response.data)) | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildAllocationTest(BuildAPITest): | class BuildAllocationTest(BuildAPITest): | ||||||
|     """ |     """ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user