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 __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 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -197,29 +197,33 @@ class BuildUnallocate(generics.CreateAPIView): | |||||||
|  |  | ||||||
|     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'), | ||||||
|     ])), |     ])), | ||||||
|   | |||||||
| @@ -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): | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 %} | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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): | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user