mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
Refactor CancelBuild form
This commit is contained in:
parent
768e23c7b8
commit
bd3d6f47a1
@ -322,6 +322,13 @@ class BuildAllocate(BuildOrderContextMixin, generics.CreateAPIView):
|
|||||||
serializer_class = build.serializers.BuildAllocationSerializer
|
serializer_class = build.serializers.BuildAllocationSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class BuildCancel(BuildOrderContextMixin, generics.CreateAPIView):
|
||||||
|
""" API endpoint for cancelling a BuildOrder """
|
||||||
|
|
||||||
|
queryset = Build.objects.all()
|
||||||
|
serializer_class = build.serializers.BuildCancelSerializer
|
||||||
|
|
||||||
|
|
||||||
class BuildItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
class BuildItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint for detail view of a BuildItem object
|
API endpoint for detail view of a BuildItem object
|
||||||
@ -462,6 +469,7 @@ build_api_urls = [
|
|||||||
re_path(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
|
re_path(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
|
||||||
re_path(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
|
re_path(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
|
||||||
re_path(r'^finish/', BuildFinish.as_view(), name='api-build-finish'),
|
re_path(r'^finish/', BuildFinish.as_view(), name='api-build-finish'),
|
||||||
|
re_path(r'^cancel/', BuildCancel.as_view(), name='api-build-cancel'),
|
||||||
re_path(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
re_path(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
||||||
re_path(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
|
re_path(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
|
||||||
])),
|
])),
|
||||||
|
@ -5,22 +5,3 @@ Django Forms for interacting with Build objects
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
|
||||||
|
|
||||||
from .models import Build
|
|
||||||
|
|
||||||
|
|
||||||
class CancelBuildForm(HelperForm):
|
|
||||||
""" Form for cancelling a build """
|
|
||||||
|
|
||||||
confirm_cancel = forms.BooleanField(required=False, label=_('Confirm cancel'), help_text=_('Confirm build cancellation'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Build
|
|
||||||
fields = [
|
|
||||||
'confirm_cancel'
|
|
||||||
]
|
|
||||||
|
@ -479,6 +479,16 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
return outputs
|
return outputs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def complete_count(self):
|
||||||
|
|
||||||
|
quantity = 0
|
||||||
|
|
||||||
|
for output in self.complete_outputs:
|
||||||
|
quantity += output.quantity
|
||||||
|
|
||||||
|
return quantity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def incomplete_outputs(self):
|
def incomplete_outputs(self):
|
||||||
"""
|
"""
|
||||||
@ -588,7 +598,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
trigger_event('build.completed', id=self.pk)
|
trigger_event('build.completed', id=self.pk)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def cancelBuild(self, user):
|
def cancel_build(self, user, **kwargs):
|
||||||
""" Mark the Build as CANCELLED
|
""" Mark the Build as CANCELLED
|
||||||
|
|
||||||
- Delete any pending BuildItem objects (but do not remove items from stock)
|
- Delete any pending BuildItem objects (but do not remove items from stock)
|
||||||
@ -596,8 +606,23 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
- Save the Build object
|
- Save the Build object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for item in self.allocated_stock.all():
|
remove_allocated_stock = kwargs.get('remove_allocated_stock', False)
|
||||||
item.delete()
|
remove_incomplete_outputs = kwargs.get('remove_incomplete_outputs', False)
|
||||||
|
|
||||||
|
# Handle stock allocations
|
||||||
|
for build_item in self.allocated_stock.all():
|
||||||
|
|
||||||
|
if remove_allocated_stock:
|
||||||
|
build_item.complete_allocation(user)
|
||||||
|
|
||||||
|
build_item.delete()
|
||||||
|
|
||||||
|
# Remove incomplete outputs (if required)
|
||||||
|
if remove_incomplete_outputs:
|
||||||
|
outputs = self.build_outputs.filter(is_building=True)
|
||||||
|
|
||||||
|
for output in outputs:
|
||||||
|
output.delete()
|
||||||
|
|
||||||
# Date of 'completion' is the date the build was cancelled
|
# Date of 'completion' is the date the build was cancelled
|
||||||
self.completion_date = datetime.now().date()
|
self.completion_date = datetime.now().date()
|
||||||
@ -1025,6 +1050,24 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
# All parts must be fully allocated!
|
# All parts must be fully allocated!
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def is_partially_allocated(self, output):
|
||||||
|
"""
|
||||||
|
Returns True if the particular build output is (at least) partially allocated
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If output is not specified, we are talking about "untracked" items
|
||||||
|
if output is None:
|
||||||
|
bom_items = self.untracked_bom_items
|
||||||
|
else:
|
||||||
|
bom_items = self.tracked_bom_items
|
||||||
|
|
||||||
|
for bom_item in bom_items:
|
||||||
|
|
||||||
|
if self.allocated_quantity(bom_item, output) > 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def are_untracked_parts_allocated(self):
|
def are_untracked_parts_allocated(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the un-tracked parts are fully allocated for this BuildOrder
|
Returns True if the un-tracked parts are fully allocated for this BuildOrder
|
||||||
|
@ -438,6 +438,52 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildCancelSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'remove_allocated_stock',
|
||||||
|
'remove_incomplete_outputs',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
|
||||||
|
build = self.context['build']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'has_allocated_stock': build.is_partially_allocated(None),
|
||||||
|
'incomplete_outputs': build.incomplete_count,
|
||||||
|
'completed_outputs': build.complete_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_allocated_stock = serializers.BooleanField(
|
||||||
|
label=_('Remove Allocated Stock'),
|
||||||
|
help_text=_('Subtract any stock which has already been allocated to this build'),
|
||||||
|
required=False,
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_incomplete_outputs = serializers.BooleanField(
|
||||||
|
label=_('Remove Incomplete Outputs'),
|
||||||
|
help_text=_('Delete any build outputs which have not been completed'),
|
||||||
|
required=False,
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
|
||||||
|
build = self.context['build']
|
||||||
|
request = self.context['request']
|
||||||
|
|
||||||
|
data = self.validated_data
|
||||||
|
|
||||||
|
build.cancel_build(
|
||||||
|
request.user,
|
||||||
|
remove_allocated_stock=data.get('remove_unallocated_stock', False),
|
||||||
|
remove_incomplete_outputs=data.get('remove_incomplete_outputs', False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BuildCompleteSerializer(serializers.Serializer):
|
class BuildCompleteSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
DRF serializer for marking a BuildOrder as complete
|
DRF serializer for marking a BuildOrder as complete
|
||||||
|
@ -214,11 +214,13 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#build-cancel").click(function() {
|
$("#build-cancel").click(function() {
|
||||||
launchModalForm("{% url 'build-cancel' build.id %}",
|
|
||||||
{
|
cancelBuildOrder(
|
||||||
reload: true,
|
{{ build.pk }},
|
||||||
submit_text: '{% trans "Cancel Build" %}',
|
{
|
||||||
});
|
reload: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#build-complete").on('click', function() {
|
$("#build-complete").on('click', function() {
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{% extends "modal_form.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block pre_form_content %}
|
|
||||||
|
|
||||||
{% trans "Are you sure you wish to cancel this build?" %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -304,7 +304,7 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
self.allocate_stock(50, 50, 200, self.output_1)
|
self.allocate_stock(50, 50, 200, self.output_1)
|
||||||
self.build.cancelBuild(None)
|
self.build.cancel_build(None)
|
||||||
|
|
||||||
self.assertEqual(BuildItem.objects.count(), 0)
|
self.assertEqual(BuildItem.objects.count(), 0)
|
||||||
"""
|
"""
|
||||||
|
@ -107,7 +107,7 @@ class BuildTestSimple(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(build.status, BuildStatus.PENDING)
|
self.assertEqual(build.status, BuildStatus.PENDING)
|
||||||
|
|
||||||
build.cancelBuild(self.user)
|
build.cancel_build(self.user)
|
||||||
|
|
||||||
self.assertEqual(build.status, BuildStatus.CANCELLED)
|
self.assertEqual(build.status, BuildStatus.CANCELLED)
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ from django.urls import include, re_path
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
build_detail_urls = [
|
build_detail_urls = [
|
||||||
re_path(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
|
|
||||||
re_path(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
|
re_path(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
|
||||||
|
|
||||||
re_path(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
re_path(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
||||||
|
@ -43,37 +43,6 @@ class BuildIndex(InvenTreeRoleMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class BuildCancel(AjaxUpdateView):
|
|
||||||
""" View to cancel a Build.
|
|
||||||
Provides a cancellation information dialog
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Build
|
|
||||||
ajax_template_name = 'build/cancel.html'
|
|
||||||
ajax_form_title = _('Cancel Build')
|
|
||||||
context_object_name = 'build'
|
|
||||||
form_class = forms.CancelBuildForm
|
|
||||||
|
|
||||||
def validate(self, build, form, **kwargs):
|
|
||||||
|
|
||||||
confirm = str2bool(form.cleaned_data.get('confirm_cancel', False))
|
|
||||||
|
|
||||||
if not confirm:
|
|
||||||
form.add_error('confirm_cancel', _('Confirm build cancellation'))
|
|
||||||
|
|
||||||
def save(self, build, form, **kwargs):
|
|
||||||
"""
|
|
||||||
Cancel the build.
|
|
||||||
"""
|
|
||||||
|
|
||||||
build.cancelBuild(self.request.user)
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return {
|
|
||||||
'danger': _('Build was cancelled')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BuildDetail(InvenTreeRoleMixin, DetailView):
|
class BuildDetail(InvenTreeRoleMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Detail view of a single Build object.
|
Detail view of a single Build object.
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
/* exported
|
/* exported
|
||||||
allocateStockToBuild,
|
allocateStockToBuild,
|
||||||
autoAllocateStockToBuild,
|
autoAllocateStockToBuild,
|
||||||
|
cancelBuildOrder,
|
||||||
completeBuildOrder,
|
completeBuildOrder,
|
||||||
createBuildOutput,
|
createBuildOutput,
|
||||||
editBuildOrder,
|
editBuildOrder,
|
||||||
@ -123,6 +124,49 @@ function newBuildOrder(options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Construct a form to cancel a build order */
|
||||||
|
function cancelBuildOrder(build_id, options={}) {
|
||||||
|
|
||||||
|
constructForm(
|
||||||
|
`/api/build/${build_id}/cancel/`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
title: '{% trans "Cancel Build Order" %}',
|
||||||
|
confirm: true,
|
||||||
|
fields: {
|
||||||
|
remove_allocated_stock: {},
|
||||||
|
remove_incomplete_outputs: {},
|
||||||
|
},
|
||||||
|
preFormContent: function(opts) {
|
||||||
|
var html = `
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
{% trans "Are you sure you wish to cancel this build?" %}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if (opts.context.has_allocated_stock) {
|
||||||
|
html += `
|
||||||
|
<div class='alert alert-block alert-warning'>
|
||||||
|
{% trans "Stock items have been allocated to this build order" %}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.context.incomplete_outputs) {
|
||||||
|
html += `
|
||||||
|
<div class='alert alert-block alert-warning'>
|
||||||
|
{% trans "There are incomplete outputs remaining for this build order" %}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
onSuccess: function(response) {
|
||||||
|
handleFormSuccess(response, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Construct a form to "complete" (finish) a build order */
|
/* Construct a form to "complete" (finish) a build order */
|
||||||
function completeBuildOrder(build_id, options={}) {
|
function completeBuildOrder(build_id, options={}) {
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user