mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Implement API endpoint and serializer for allocation of stock items
This commit is contained in:
parent
22d6d49b97
commit
99c1819c69
@ -5,10 +5,15 @@ JSON API for the Build app
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
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 django.db import transaction
|
||||||
from rest_framework import generics
|
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.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as rest_filters
|
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 .models import Build, BuildItem, BuildOrderAttachment
|
||||||
from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
|
from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
|
||||||
|
from .serializers import BuildAllocationSerializer
|
||||||
|
|
||||||
|
|
||||||
class BuildFilter(rest_filters.FilterSet):
|
class BuildFilter(rest_filters.FilterSet):
|
||||||
@ -181,6 +187,100 @@ class BuildDetail(generics.RetrieveUpdateAPIView):
|
|||||||
serializer_class = BuildSerializer
|
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):
|
class BuildItemList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of BuildItem objects
|
""" API endpoint for accessing a list of BuildItem objects
|
||||||
|
|
||||||
@ -291,7 +391,10 @@ build_api_urls = [
|
|||||||
])),
|
])),
|
||||||
|
|
||||||
# Build Detail
|
# 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
|
# Build List
|
||||||
url(r'^.*$', BuildList.as_view(), name='api-build-list'),
|
url(r'^.*$', BuildList.as_view(), name='api-build-list'),
|
||||||
|
@ -5,16 +5,21 @@ JSON serializers for Build API
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
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 Case, When, Value
|
||||||
from django.db.models import BooleanField
|
from django.db.models import BooleanField
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
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
|
||||||
|
|
||||||
from stock.serializers import StockItemSerializerBrief
|
from stock.models import StockItem
|
||||||
from stock.serializers import LocationSerializer
|
from stock.serializers import StockItemSerializerBrief, LocationSerializer
|
||||||
|
|
||||||
|
from part.models import Part, BomItem
|
||||||
from part.serializers import PartSerializer, PartBriefSerializer
|
from part.serializers import PartSerializer, PartBriefSerializer
|
||||||
from users.serializers import OwnerSerializer
|
from users.serializers import OwnerSerializer
|
||||||
|
|
||||||
@ -22,7 +27,9 @@ from .models import Build, BuildItem, BuildOrderAttachment
|
|||||||
|
|
||||||
|
|
||||||
class BuildSerializer(InvenTreeModelSerializer):
|
class BuildSerializer(InvenTreeModelSerializer):
|
||||||
""" Serializes a Build object """
|
"""
|
||||||
|
Serializes a Build object
|
||||||
|
"""
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
status_text = serializers.CharField(source='get_status_display', 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):
|
class BuildItemSerializer(InvenTreeModelSerializer):
|
||||||
""" Serializes a BuildItem object """
|
""" Serializes a BuildItem object """
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user