mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Add an API serializer to complete build outputs
This commit is contained in:
		| @@ -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 | ||||
|  | ||||
|  | ||||
| @@ -197,29 +197,33 @@ class BuildUnallocate(generics.CreateAPIView): | ||||
|  | ||||
|     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'), | ||||
|     ])), | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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): | ||||
|     """ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user