diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index c8c54b3a43..c6c1c73128 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -282,6 +282,13 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView): 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() serializer_class = build.serializers.BuildOutputDeleteSerializer diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index ebd98fefc9..e4919f1433 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -199,7 +199,7 @@ class BuildOutputCreateSerializer(serializers.Serializer): def validate_quantity(self, quantity): - if quantity < 0: + if quantity <= 0: raise ValidationError(_("Quantity must be greater than zero")) part = self.get_part() @@ -209,7 +209,7 @@ class BuildOutputCreateSerializer(serializers.Serializer): if part.trackable: 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")) return quantity @@ -232,7 +232,6 @@ class BuildOutputCreateSerializer(serializers.Serializer): serial_numbers = serial_numbers.strip() - # TODO: Field level validation necessary here? return serial_numbers auto_allocate = serializers.BooleanField( diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index a54a92dda8..be64aa28c7 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -305,6 +305,215 @@ class BuildTest(BuildAPITest): 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): """