mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Merge pull request #2510 from SchrodingersGat/build-order-complete-improvements
Adds confirmation inputs when completing build order
This commit is contained in:
		| @@ -18,8 +18,7 @@ from InvenTree.filters import InvenTreeOrderingFilter | |||||||
| 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, BuildCompleteSerializer, BuildSerializer, BuildItemSerializer | import build.serializers | ||||||
| from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer |  | ||||||
| from users.models import Owner | from users.models import Owner | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -80,7 +79,7 @@ class BuildList(generics.ListCreateAPIView): | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     queryset = Build.objects.all() |     queryset = Build.objects.all() | ||||||
|     serializer_class = BuildSerializer |     serializer_class = build.serializers.BuildSerializer | ||||||
|     filterset_class = BuildFilter |     filterset_class = BuildFilter | ||||||
|  |  | ||||||
|     filter_backends = [ |     filter_backends = [ | ||||||
| @@ -119,7 +118,7 @@ class BuildList(generics.ListCreateAPIView): | |||||||
|  |  | ||||||
|         queryset = super().get_queryset().select_related('part') |         queryset = super().get_queryset().select_related('part') | ||||||
|  |  | ||||||
|         queryset = BuildSerializer.annotate_queryset(queryset) |         queryset = build.serializers.BuildSerializer.annotate_queryset(queryset) | ||||||
|  |  | ||||||
|         return queryset |         return queryset | ||||||
|  |  | ||||||
| @@ -203,7 +202,7 @@ class BuildDetail(generics.RetrieveUpdateAPIView): | |||||||
|     """ API endpoint for detail view of a Build object """ |     """ API endpoint for detail view of a Build object """ | ||||||
|  |  | ||||||
|     queryset = Build.objects.all() |     queryset = Build.objects.all() | ||||||
|     serializer_class = BuildSerializer |     serializer_class = build.serializers.BuildSerializer | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildUnallocate(generics.CreateAPIView): | class BuildUnallocate(generics.CreateAPIView): | ||||||
| @@ -217,7 +216,7 @@ class BuildUnallocate(generics.CreateAPIView): | |||||||
|  |  | ||||||
|     queryset = Build.objects.none() |     queryset = Build.objects.none() | ||||||
|  |  | ||||||
|     serializer_class = BuildUnallocationSerializer |     serializer_class = build.serializers.BuildUnallocationSerializer | ||||||
|  |  | ||||||
|     def get_serializer_context(self): |     def get_serializer_context(self): | ||||||
|  |  | ||||||
| @@ -233,14 +232,36 @@ class BuildUnallocate(generics.CreateAPIView): | |||||||
|         return ctx |         return ctx | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildComplete(generics.CreateAPIView): | class BuildOutputComplete(generics.CreateAPIView): | ||||||
|     """ |     """ | ||||||
|     API endpoint for completing build outputs |     API endpoint for completing build outputs | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     queryset = Build.objects.none() |     queryset = Build.objects.none() | ||||||
|  |  | ||||||
|     serializer_class = BuildCompleteSerializer |     serializer_class = build.serializers.BuildOutputCompleteSerializer | ||||||
|  |  | ||||||
|  |     def get_serializer_context(self): | ||||||
|  |         ctx = super().get_serializer_context() | ||||||
|  |  | ||||||
|  |         ctx['request'] = self.request | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None)) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         return ctx | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BuildFinish(generics.CreateAPIView): | ||||||
|  |     """ | ||||||
|  |     API endpoint for marking a build as finished (completed) | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     queryset = Build.objects.none() | ||||||
|  |  | ||||||
|  |     serializer_class = build.serializers.BuildCompleteSerializer | ||||||
|  |  | ||||||
|     def get_serializer_context(self): |     def get_serializer_context(self): | ||||||
|         ctx = super().get_serializer_context() |         ctx = super().get_serializer_context() | ||||||
| @@ -269,7 +290,7 @@ class BuildAllocate(generics.CreateAPIView): | |||||||
|  |  | ||||||
|     queryset = Build.objects.none() |     queryset = Build.objects.none() | ||||||
|  |  | ||||||
|     serializer_class = BuildAllocationSerializer |     serializer_class = build.serializers.BuildAllocationSerializer | ||||||
|  |  | ||||||
|     def get_serializer_context(self): |     def get_serializer_context(self): | ||||||
|         """ |         """ | ||||||
| @@ -294,7 +315,7 @@ class BuildItemDetail(generics.RetrieveUpdateDestroyAPIView): | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     queryset = BuildItem.objects.all() |     queryset = BuildItem.objects.all() | ||||||
|     serializer_class = BuildItemSerializer |     serializer_class = build.serializers.BuildItemSerializer | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildItemList(generics.ListCreateAPIView): | class BuildItemList(generics.ListCreateAPIView): | ||||||
| @@ -304,7 +325,7 @@ class BuildItemList(generics.ListCreateAPIView): | |||||||
|     - POST: Create a new BuildItem object |     - POST: Create a new BuildItem object | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     serializer_class = BuildItemSerializer |     serializer_class = build.serializers.BuildItemSerializer | ||||||
|  |  | ||||||
|     def get_serializer(self, *args, **kwargs): |     def get_serializer(self, *args, **kwargs): | ||||||
|  |  | ||||||
| @@ -373,7 +394,7 @@ class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin): | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     queryset = BuildOrderAttachment.objects.all() |     queryset = BuildOrderAttachment.objects.all() | ||||||
|     serializer_class = BuildAttachmentSerializer |     serializer_class = build.serializers.BuildAttachmentSerializer | ||||||
|  |  | ||||||
|     filter_backends = [ |     filter_backends = [ | ||||||
|         DjangoFilterBackend, |         DjangoFilterBackend, | ||||||
| @@ -390,7 +411,7 @@ class BuildAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMix | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     queryset = BuildOrderAttachment.objects.all() |     queryset = BuildOrderAttachment.objects.all() | ||||||
|     serializer_class = BuildAttachmentSerializer |     serializer_class = build.serializers.BuildAttachmentSerializer | ||||||
|  |  | ||||||
|  |  | ||||||
| build_api_urls = [ | build_api_urls = [ | ||||||
| @@ -410,7 +431,8 @@ 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'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'), | ||||||
|  |         url(r'^finish/', BuildFinish.as_view(), name='api-build-finish'), | ||||||
|         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'), | ||||||
|     ])), |     ])), | ||||||
|   | |||||||
| @@ -83,24 +83,6 @@ class BuildOutputDeleteForm(HelperForm): | |||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class CompleteBuildForm(HelperForm): |  | ||||||
|     """ |  | ||||||
|     Form for marking a build as complete |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     confirm = forms.BooleanField( |  | ||||||
|         required=True, |  | ||||||
|         label=_('Confirm'), |  | ||||||
|         help_text=_('Mark build as complete'), |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         model = Build |  | ||||||
|         fields = [ |  | ||||||
|             'confirm', |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CancelBuildForm(HelperForm): | class CancelBuildForm(HelperForm): | ||||||
|     """ Form for cancelling a build """ |     """ Form for cancelling a build """ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -555,7 +555,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): | |||||||
|         if self.incomplete_count > 0: |         if self.incomplete_count > 0: | ||||||
|             return False |             return False | ||||||
|  |  | ||||||
|         if self.completed < self.quantity: |         if self.remaining > 0: | ||||||
|             return False |             return False | ||||||
|  |  | ||||||
|         if not self.areUntrackedPartsFullyAllocated(): |         if not self.areUntrackedPartsFullyAllocated(): | ||||||
|   | |||||||
| @@ -165,7 +165,7 @@ class BuildOutputSerializer(serializers.Serializer): | |||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildCompleteSerializer(serializers.Serializer): | class BuildOutputCompleteSerializer(serializers.Serializer): | ||||||
|     """ |     """ | ||||||
|     DRF serializer for completing one or more build outputs |     DRF serializer for completing one or more build outputs | ||||||
|     """ |     """ | ||||||
| @@ -240,6 +240,47 @@ class BuildCompleteSerializer(serializers.Serializer): | |||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BuildCompleteSerializer(serializers.Serializer): | ||||||
|  |     """ | ||||||
|  |     DRF serializer for marking a BuildOrder as complete | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     accept_unallocated = serializers.BooleanField( | ||||||
|  |         label=_('Accept Unallocated'), | ||||||
|  |         help_text=_('Accept that stock items have not been fully allocated to this build order'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def validate_accept_unallocated(self, value): | ||||||
|  |  | ||||||
|  |         build = self.context['build'] | ||||||
|  |  | ||||||
|  |         if not build.areUntrackedPartsFullyAllocated() and not value: | ||||||
|  |             raise ValidationError(_('Required stock has not been fully allocated')) | ||||||
|  |  | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     accept_incomplete = serializers.BooleanField( | ||||||
|  |         label=_('Accept Incomplete'), | ||||||
|  |         help_text=_('Accept that the required number of build outputs have not been completed'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def validate_accept_incomplete(self, value): | ||||||
|  |  | ||||||
|  |         build = self.context['build'] | ||||||
|  |  | ||||||
|  |         if build.remaining > 0 and not value: | ||||||
|  |             raise ValidationError(_('Required build quantity has not been completed')) | ||||||
|  |  | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def save(self): | ||||||
|  |  | ||||||
|  |         request = self.context['request'] | ||||||
|  |         build = self.context['build'] | ||||||
|  |  | ||||||
|  |         build.complete_build(request.user) | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildUnallocationSerializer(serializers.Serializer): | class BuildUnallocationSerializer(serializers.Serializer): | ||||||
|     """ |     """ | ||||||
|     DRF serializer for unallocating stock from a BuildOrder |     DRF serializer for unallocating stock from a BuildOrder | ||||||
|   | |||||||
| @@ -224,13 +224,11 @@ src="{% static 'img/blank_image.png' %}" | |||||||
|             '{% trans "Build Order cannot be completed as incomplete build outputs remain" %}' |             '{% trans "Build Order cannot be completed as incomplete build outputs remain" %}' | ||||||
|         ); |         ); | ||||||
|         {% else %} |         {% else %} | ||||||
|         launchModalForm( |  | ||||||
|             "{% url 'build-complete' build.id %}", |         completeBuildOrder({{ build.pk }}, { | ||||||
|             { |             allocated: {% if build.areUntrackedPartsFullyAllocated %}true{% else %}false{% endif %}, | ||||||
|                 reload: true, |             completed: {% if build.remaining == 0 %}true{% else %}false{% endif %}, | ||||||
|                 submit_text: '{% trans "Complete Build" %}', |         }); | ||||||
|             } |  | ||||||
|         ); |  | ||||||
|         {% endif %} |         {% endif %} | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,26 +0,0 @@ | |||||||
| {% extends "modal_form.html" %} |  | ||||||
| {% load i18n %} |  | ||||||
|  |  | ||||||
| {% block pre_form_content %} |  | ||||||
|  |  | ||||||
| {% if build.can_complete %} |  | ||||||
| <div class='alert alert-block alert-success'> |  | ||||||
|     {% trans "Build Order is complete" %} |  | ||||||
| </div> |  | ||||||
| {% else %} |  | ||||||
| <div class='alert alert-block alert-danger'> |  | ||||||
|     <strong>{% trans "Build Order is incomplete" %}</strong><br> |  | ||||||
|     <ul> |  | ||||||
|         {% if build.incomplete_count > 0 %} |  | ||||||
|         <li>{% trans "Incompleted build outputs remain" %}</li> |  | ||||||
|         {% endif %} |  | ||||||
|         {% if build.completed < build.quantity %} |  | ||||||
|         <li>{% trans "Required build quantity has not been completed" %}</li> |  | ||||||
|         {% endif %} |  | ||||||
|         {% if not build.areUntrackedPartsFullyAllocated %} |  | ||||||
|         <li>{% trans "Required stock has not been fully allocated" %}</li> |  | ||||||
|         {% endif %} |  | ||||||
|     </ul> |  | ||||||
| </div> |  | ||||||
| {% endif %} |  | ||||||
| {% endblock %} |  | ||||||
| @@ -49,7 +49,7 @@ class BuildCompleteTest(BuildAPITest): | |||||||
|  |  | ||||||
|         self.build = Build.objects.get(pk=1) |         self.build = Build.objects.get(pk=1) | ||||||
|  |  | ||||||
|         self.url = reverse('api-build-complete', kwargs={'pk': self.build.pk}) |         self.url = reverse('api-build-output-complete', kwargs={'pk': self.build.pk}) | ||||||
|  |  | ||||||
|     def test_invalid(self): |     def test_invalid(self): | ||||||
|         """ |         """ | ||||||
| @@ -58,7 +58,7 @@ class BuildCompleteTest(BuildAPITest): | |||||||
|  |  | ||||||
|         # Test with an invalid build ID |         # Test with an invalid build ID | ||||||
|         self.post( |         self.post( | ||||||
|             reverse('api-build-complete', kwargs={'pk': 99999}), |             reverse('api-build-output-complete', kwargs={'pk': 99999}), | ||||||
|             {}, |             {}, | ||||||
|             expected_code=400 |             expected_code=400 | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -11,7 +11,6 @@ build_detail_urls = [ | |||||||
|     url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'), |     url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'), | ||||||
|     url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'), |     url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'), | ||||||
|     url(r'^delete-output/', views.BuildOutputDelete.as_view(), name='build-output-delete'), |     url(r'^delete-output/', views.BuildOutputDelete.as_view(), name='build-output-delete'), | ||||||
|     url(r'^complete/', views.BuildComplete.as_view(), name='build-complete'), |  | ||||||
|  |  | ||||||
|     url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), |     url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -246,39 +246,6 @@ class BuildOutputDelete(AjaxUpdateView): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildComplete(AjaxUpdateView): |  | ||||||
|     """ |  | ||||||
|     View to mark the build as complete. |  | ||||||
|  |  | ||||||
|     Requirements: |  | ||||||
|     - There can be no outstanding build outputs |  | ||||||
|     - The "completed" value must meet or exceed the "quantity" value |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     model = Build |  | ||||||
|     form_class = forms.CompleteBuildForm |  | ||||||
|  |  | ||||||
|     ajax_form_title = _('Complete Build Order') |  | ||||||
|     ajax_template_name = 'build/complete.html' |  | ||||||
|  |  | ||||||
|     def validate(self, build, form, **kwargs): |  | ||||||
|  |  | ||||||
|         if build.incomplete_count > 0: |  | ||||||
|             form.add_error(None, _('Build order cannot be completed - incomplete outputs remain')) |  | ||||||
|  |  | ||||||
|     def save(self, build, form, **kwargs): |  | ||||||
|         """ |  | ||||||
|         Perform the build completion step |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         build.complete_build(self.request.user) |  | ||||||
|  |  | ||||||
|     def get_data(self): |  | ||||||
|         return { |  | ||||||
|             'success': _('Completed build order') |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildDetail(InvenTreeRoleMixin, DetailView): | class BuildDetail(InvenTreeRoleMixin, DetailView): | ||||||
|     """ |     """ | ||||||
|     Detail view of a single Build object. |     Detail view of a single Build object. | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ | |||||||
|  |  | ||||||
| /* exported | /* exported | ||||||
|     allocateStockToBuild, |     allocateStockToBuild, | ||||||
|  |     completeBuildOrder, | ||||||
|     editBuildOrder, |     editBuildOrder, | ||||||
|     loadAllocationTable, |     loadAllocationTable, | ||||||
|     loadBuildOrderAllocationTable, |     loadBuildOrderAllocationTable, | ||||||
| @@ -120,6 +121,57 @@ function newBuildOrder(options={}) { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* Construct a form to "complete" (finish) a build order */ | ||||||
|  | function completeBuildOrder(build_id, options={}) { | ||||||
|  |  | ||||||
|  |     var url = `/api/build/${build_id}/finish/`; | ||||||
|  |  | ||||||
|  |     var fields = { | ||||||
|  |         accept_unallocated: {}, | ||||||
|  |         accept_incomplete: {}, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     var html = ''; | ||||||
|  |  | ||||||
|  |     if (options.can_complete) { | ||||||
|  |  | ||||||
|  |     } else { | ||||||
|  |         html += ` | ||||||
|  |         <div class='alert alert-block alert-danger'> | ||||||
|  |         <strong>{% trans "Build Order is incomplete" %}</strong> | ||||||
|  |         </div> | ||||||
|  |         `; | ||||||
|  |  | ||||||
|  |         if (!options.allocated) { | ||||||
|  |             html += `<div class='alert alert-block alert-warning'>{% trans "Required stock has not been fully allocated" %}</div>`; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!options.completed) { | ||||||
|  |             html += `<div class='alert alert-block alert-warning'>{% trans "Required build quantity has not been completed" %}</div>`; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Hide particular fields if they are not required | ||||||
|  |  | ||||||
|  |     if (options.allocated) { | ||||||
|  |         delete fields.accept_unallocated; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (options.completed) { | ||||||
|  |         delete fields.accept_incomplete; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     constructForm(url, { | ||||||
|  |         fields: fields, | ||||||
|  |         reload: true, | ||||||
|  |         confirm: true, | ||||||
|  |         method: 'POST', | ||||||
|  |         title: '{% trans "Complete Build Order" %}', | ||||||
|  |         preFormContent: html, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| /* | /* | ||||||
|  * Construct a set of output buttons for a particular build output |  * Construct a set of output buttons for a particular build output | ||||||
|  */ |  */ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user