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:
parent
074466f087
commit
5ded23fd99
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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)),
|
||||||
|
@ -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 = [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user