mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2510 from SchrodingersGat/build-order-complete-improvements
Adds confirmation inputs when completing build order
This commit is contained in:
commit
c1ef9a445a
@ -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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user