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

View File

@ -722,7 +722,7 @@ class Build(MPTTModel):
items.all().delete() items.all().delete()
@transaction.atomic @transaction.atomic
def completeBuildOutput(self, output, user, **kwargs): def complete_build_output(self, output, user, **kwargs):
""" """
Complete a particular build output Complete a particular build output
@ -739,10 +739,6 @@ class Build(MPTTModel):
allocated_items = output.items_to_install.all() allocated_items = output.items_to_install.all()
for build_item in allocated_items: 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 # Complete the allocation of stock for that item
build_item.complete_allocation(user) build_item.complete_allocation(user)
@ -768,6 +764,7 @@ class Build(MPTTModel):
# Increase the completed quantity for this build # Increase the completed quantity for this build
self.completed += output.quantity self.completed += output.quantity
self.save() self.save()
def requiredQuantity(self, part, output): 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 InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
from InvenTree.status_codes import StockStatus
import InvenTree.helpers import InvenTree.helpers
from stock.models import StockItem from stock.models import StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer from stock.serializers import StockItemSerializerBrief, LocationSerializer
from part.models import BomItem 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): class BuildUnallocationSerializer(serializers.Serializer):
""" """
DRF serializer for unallocating stock from a BuildOrder DRF serializer for unallocating stock from a BuildOrder

View File

@ -96,11 +96,6 @@ src="{% static 'img/blank_image.png' %}"
</div> </div>
<!-- Build actions --> <!-- Build actions -->
{% if roles.build.change %} {% 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'> <div class='btn-group'>
<button id='build-options' title='{% trans "Build actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'> <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> <span class='fas fa-tools'></span> <span class='caret'></span>
@ -115,6 +110,11 @@ src="{% static 'img/blank_image.png' %}"
{% endif %} {% endif %}
</ul> </ul>
</div> </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 %} {% endif %}
</div> </div>
{% endblock %} {% 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_1))
self.assertTrue(self.build.isFullyAllocated(self.output_2)) 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.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) self.assertTrue(self.build.can_complete)

View File

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

View File

@ -8,6 +8,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf.urls import url, include from django.conf.urls import url, include
from django.db.models import Q, F 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 django_filters import rest_framework as rest_filters
from rest_framework import generics from rest_framework import generics
@ -232,25 +233,11 @@ class POReceive(generics.CreateAPIView):
context = super().get_serializer_context() context = super().get_serializer_context()
# Pass the purchase order through to the serializer for validation # 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 context['request'] = self.request
return context 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): 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 // Add a button to "complete" the particular build output
html += makeIconButton( html += makeIconButton(
'fa-check icon-green', 'button-output-complete', outputId, 'fa-check-circle icon-green', 'button-output-complete', outputId,
'{% trans "Complete build output" %}', '{% trans "Complete build output" %}',
{ {
// disabled: true // disabled: true