Implement API endpoint and serializer for allocation of stock items

This commit is contained in:
Oliver 2021-10-04 18:26:30 +11:00
parent 22d6d49b97
commit 99c1819c69
2 changed files with 235 additions and 7 deletions

View File

@ -5,10 +5,15 @@ JSON API for the Build app
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf.urls import url, include
from django.utils.translation import ugettext_lazy as _
from rest_framework import filters
from rest_framework import generics
from django.db import transaction
from django.conf.urls import url, include
from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework import filters, generics, serializers, status
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
@ -19,6 +24,7 @@ from InvenTree.status_codes import BuildStatus
from .models import Build, BuildItem, BuildOrderAttachment
from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
from .serializers import BuildAllocationSerializer
class BuildFilter(rest_filters.FilterSet):
@ -181,6 +187,100 @@ class BuildDetail(generics.RetrieveUpdateAPIView):
serializer_class = BuildSerializer
class BuildAllocate(generics.CreateAPIView):
"""
API endpoint to allocate stock items to a build order
- The BuildOrder object is specified by the URL
- Items to allocate are specified as a list called "items" with the following options:
- bom_item: pk value of a given BomItem object (must match the part associated with this build)
- stock_item: pk value of a given StockItem object
- quantity: quantity to allocate
- output: StockItem (build order output) to allocate stock against (optional)
"""
queryset = Build.objects.none()
serializer_class = BuildAllocationSerializer
def get_build(self):
"""
Returns the BuildOrder associated with this API endpoint
"""
pk = self.kwargs.get('pk', None)
try:
build = Build.objects.get(pk=pk)
except (Build.DoesNotExist, ValueError):
raise ValidationError(_("Matching build order does not exist"))
return build
def get_serializer_context(self):
context = super().get_serializer_context()
context['build'] = self.get_build()
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):
""" API endpoint for accessing a list of BuildItem objects
@ -291,7 +391,10 @@ build_api_urls = [
])),
# Build Detail
url(r'^(?P<pk>\d+)/', BuildDetail.as_view(), name='api-build-detail'),
url(r'^(?P<pk>\d+)/', include([
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
])),
# Build List
url(r'^.*$', BuildList.as_view(), name='api-build-list'),

View File

@ -5,16 +5,21 @@ JSON serializers for Build API
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.db.models import Case, When, Value
from django.db.models import BooleanField
from rest_framework import serializers
from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
from stock.serializers import StockItemSerializerBrief
from stock.serializers import LocationSerializer
from stock.models import StockItem
from stock.serializers import StockItemSerializerBrief, LocationSerializer
from part.models import Part, BomItem
from part.serializers import PartSerializer, PartBriefSerializer
from users.serializers import OwnerSerializer
@ -22,7 +27,9 @@ from .models import Build, BuildItem, BuildOrderAttachment
class BuildSerializer(InvenTreeModelSerializer):
""" Serializes a Build object """
"""
Serializes a Build object
"""
url = serializers.CharField(source='get_absolute_url', read_only=True)
status_text = serializers.CharField(source='get_status_display', read_only=True)
@ -109,6 +116,124 @@ class BuildSerializer(InvenTreeModelSerializer):
]
class BuildAllocationItemSerializer(serializers.Serializer):
"""
A serializer for allocating a single stock item against a build order
"""
bom_item = serializers.PrimaryKeyRelatedField(
queryset=BomItem.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('BOM Item'),
)
def validate_bom_item(self, bom_item):
build = self.context['build']
# BomItem must point to the same 'part' as the parent build
if build.part != bom_item.part:
raise ValidationError(_("bom_item.part must point to the same part as the build order"))
return bom_item
stock_item = serializers.PrimaryKeyRelatedField(
queryset=StockItem.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('Stock Item'),
)
quantity = serializers.DecimalField(
max_digits=15,
decimal_places=5,
min_value=0,
required=True
)
output = serializers.PrimaryKeyRelatedField(
queryset=StockItem.objects.filter(is_building=True),
many=False,
allow_null=True,
required=False,
label=_('Build Output'),
)
class Meta:
fields = [
'bom_item',
'stock_item',
'quantity',
'output',
]
def is_valid(self, raise_exception=False):
if super().is_valid(raise_exception):
data = self.validated_data
bom_item = data['bom_item']
stock_item = data['stock_item']
quantity = data['quantity']
output = data.get('output', None)
build = self.context['build']
# 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!
# TODO: Check that the quantity does not exceed the available amount from the stock item
# Output *must* be set for trackable parts
if output is None and bom_item.sub_part.trackable:
self._errors['output'] = _('Build output must be specified for allocation of tracked parts')
# Output *cannot* be set for un-tracked parts
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(self.errors)
return not bool(self._errors)
class BuildAllocationSerializer(serializers.Serializer):
"""
DRF serializer for allocation stock items against a build order
"""
items = BuildAllocationItemSerializer(many=True)
class Meta:
fields = [
'items',
]
def is_valid(self, raise_exception=False):
"""
Validation
"""
super().is_valid(raise_exception)
data = self.validated_data
items = data.get('items', [])
if len(items) == 0:
self._errors['items'] = _('Allocation items must be provided')
if self._errors and raise_exception:
raise ValidationError(self.errors)
return not bool(self._errors)
class BuildItemSerializer(InvenTreeModelSerializer):
""" Serializes a BuildItem object """