Add an API serializer to complete build outputs

This commit is contained in:
Oliver 2021-10-14 23:13:01 +11:00
parent 4b4bf38ae5
commit 54dd05a24d
8 changed files with 153 additions and 63 deletions

View File

@ -6,7 +6,7 @@ JSON API for the Build app
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from django.conf.urls import url, include
from rest_framework import filters, generics
@ -20,7 +20,7 @@ from InvenTree.helpers import str2bool, isNull
from InvenTree.status_codes import BuildStatus
from .models import Build, BuildItem, BuildOrderAttachment
from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
from .serializers import BuildAttachmentSerializer, BuildCompleteSerializer, BuildSerializer, BuildItemSerializer
from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer
@ -196,30 +196,34 @@ class BuildUnallocate(generics.CreateAPIView):
queryset = Build.objects.none()
serializer_class = BuildUnallocationSerializer
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 (ValueError, Build.DoesNotExist):
raise ValidationError(_("Matching build order does not exist"))
return build
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['build'] = self.get_build()
ctx['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None))
ctx['request'] = self.request
return ctx
class BuildComplete(generics.CreateAPIView):
"""
API endpoint for completing build outputs
"""
queryset = Build.objects.none()
serializer_class = BuildCompleteSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['request'] = self.request
ctx['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None))
return ctx
class BuildAllocate(generics.CreateAPIView):
"""
API endpoint to allocate stock items to a build order
@ -236,20 +240,6 @@ class BuildAllocate(generics.CreateAPIView):
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):
"""
Provide the Build object to the serializer context
@ -257,7 +247,7 @@ class BuildAllocate(generics.CreateAPIView):
context = super().get_serializer_context()
context['build'] = self.get_build()
context['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None))
context['request'] = self.request
return context
@ -385,6 +375,7 @@ build_api_urls = [
# Build Detail
url(r'^(?P<pk>\d+)/', include([
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
url(r'^complete/', BuildComplete.as_view(), name='api-build-complete'),
url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
])),

View File

@ -722,7 +722,7 @@ class Build(MPTTModel):
items.all().delete()
@transaction.atomic
def completeBuildOutput(self, output, user, **kwargs):
def complete_build_output(self, output, user, **kwargs):
"""
Complete a particular build output
@ -739,10 +739,6 @@ class Build(MPTTModel):
allocated_items = output.items_to_install.all()
for build_item in allocated_items:
# TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete
# TODO: Use the background worker process to handle this task!
# Complete the allocation of stock for that item
build_item.complete_allocation(user)
@ -768,6 +764,7 @@ class Build(MPTTModel):
# Increase the completed quantity for this build
self.completed += output.quantity
self.save()
def requiredQuantity(self, part, output):

View File

@ -18,9 +18,10 @@ from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
from InvenTree.status_codes import StockStatus
import InvenTree.helpers
from stock.models import StockItem
from stock.models import StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer
from part.models import BomItem
@ -120,6 +121,120 @@ class BuildSerializer(InvenTreeModelSerializer):
]
class BuildOutputSerializer(serializers.Serializer):
"""
Serializer for a "BuildOutput"
Note that a "BuildOutput" is really just a StockItem which is "in production"!
"""
output = serializers.PrimaryKeyRelatedField(
queryset=StockItem.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('Build Output'),
)
def validate_output(self, output):
build = self.context['build']
# The stock item must point to the build
if output.build != build:
raise ValidationError(_("Build output does not match the parent build"))
# The part must match!
if output.part != build.part:
raise ValidationError(_("Output part does not match BuildOrder part"))
# The build output must be "in production"
if not output.is_building:
raise ValidationError(_("This build output has already been completed"))
return output
class Meta:
fields = [
'output',
]
class BuildCompleteSerializer(serializers.Serializer):
"""
DRF serializer for completing one or more build outputs
"""
class Meta:
fields = [
'outputs',
'location',
'status',
'notes',
]
outputs = BuildOutputSerializer(
many=True,
required=True,
)
location = serializers.PrimaryKeyRelatedField(
queryset=StockLocation.objects.all(),
required=True,
many=False,
label=_("Location"),
help_text=_("Location for completed build outputs"),
)
status = serializers.ChoiceField(
choices=list(StockStatus.items()),
default=StockStatus.OK,
label=_("Status"),
)
notes = serializers.CharField(
label=_("Notes"),
required=False,
allow_blank=True,
)
def validate(self, data):
super().validate(data)
outputs = data.get('outputs', [])
if len(outputs) == 0:
raise ValidationError(_("A list of build outputs must be provided"))
return data
def save(self):
"""
"save" the serializer to complete the build outputs
"""
build = self.context['build']
request = self.context['request']
data = self.validated_data
outputs = data.get('outputs', [])
# Mark the specified build outputs as "complete"
with transaction.atomic():
for item in outputs:
output = item['output']
build.complete_build_output(
output,
request.user,
status=data['status'],
notes=data.get('notes', '')
)
class BuildUnallocationSerializer(serializers.Serializer):
"""
DRF serializer for unallocating stock from a BuildOrder

View File

@ -96,11 +96,6 @@ src="{% static 'img/blank_image.png' %}"
</div>
<!-- Build actions -->
{% if roles.build.change %}
{% if build.active %}
<button id='build-complete' title='{% trans "Complete Build" %}' class='btn btn-success'>
<span class='fas fa-paper-plane'></span>
</button>
{% endif %}
<div class='btn-group'>
<button id='build-options' title='{% trans "Build actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
<span class='fas fa-tools'></span> <span class='caret'></span>
@ -115,6 +110,11 @@ src="{% static 'img/blank_image.png' %}"
{% endif %}
</ul>
</div>
{% if build.active %}
<button id='build-complete' title='{% trans "Complete Build" %}' class='btn btn-success'>
<span class='fas fa-check-circle'></span>
</button>
{% endif %}
{% endif %}
</div>
{% endblock %}

View File

@ -319,11 +319,11 @@ class BuildTest(TestCase):
self.assertTrue(self.build.isFullyAllocated(self.output_1))
self.assertTrue(self.build.isFullyAllocated(self.output_2))
self.build.completeBuildOutput(self.output_1, None)
self.build.complete_build_output(self.output_1, None)
self.assertFalse(self.build.can_complete)
self.build.completeBuildOutput(self.output_2, None)
self.build.complete_build_output(self.output_2, None)
self.assertTrue(self.build.can_complete)

View File

@ -434,7 +434,7 @@ class BuildOutputComplete(AjaxUpdateView):
stock_status = StockStatus.OK
# Complete the build output
build.completeBuildOutput(
build.complete_build_output(
output,
self.request.user,
location=location,

View File

@ -8,6 +8,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.conf.urls import url, include
from django.db.models import Q, F
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as rest_filters
from rest_framework import generics
@ -232,25 +233,11 @@ class POReceive(generics.CreateAPIView):
context = super().get_serializer_context()
# Pass the purchase order through to the serializer for validation
context['order'] = self.get_order()
context['order'] = get_object_or_404(PurchaseOrder, pk=self.kwargs.get('pk', None))
context['request'] = self.request
return context
def get_order(self):
"""
Returns the PurchaseOrder associated with this API endpoint
"""
pk = self.kwargs.get('pk', None)
try:
order = PurchaseOrder.objects.get(pk=pk)
except (PurchaseOrder.DoesNotExist, ValueError):
raise ValidationError(_("Matching purchase order does not exist"))
return order
class POLineItemFilter(rest_filters.FilterSet):
"""

View File

@ -151,7 +151,7 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
// Add a button to "complete" the particular build output
html += makeIconButton(
'fa-check icon-green', 'button-output-complete', outputId,
'fa-check-circle icon-green', 'button-output-complete', outputId,
'{% trans "Complete build output" %}',
{
// disabled: true