diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 7920003d8b..ddedd60ce6 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -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'), ])), diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 449776579e..994d060585 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -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): diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 547f565905..cf7c8065fe 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -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 diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index e3119e6fdb..5f53fa36bb 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -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 %} diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index a0874d0979..31e4cf1822 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -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) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 8c63c1296c..37ec567f1a 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -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, diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index af30a3a5c5..0a540c3412 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -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): """ diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index d3499deedf..686e8e1bfa 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -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