diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 84c5c2ed3c..648a4f4417 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -5,7 +5,9 @@ JSON API for the Order app # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.utils.translation import ugettext_lazy as _ from django.conf.urls import url, include +from django.db import transaction from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics @@ -13,7 +15,6 @@ 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 @@ -252,25 +253,34 @@ class POReceive(generics.CreateAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - # Check that the received line items are indeed correct - self.validate(serializer.validated_data) + # Receive the line items + self.receive_items(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - def validate(self, data): + @transaction.atomic + def receive_items(self, serializer): """ - Validate the deserialized data. + Receive the items - At this point, much of the heavy lifting has been done for us by DRF serializers + At this point, much of the heavy lifting has been done for us by DRF serializers! + + We have a list of "items", each a dict which contains: + - line_item: A PurchaseOrderLineItem matching this order + - location: A destination location + - quantity: A validated numerical quantity + - status: The status code for the received item """ + data = serializer.validated_data + location = data['location'] - # Keep track of validated data "on the fly" - self.items = [] + items = data['items'] - for item in data['items']: + # Check if the location is not specified for any particular item + for item in items: supplier_part = item['supplier_part'] @@ -287,7 +297,15 @@ class POReceive(generics.CreateAPIView): item['location'] = location - quantity = item['quantity'] + # Now we can actually receive the items + for item in items: + self.order.receive_line_item( + item['line_item'], + item['location'], + item['quantity'], + self.request.user, + status=item['status'], + ) class POLineItemList(generics.ListCreateAPIView): diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e55f5203ba..35982349d5 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -433,7 +433,7 @@ class PurchaseOrder(Order): quantity=quantity, purchase_order=self, status=status, - purchase_price=purchase_price, + purchase_price=line.purchase_price, ) stock.save(add_note=False) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index cc40c321dc..2b3da3f87d 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -20,6 +20,8 @@ from InvenTree.serializers import InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeAttachmentSerializerField +from InvenTree.status_codes import StockStatus + import company.models from company.serializers import CompanyBriefSerializer, SupplierPartSerializer @@ -200,10 +202,17 @@ class POLineItemReceiveSerializer(serializers.Serializer): required=True, ) + status = serializers.ChoiceField( + choices=StockStatus.options, + default=StockStatus.OK, + label=_('Status'), + ) + class Meta: fields = [ 'supplier_part', 'location', + 'status', ] diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 6db4cbc47e..0074442930 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -2,6 +2,7 @@ Tests for the Order API """ +from InvenTree.stock.models import StockItem from datetime import datetime, timedelta from rest_framework import status @@ -213,6 +214,9 @@ class PurchaseOrderReceiveTest(OrderTest): self.url = reverse('api-po-receive', kwargs={'pk': 1}) + # Number of stock items which exist at the start of each test + self.n = StockItem.objects.count() + def test_empty(self): """ Test without any POST data @@ -223,6 +227,9 @@ class PurchaseOrderReceiveTest(OrderTest): self.assertIn('This field is required', str(data['items'])) self.assertIn('This field is required', str(data['location'])) + # No new stock items have been created + self.assertEqual(self.n, StockItem.objects.count()) + def test_no_items(self): """ Test with an empty list of items @@ -239,6 +246,9 @@ class PurchaseOrderReceiveTest(OrderTest): self.assertIn('Line items must be provided', str(data['items'])) + # No new stock items have been created + self.assertEqual(self.n, StockItem.objects.count()) + def test_invalid_items(self): """ Test than errors are returned as expected for invalid data @@ -262,6 +272,34 @@ class PurchaseOrderReceiveTest(OrderTest): self.assertIn('Invalid pk "12345"', str(items['line_item'])) self.assertIn("object does not exist", str(items['location'])) + # No new stock items have been created + self.assertEqual(self.n, StockItem.objects.count()) + + def test_invalid_status(self): + """ + Test with an invalid StockStatus value + """ + + data = self.post( + self.url, + { + "items": [ + { + "line_item": 22, + "location": 1, + "status": 99999, + "quantity": 5, + } + ] + }, + expected_code=400 + ).data + + self.assertIn('"99999" is not a valid choice.', str(data)) + + # No new stock items have been created + self.assertEqual(self.n, StockItem.objects.count()) + def test_mismatched_items(self): """ Test for supplier parts which *do* exist but do not match the order supplier @@ -284,6 +322,9 @@ class PurchaseOrderReceiveTest(OrderTest): self.assertIn('Line item does not match purchase order', str(data)) + # No new stock items have been created + self.assertEqual(self.n, StockItem.objects.count()) + class SalesOrderTest(OrderTest): """