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:
commit
c1ef9a445a
@ -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'),
|
||||||
])),
|
])),
|
||||||
|
@ -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 """
|
||||||
|
|
||||||
|
@ -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():
|
||||||
|
@ -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
|
||||||
|
@ -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 %}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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.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
|
||||||
)
|
)
|
||||||
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user