From dc53a433a7cd62a8aef6024bcb99b25aa02f0f55 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 23 Aug 2021 23:35:22 +1000 Subject: [PATCH] Fix serializer nesting - Add new API endpoint to receive items - Add unit testing --- InvenTree/order/api.py | 61 ++++++++++++++++++++++++++++--- InvenTree/order/serializers.py | 18 ++++++--- InvenTree/order/test_api.py | 67 ++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 10 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index a834989fd9..40819a3bff 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -11,6 +11,9 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics from rest_framework import filters, status from rest_framework.response import Response +from rest_framework.serializers import ValidationError + +from django.utils.translation import ugettext_lazy as _ from InvenTree.helpers import str2bool from InvenTree.api import AttachmentMixin @@ -27,6 +30,7 @@ from .models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation from .models import SalesOrderAttachment from .serializers import SalesOrderSerializer, SOLineItemSerializer, SOAttachmentSerializer from .serializers import SalesOrderAllocationSerializer +from .serializers import POReceiveSerializer, POLineItemReceiveSerializer class POList(generics.ListCreateAPIView): @@ -204,6 +208,41 @@ class PODetail(generics.RetrieveUpdateDestroyAPIView): return queryset +class POReceive(generics.CreateAPIView): + """ + API endpoint to receive stock items against a purchase order. + + - The purchase order is specified in the URL. + - Items to receive are specified as a list called "items" with the following options: + - supplier_part: pk value of the supplier part + - quantity: quantity to receive + - status: stock item status + - location: destination for stock item (optional) + - A global location can also be specified + """ + + queryset = PurchaseOrderLineItem.objects.none() + + serializer_class = POReceiveSerializer + + def get_order(self): + """ + Returns the PurchaseOrder associated with this API endpoint + """ + + order = PurchaseOrder.objects.get(pk=self.kwargs['pk']) + + return order + + def create(self, request, *args, **kwargs): + # Validate the serialized data + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + class POLineItemList(generics.ListCreateAPIView): """ API endpoint for accessing a list of POLineItem objects @@ -616,13 +655,25 @@ class POAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin) order_api_urls = [ + # API endpoints for purchase orders - url(r'po/attachment/', include([ - url(r'^(?P\d+)/$', POAttachmentDetail.as_view(), name='api-po-attachment-detail'), - url(r'^.*$', POAttachmentList.as_view(), name='api-po-attachment-list'), + url(r'^po/', include([ + + # Purchase order attachments + url(r'attachment/', include([ + url(r'^(?P\d+)/$', POAttachmentDetail.as_view(), name='api-po-attachment-detail'), + url(r'^.*$', POAttachmentList.as_view(), name='api-po-attachment-list'), + ])), + + # Individual purchase order detail URLs + url(r'^(?P\d+)/', include([ + url(r'^receive/', POReceive.as_view(), name='api-po-receive'), + url(r'.*$', PODetail.as_view(), name='api-po-detail'), + ])), + + # Purchase order list + url(r'^.*$', POList.as_view(), name='api-po-list'), ])), - url(r'^po/(?P\d+)/$', PODetail.as_view(), name='api-po-detail'), - url(r'^po/.*$', POList.as_view(), name='api-po-list'), # API endpoints for purchase order line items url(r'^po-line/(?P\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'), diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index dd35eb3cb8..f090faf214 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -186,6 +186,13 @@ class POLineItemReceiveSerializer(serializers.Serializer): help_text=_('Select destination location for received items'), ) + quantity = serializers.DecimalField( + max_digits=15, + decimal_places=5, + min_value=0, + required=True, + ) + class Meta: fields = [ 'supplier_part', @@ -198,8 +205,8 @@ class POReceiveSerializer(serializers.Serializer): Serializer for receiving items against a purchase order """ - items = serializers.StringRelatedField( - many=True + items = POLineItemReceiveSerializer( + many=True, ) location = serializers.PrimaryKeyRelatedField( @@ -220,9 +227,10 @@ class POReceiveSerializer(serializers.Serializer): items = data.get('items', []) if len(items) == 0: - raise ValidationError({ - 'items': _('Line items must be provided'), - }) + self._errors['items'] = _('Line items must be provided') + + if self._errors and raise_exception: + raise ValidationError(self.errors) return not bool(self._errors) diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 24ca8581d9..5ab5230d8e 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -201,6 +201,73 @@ class PurchaseOrderTest(OrderTest): response = self.get(url, expected_code=404) +class PurchaseOrderReceiveTest(OrderTest): + """ + Unit tests for receiving items against a PurchaseOrder + """ + + def setUp(self): + super().setUp() + + self.assignRole('purchase_order.add') + + self.url = reverse('api-po-receive', kwargs={'pk': 1}) + + def test_empty(self): + """ + Test without any POST data + """ + + data = self.post(self.url, {}, expected_code=400).data + + self.assertIn('This field is required', str(data['items'])) + self.assertIn('This field is required', str(data['location'])) + + def test_no_items(self): + """ + Test with an empty list of items + """ + + data = self.post( + self.url, + { + "items": [], + "location": None, + }, + expected_code=400 + ).data + + self.assertIn('Line items must be provided', str(data['items'])) + + def test_invalid_items(self): + """ + Test than errors are returned as expected for invalid data + """ + + data = self.post( + self.url, + { + "items": [ + { + "supplier_part": 12345, + "location": 12345 + } + ] + }, + expected_code=400 + ).data + + items = data['items'] + + self.assertIn('Invalid pk "12345"', str(items['supplier_part'])) + self.assertIn("object does not exist", str(items['location'])) + + def test_mismatched_items(self): + """ + Test for supplier parts which *do* exist but do not match the order supplier + """ + + class SalesOrderTest(OrderTest): """ Tests for the SalesOrder API