diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 0c0efea64d..d53956cbfb 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -218,6 +218,9 @@ class BuildAllocate(generics.CreateAPIView): return build def get_serializer_context(self): + """ + Provide the Build object to the serializer context + """ context = super().get_serializer_context() @@ -225,61 +228,6 @@ class BuildAllocate(generics.CreateAPIView): return context - def create(self, request, *args, **kwargs): - - # Which build are we receiving against? - build = self.get_build() - - # Validate the serialized data - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - # Allocate the stock items - try: - self.allocate_items(build, serializer) - except DjangoValidationError as exc: - # Re-throw a django error as a DRF error - raise ValidationError(detail=serializers.as_serializer_error(exc)) - - headers = self.get_success_headers(serializer.data) - - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - - @transaction.atomic - def allocate_items(self, build, serializer): - """ - Allocate the provided stock items to this order. - - At this point, most of the heavy lifting has been done for us by the DRF serializer. - - We have a list of "items" each a dict containing: - - - bom_item: A validated BomItem object which matches this build - - stock_item: A validated StockItem object which matches the bom_item - - quantity: A validated numerical quantity which does not exceed the available stock - - output: A validated StockItem object to assign stock against (optional) - - """ - - data = serializer.validated_data - - items = data.get('items', []) - - for item in items: - - bom_item = item['bom_item'] - stock_item = item['stock_item'] - quantity = item['quantity'] - output = item.get('output', None) - - # Create a new BuildItem to allocate stock - build_item = BuildItem.objects.create( - build=build, - stock_item=stock_item, - quantity=quantity, - install_into=output - ) - class BuildItemList(generics.ListCreateAPIView): """ API endpoint for accessing a list of BuildItem objects diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 084f9ab2db..3224554156 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -1190,28 +1190,6 @@ class BuildItem(models.Model): super().save() - def validate_unique(self, exclude=None): - """ - Test that this BuildItem object is "unique". - Essentially we do not want a stock_item being allocated to a Build multiple times. - """ - - super().validate_unique(exclude) - - items = BuildItem.objects.exclude(id=self.id).filter( - build=self.build, - stock_item=self.stock_item, - install_into=self.install_into - ) - - if items.exists(): - msg = _("BuildItem must be unique for build, stock_item and install_into") - raise ValidationError({ - 'build': msg, - 'stock_item': msg, - 'install_into': msg - }) - def clean(self): """ Check validity of the BuildItem model. The following checks are performed: diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 536aced1a2..adbf7361eb 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -5,6 +5,8 @@ JSON serializers for Build API # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.db import transaction + from django.utils.translation import ugettext_lazy as _ from django.db.models import Case, When, Value @@ -16,6 +18,8 @@ from rest_framework.serializers import ValidationError from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief +import InvenTree.helpers + from stock.models import StockItem from stock.serializers import StockItemSerializerBrief, LocationSerializer @@ -147,6 +151,13 @@ class BuildAllocationItemSerializer(serializers.Serializer): label=_('Stock Item'), ) + def validate_stock_item(self, stock_item): + + if not stock_item.in_stock: + raise ValidationError(_("Item must be in stock")) + + return stock_item + quantity = serializers.DecimalField( max_digits=15, decimal_places=5, @@ -177,36 +188,45 @@ class BuildAllocationItemSerializer(serializers.Serializer): 'output', ] - def is_valid(self, raise_exception=False): + def validate(self, data): - if super().is_valid(raise_exception): + super().validate(data) - data = self.validated_data + bom_item = data['bom_item'] + stock_item = data['stock_item'] + quantity = data['quantity'] + output = data.get('output', None) - bom_item = data['bom_item'] - stock_item = data['stock_item'] - quantity = data['quantity'] - output = data.get('output', None) + build = self.context['build'] - build = self.context['build'] + # TODO: Check that the "stock item" is valid for the referenced "sub_part" + # Note: Because of allow_variants options, it may not be a direct match! - # TODO: Check that the "stock item" is valid for the referenced "sub_part" - # Note: Because of allow_variants options, it may not be a direct match! + # Check that the quantity does not exceed the available amount from the stock item + q = stock_item.unallocated_quantity() - # TODO: Check that the quantity does not exceed the available amount from the stock item + if quantity > q: - # Output *must* be set for trackable parts - if output is None and bom_item.sub_part.trackable: - self._errors['output'] = _('Build output must be specified for allocation of tracked parts') + q = InvenTree.helpers.clean_decimal(q) - # Output *cannot* be set for un-tracked parts - if output is not None and not bom_item.sub_part.trackable: - self._errors['output'] = _('Build output cannot be specified for allocation of untracked parts') + raise ValidationError({ + 'quantity': _(f"Available quantity ({q}) exceeded") + }) + + # Output *must* be set for trackable parts + if output is None and bom_item.sub_part.trackable: + raise ValidationError({ + 'output': _('Build output must be specified for allocation of tracked parts') + }) + + # Output *cannot* be set for un-tracked parts + if output is not None and not bom_item.sub_part.trackable: - if self._errors and raise_exception: - raise ValidationError(self.errors) + raise ValidationError({ + 'output': _('Build output cannot be specified for allocation of untracked parts') + }) - return not bool(self._errors) + return data class BuildAllocationSerializer(serializers.Serializer): @@ -221,24 +241,56 @@ class BuildAllocationSerializer(serializers.Serializer): 'items', ] - def is_valid(self, raise_exception=False): + def validate(self, data): """ Validation """ - - super().is_valid(raise_exception) - - data = self.validated_data + + super().validate(data) items = data.get('items', []) if len(items) == 0: - self._errors['items'] = _('Allocation items must be provided') + raise ValidationError(_('Allocation items must be provided')) - if self._errors and raise_exception: - raise ValidationError(self.errors) + return data - return not bool(self._errors) + def save(self): + print("creating new allocation items!") + + data = self.validated_data + + print("data:") + print(data) + + items = data.get('items', []) + + print("items:") + print(items) + + build = self.context['build'] + + created_items = [] + + with transaction.atomic(): + for item in items: + bom_item = item['bom_item'] + stock_item = item['stock_item'] + quantity = item['quantity'] + output = item.get('output', None) + + # Create a new BuildItem to allocate stock + build_item = BuildItem.objects.create( + build=build, + bom_item=bom_item, + stock_item=stock_item, + quantity=quantity, + install_into=output + ) + + created_items.append(build_item) + + return created_items class BuildItemSerializer(InvenTreeModelSerializer): diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index b5e5406f69..dc44c3c6c8 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -252,34 +252,6 @@ class TestBuildViews(TestCase): self.assertIn(build.title, content) - def test_build_item_create(self): - """ Test the BuildItem creation view (ajax form) """ - - url = reverse('build-item-create') - - # Try without a part specified - response = self.client.get(url, {'build': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # Try with an invalid build ID - response = self.client.get(url, {'build': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # Try with a valid part specified - response = self.client.get(url, {'build': 1, 'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # Try with an invalid part specified - response = self.client.get(url, {'build': 1, 'part': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - def test_build_item_edit(self): - """ Test the BuildItem edit view (ajax form) """ - - # TODO - # url = reverse('build-item-edit') - pass - def test_build_output_complete(self): """ Test the build output completion form diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 9814dc83f7..7246e48d9a 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -25,7 +25,6 @@ build_urls = [ url('^edit/', views.BuildItemEdit.as_view(), name='build-item-edit'), url('^delete/', views.BuildItemDelete.as_view(), name='build-item-delete'), ])), - url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'), ])), url(r'^(?P\d+)/', include(build_detail_urls)), diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index da2d23cd0d..91460e5961 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -276,35 +276,30 @@ class POReceiveSerializer(serializers.Serializer): help_text=_('Select destination location for received items'), ) - def is_valid(self, raise_exception=False): + def validate(self, data): - super().is_valid(raise_exception) - - # Custom validation - data = self.validated_data + super().validate(data) items = data.get('items', []) if len(items) == 0: - self._errors['items'] = _('Line items must be provided') - else: - # Ensure barcodes are unique - unique_barcodes = set() + raise ValidationError({ + 'items': _('Line items must be provided') + }) - for item in items: - barcode = item.get('barcode', '') + # Ensure barcodes are unique + unique_barcodes = set() - if barcode: - if barcode in unique_barcodes: - self._errors['items'] = _('Supplied barcode values must be unique') - break - else: - unique_barcodes.add(barcode) + for item in items: + barcode = item.get('barcode', '') - if self._errors and raise_exception: - raise ValidationError(self.errors) + if barcode: + if barcode in unique_barcodes: + raise ValidationError(_('Supplied barcode values must be unique')) + else: + unique_barcodes.add(barcode) - return not bool(self._errors) + return data class Meta: fields = [