mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +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 .models import Build, BuildItem, BuildOrderAttachment | ||||
| from .serializers import BuildAttachmentSerializer, BuildCompleteSerializer, BuildSerializer, BuildItemSerializer | ||||
| from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer | ||||
| import build.serializers | ||||
| from users.models import Owner | ||||
|  | ||||
|  | ||||
| @@ -80,7 +79,7 @@ class BuildList(generics.ListCreateAPIView): | ||||
|     """ | ||||
|  | ||||
|     queryset = Build.objects.all() | ||||
|     serializer_class = BuildSerializer | ||||
|     serializer_class = build.serializers.BuildSerializer | ||||
|     filterset_class = BuildFilter | ||||
|  | ||||
|     filter_backends = [ | ||||
| @@ -119,7 +118,7 @@ class BuildList(generics.ListCreateAPIView): | ||||
|  | ||||
|         queryset = super().get_queryset().select_related('part') | ||||
|  | ||||
|         queryset = BuildSerializer.annotate_queryset(queryset) | ||||
|         queryset = build.serializers.BuildSerializer.annotate_queryset(queryset) | ||||
|  | ||||
|         return queryset | ||||
|  | ||||
| @@ -203,7 +202,7 @@ class BuildDetail(generics.RetrieveUpdateAPIView): | ||||
|     """ API endpoint for detail view of a Build object """ | ||||
|  | ||||
|     queryset = Build.objects.all() | ||||
|     serializer_class = BuildSerializer | ||||
|     serializer_class = build.serializers.BuildSerializer | ||||
|  | ||||
|  | ||||
| class BuildUnallocate(generics.CreateAPIView): | ||||
| @@ -217,7 +216,7 @@ class BuildUnallocate(generics.CreateAPIView): | ||||
|  | ||||
|     queryset = Build.objects.none() | ||||
|  | ||||
|     serializer_class = BuildUnallocationSerializer | ||||
|     serializer_class = build.serializers.BuildUnallocationSerializer | ||||
|  | ||||
|     def get_serializer_context(self): | ||||
|  | ||||
| @@ -233,14 +232,36 @@ class BuildUnallocate(generics.CreateAPIView): | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| class BuildComplete(generics.CreateAPIView): | ||||
| class BuildOutputComplete(generics.CreateAPIView): | ||||
|     """ | ||||
|     API endpoint for completing build outputs | ||||
|     """ | ||||
|  | ||||
|     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): | ||||
|         ctx = super().get_serializer_context() | ||||
| @@ -269,7 +290,7 @@ class BuildAllocate(generics.CreateAPIView): | ||||
|  | ||||
|     queryset = Build.objects.none() | ||||
|  | ||||
|     serializer_class = BuildAllocationSerializer | ||||
|     serializer_class = build.serializers.BuildAllocationSerializer | ||||
|  | ||||
|     def get_serializer_context(self): | ||||
|         """ | ||||
| @@ -294,7 +315,7 @@ class BuildItemDetail(generics.RetrieveUpdateDestroyAPIView): | ||||
|     """ | ||||
|  | ||||
|     queryset = BuildItem.objects.all() | ||||
|     serializer_class = BuildItemSerializer | ||||
|     serializer_class = build.serializers.BuildItemSerializer | ||||
|  | ||||
|  | ||||
| class BuildItemList(generics.ListCreateAPIView): | ||||
| @@ -304,7 +325,7 @@ class BuildItemList(generics.ListCreateAPIView): | ||||
|     - POST: Create a new BuildItem object | ||||
|     """ | ||||
|  | ||||
|     serializer_class = BuildItemSerializer | ||||
|     serializer_class = build.serializers.BuildItemSerializer | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|  | ||||
| @@ -373,7 +394,7 @@ class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin): | ||||
|     """ | ||||
|  | ||||
|     queryset = BuildOrderAttachment.objects.all() | ||||
|     serializer_class = BuildAttachmentSerializer | ||||
|     serializer_class = build.serializers.BuildAttachmentSerializer | ||||
|  | ||||
|     filter_backends = [ | ||||
|         DjangoFilterBackend, | ||||
| @@ -390,7 +411,7 @@ class BuildAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMix | ||||
|     """ | ||||
|  | ||||
|     queryset = BuildOrderAttachment.objects.all() | ||||
|     serializer_class = BuildAttachmentSerializer | ||||
|     serializer_class = build.serializers.BuildAttachmentSerializer | ||||
|  | ||||
|  | ||||
| build_api_urls = [ | ||||
| @@ -410,7 +431,8 @@ 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'^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'^.*$', 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): | ||||
|     """ Form for cancelling a build """ | ||||
|  | ||||
|   | ||||
| @@ -555,7 +555,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): | ||||
|         if self.incomplete_count > 0: | ||||
|             return False | ||||
|  | ||||
|         if self.completed < self.quantity: | ||||
|         if self.remaining > 0: | ||||
|             return False | ||||
|  | ||||
|         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 | ||||
|     """ | ||||
| @@ -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): | ||||
|     """ | ||||
|     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" %}' | ||||
|         ); | ||||
|         {% else %} | ||||
|         launchModalForm( | ||||
|             "{% url 'build-complete' build.id %}", | ||||
|             { | ||||
|                 reload: true, | ||||
|                 submit_text: '{% trans "Complete Build" %}', | ||||
|             } | ||||
|         ); | ||||
|  | ||||
|         completeBuildOrder({{ build.pk }}, { | ||||
|             allocated: {% if build.areUntrackedPartsFullyAllocated %}true{% else %}false{% endif %}, | ||||
|             completed: {% if build.remaining == 0 %}true{% else %}false{% 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.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): | ||||
|         """ | ||||
| @@ -58,7 +58,7 @@ class BuildCompleteTest(BuildAPITest): | ||||
|  | ||||
|         # Test with an invalid build ID | ||||
|         self.post( | ||||
|             reverse('api-build-complete', kwargs={'pk': 99999}), | ||||
|             reverse('api-build-output-complete', kwargs={'pk': 99999}), | ||||
|             {}, | ||||
|             expected_code=400 | ||||
|         ) | ||||
|   | ||||
| @@ -11,7 +11,6 @@ build_detail_urls = [ | ||||
|     url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'), | ||||
|     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'^complete/', views.BuildComplete.as_view(), name='build-complete'), | ||||
|  | ||||
|     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): | ||||
|     """ | ||||
|     Detail view of a single Build object. | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
|  | ||||
| /* exported | ||||
|     allocateStockToBuild, | ||||
|     completeBuildOrder, | ||||
|     editBuildOrder, | ||||
|     loadAllocationTable, | ||||
|     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 | ||||
|  */ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user