2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-01 13:06:45 +00:00

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

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): class CancelBuildForm(HelperForm):
""" Form for cancelling a build """ """ Form for cancelling a build """

View File

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

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

View File

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

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

View File

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

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): class BuildDetail(InvenTreeRoleMixin, DetailView):
""" """
Detail view of a single Build object. Detail view of a single Build object.

View File

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