Fix serializer nesting

- Add new API endpoint to receive items
- Add unit testing
This commit is contained in:
Oliver 2021-08-23 23:35:22 +10:00
parent 28cc241354
commit dc53a433a7
3 changed files with 136 additions and 10 deletions

View File

@ -11,6 +11,9 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics from rest_framework import generics
from rest_framework import filters, status from rest_framework import filters, status
from rest_framework.response import Response 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.helpers import str2bool
from InvenTree.api import AttachmentMixin from InvenTree.api import AttachmentMixin
@ -27,6 +30,7 @@ from .models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation
from .models import SalesOrderAttachment from .models import SalesOrderAttachment
from .serializers import SalesOrderSerializer, SOLineItemSerializer, SOAttachmentSerializer from .serializers import SalesOrderSerializer, SOLineItemSerializer, SOAttachmentSerializer
from .serializers import SalesOrderAllocationSerializer from .serializers import SalesOrderAllocationSerializer
from .serializers import POReceiveSerializer, POLineItemReceiveSerializer
class POList(generics.ListCreateAPIView): class POList(generics.ListCreateAPIView):
@ -204,6 +208,41 @@ class PODetail(generics.RetrieveUpdateDestroyAPIView):
return queryset 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): class POLineItemList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of POLineItem objects """ API endpoint for accessing a list of POLineItem objects
@ -616,13 +655,25 @@ class POAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin)
order_api_urls = [ order_api_urls = [
# API endpoints for purchase orders # API endpoints for purchase orders
url(r'po/attachment/', include([ url(r'^po/', include([
url(r'^(?P<pk>\d+)/$', POAttachmentDetail.as_view(), name='api-po-attachment-detail'),
url(r'^.*$', POAttachmentList.as_view(), name='api-po-attachment-list'), # Purchase order attachments
url(r'attachment/', include([
url(r'^(?P<pk>\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<pk>\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<pk>\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 # API endpoints for purchase order line items
url(r'^po-line/(?P<pk>\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'), url(r'^po-line/(?P<pk>\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'),

View File

@ -186,6 +186,13 @@ class POLineItemReceiveSerializer(serializers.Serializer):
help_text=_('Select destination location for received items'), help_text=_('Select destination location for received items'),
) )
quantity = serializers.DecimalField(
max_digits=15,
decimal_places=5,
min_value=0,
required=True,
)
class Meta: class Meta:
fields = [ fields = [
'supplier_part', 'supplier_part',
@ -198,8 +205,8 @@ class POReceiveSerializer(serializers.Serializer):
Serializer for receiving items against a purchase order Serializer for receiving items against a purchase order
""" """
items = serializers.StringRelatedField( items = POLineItemReceiveSerializer(
many=True many=True,
) )
location = serializers.PrimaryKeyRelatedField( location = serializers.PrimaryKeyRelatedField(
@ -220,9 +227,10 @@ class POReceiveSerializer(serializers.Serializer):
items = data.get('items', []) items = data.get('items', [])
if len(items) == 0: if len(items) == 0:
raise ValidationError({ self._errors['items'] = _('Line items must be provided')
'items': _('Line items must be provided'),
}) if self._errors and raise_exception:
raise ValidationError(self.errors)
return not bool(self._errors) return not bool(self._errors)

View File

@ -201,6 +201,73 @@ class PurchaseOrderTest(OrderTest):
response = self.get(url, expected_code=404) 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): class SalesOrderTest(OrderTest):
""" """
Tests for the SalesOrder API Tests for the SalesOrder API