Merge pull request #2510 from SchrodingersGat/build-order-complete-improvements

Adds confirmation inputs when completing build order
This commit is contained in:
Oliver 2022-01-07 12:45:34 +11:00 committed by GitHub
commit c1ef9a445a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 138 additions and 103 deletions

View File

@ -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'),
])),

View File

@ -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 """

View File

@ -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():

View File

@ -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

View File

@ -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 %}
});

View File

@ -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 %}

View File

@ -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
)

View File

@ -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'),
]

View File

@ -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.

View File

@ -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
*/