2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

Fixes for serializer validation

- Note: use the validate() function!
- Ref: https://www.django-rest-framework.org/api-guide/serializers/
- override serializer.save() functionality for simpler operation
This commit is contained in:
Oliver 2021-10-04 23:44:23 +11:00
parent 074466f087
commit 5ded23fd99
6 changed files with 99 additions and 155 deletions

View File

@ -218,6 +218,9 @@ class BuildAllocate(generics.CreateAPIView):
return build return build
def get_serializer_context(self): def get_serializer_context(self):
"""
Provide the Build object to the serializer context
"""
context = super().get_serializer_context() context = super().get_serializer_context()
@ -225,61 +228,6 @@ class BuildAllocate(generics.CreateAPIView):
return context 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): class BuildItemList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of BuildItem objects """ API endpoint for accessing a list of BuildItem objects

View File

@ -1190,28 +1190,6 @@ class BuildItem(models.Model):
super().save() 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): def clean(self):
""" Check validity of the BuildItem model. """ Check validity of the BuildItem model.
The following checks are performed: The following checks are performed:

View File

@ -5,6 +5,8 @@ JSON serializers for Build API
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import transaction
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Case, When, Value 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 InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
import InvenTree.helpers
from stock.models import StockItem from stock.models import StockItem
from stock.serializers import StockItemSerializerBrief, LocationSerializer from stock.serializers import StockItemSerializerBrief, LocationSerializer
@ -147,6 +151,13 @@ class BuildAllocationItemSerializer(serializers.Serializer):
label=_('Stock Item'), 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( quantity = serializers.DecimalField(
max_digits=15, max_digits=15,
decimal_places=5, decimal_places=5,
@ -177,11 +188,9 @@ class BuildAllocationItemSerializer(serializers.Serializer):
'output', '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'] bom_item = data['bom_item']
stock_item = data['stock_item'] stock_item = data['stock_item']
@ -193,20 +202,31 @@ class BuildAllocationItemSerializer(serializers.Serializer):
# TODO: Check that the "stock item" is valid for the referenced "sub_part" # 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! # Note: Because of allow_variants options, it may not be a direct match!
# TODO: Check that the quantity does not exceed the available amount from the stock item # Check that the quantity does not exceed the available amount from the stock item
q = stock_item.unallocated_quantity()
if quantity > q:
q = InvenTree.helpers.clean_decimal(q)
raise ValidationError({
'quantity': _(f"Available quantity ({q}) exceeded")
})
# Output *must* be set for trackable parts # Output *must* be set for trackable parts
if output is None and bom_item.sub_part.trackable: if output is None and bom_item.sub_part.trackable:
self._errors['output'] = _('Build output must be specified for allocation of tracked parts') raise ValidationError({
'output': _('Build output must be specified for allocation of tracked parts')
})
# Output *cannot* be set for un-tracked parts # Output *cannot* be set for un-tracked parts
if output is not None and not bom_item.sub_part.trackable: 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')
if self._errors and raise_exception: raise ValidationError({
raise ValidationError(self.errors) 'output': _('Build output cannot be specified for allocation of untracked parts')
})
return not bool(self._errors) return data
class BuildAllocationSerializer(serializers.Serializer): class BuildAllocationSerializer(serializers.Serializer):
@ -221,24 +241,56 @@ class BuildAllocationSerializer(serializers.Serializer):
'items', 'items',
] ]
def is_valid(self, raise_exception=False): def validate(self, data):
""" """
Validation Validation
""" """
super().is_valid(raise_exception) super().validate(data)
data = self.validated_data
items = data.get('items', []) items = data.get('items', [])
if len(items) == 0: 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: return data
raise ValidationError(self.errors)
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): class BuildItemSerializer(InvenTreeModelSerializer):

View File

@ -252,34 +252,6 @@ class TestBuildViews(TestCase):
self.assertIn(build.title, content) 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): def test_build_output_complete(self):
""" """
Test the build output completion form Test the build output completion form

View File

@ -25,7 +25,6 @@ build_urls = [
url('^edit/', views.BuildItemEdit.as_view(), name='build-item-edit'), url('^edit/', views.BuildItemEdit.as_view(), name='build-item-edit'),
url('^delete/', views.BuildItemDelete.as_view(), name='build-item-delete'), url('^delete/', views.BuildItemDelete.as_view(), name='build-item-delete'),
])), ])),
url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'),
])), ])),
url(r'^(?P<pk>\d+)/', include(build_detail_urls)), url(r'^(?P<pk>\d+)/', include(build_detail_urls)),

View File

@ -276,18 +276,17 @@ class POReceiveSerializer(serializers.Serializer):
help_text=_('Select destination location for received items'), help_text=_('Select destination location for received items'),
) )
def is_valid(self, raise_exception=False): def validate(self, data):
super().is_valid(raise_exception) super().validate(data)
# Custom validation
data = self.validated_data
items = data.get('items', []) items = data.get('items', [])
if len(items) == 0: if len(items) == 0:
self._errors['items'] = _('Line items must be provided') raise ValidationError({
else: 'items': _('Line items must be provided')
})
# Ensure barcodes are unique # Ensure barcodes are unique
unique_barcodes = set() unique_barcodes = set()
@ -296,15 +295,11 @@ class POReceiveSerializer(serializers.Serializer):
if barcode: if barcode:
if barcode in unique_barcodes: if barcode in unique_barcodes:
self._errors['items'] = _('Supplied barcode values must be unique') raise ValidationError(_('Supplied barcode values must be unique'))
break
else: else:
unique_barcodes.add(barcode) unique_barcodes.add(barcode)
if self._errors and raise_exception: return data
raise ValidationError(self.errors)
return not bool(self._errors)
class Meta: class Meta:
fields = [ fields = [