diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 4d47cf9076..733799f890 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -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\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'), ])), diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 19bf3566dc..43899ba819 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -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 """ diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 392c773e6b..f03cb30c74 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -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(): diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 452864e3c4..55f89c1844 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -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 diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 48ef98b2b1..312accb18f 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -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 %} }); diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html deleted file mode 100644 index eeedc027dd..0000000000 --- a/InvenTree/build/templates/build/complete.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "modal_form.html" %} -{% load i18n %} - -{% block pre_form_content %} - -{% if build.can_complete %} -
- {% trans "Build Order is complete" %} -
-{% else %} -
- {% trans "Build Order is incomplete" %}
- -
-{% endif %} -{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index e2b6448f2f..45662a58d6 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -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 ) diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 8ea339ae26..fecece232e 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -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'), ] diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 1d28cb8d50..1a933af835 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -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. diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 0deec4f859..ebb37fa61c 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -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 += ` +
+ {% trans "Build Order is incomplete" %} +
+ `; + + if (!options.allocated) { + html += `
{% trans "Required stock has not been fully allocated" %}
`; + } + + if (!options.completed) { + html += `
{% trans "Required build quantity has not been completed" %}
`; + } + } + + // 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 */