mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2615 from SchrodingersGat/delete-multiple-outputs
Delete multiple outputs
This commit is contained in:
commit
6c083622e5
@ -12,11 +12,14 @@ import common.models
|
||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 23
|
||||
INVENTREE_API_VERSION = 24
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v24 -> 2022-02-10
|
||||
- Adds API endpoint for deleting (cancelling) build order outputs
|
||||
|
||||
v23 -> 2022-02-02
|
||||
- Adds API endpoints for managing plugin classes
|
||||
- Adds API endpoints for managing plugin settings
|
||||
|
@ -241,6 +241,29 @@ class BuildOutputComplete(generics.CreateAPIView):
|
||||
|
||||
serializer_class = build.serializers.BuildOutputCompleteSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
ctx['request'] = self.request
|
||||
ctx['to_complete'] = True
|
||||
|
||||
try:
|
||||
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
||||
except:
|
||||
pass
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class BuildOutputDelete(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for deleting multiple build outputs
|
||||
"""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
serializer_class = build.serializers.BuildOutputDeleteSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
@ -432,6 +455,7 @@ build_api_urls = [
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
|
||||
url(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
|
||||
url(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
|
||||
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'),
|
||||
|
@ -59,30 +59,6 @@ class BuildOutputCreateForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class BuildOutputDeleteForm(HelperForm):
|
||||
"""
|
||||
Form for deleting a build output.
|
||||
"""
|
||||
|
||||
confirm = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Confirm'),
|
||||
help_text=_('Confirm deletion of build output')
|
||||
)
|
||||
|
||||
output_id = forms.IntegerField(
|
||||
required=True,
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Build
|
||||
fields = [
|
||||
'confirm',
|
||||
'output_id',
|
||||
]
|
||||
|
||||
|
||||
class CancelBuildForm(HelperForm):
|
||||
""" Form for cancelling a build """
|
||||
|
||||
|
@ -708,7 +708,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def deleteBuildOutput(self, output):
|
||||
def delete_output(self, output):
|
||||
"""
|
||||
Remove a build output from the database:
|
||||
|
||||
|
@ -141,6 +141,9 @@ class BuildOutputSerializer(serializers.Serializer):
|
||||
|
||||
build = self.context['build']
|
||||
|
||||
# As this serializer can be used in multiple contexts, we need to work out why we are here
|
||||
to_complete = self.context.get('to_complete', False)
|
||||
|
||||
# The stock item must point to the build
|
||||
if output.build != build:
|
||||
raise ValidationError(_("Build output does not match the parent build"))
|
||||
@ -153,9 +156,11 @@ class BuildOutputSerializer(serializers.Serializer):
|
||||
if not output.is_building:
|
||||
raise ValidationError(_("This build output has already been completed"))
|
||||
|
||||
# The build output must have all tracked parts allocated
|
||||
if not build.isFullyAllocated(output):
|
||||
raise ValidationError(_("This build output is not fully allocated"))
|
||||
if to_complete:
|
||||
|
||||
# The build output must have all tracked parts allocated
|
||||
if not build.isFullyAllocated(output):
|
||||
raise ValidationError(_("This build output is not fully allocated"))
|
||||
|
||||
return output
|
||||
|
||||
@ -165,6 +170,48 @@ class BuildOutputSerializer(serializers.Serializer):
|
||||
]
|
||||
|
||||
|
||||
class BuildOutputDeleteSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for deleting (cancelling) one or more build outputs
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
'outputs',
|
||||
]
|
||||
|
||||
outputs = BuildOutputSerializer(
|
||||
many=True,
|
||||
required=True,
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
data = super().validate(data)
|
||||
|
||||
outputs = data.get('outputs', [])
|
||||
|
||||
if len(outputs) == 0:
|
||||
raise ValidationError(_("A list of build outputs must be provided"))
|
||||
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
'save' the serializer to delete the build outputs
|
||||
"""
|
||||
|
||||
data = self.validated_data
|
||||
outputs = data.get('outputs', [])
|
||||
|
||||
build = self.context['build']
|
||||
|
||||
with transaction.atomic():
|
||||
for item in outputs:
|
||||
output = item['output']
|
||||
build.delete_output(output)
|
||||
|
||||
|
||||
class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for completing one or more build outputs
|
||||
|
@ -243,13 +243,16 @@
|
||||
|
||||
<!-- Build output actions -->
|
||||
<div class='btn-group'>
|
||||
<button id='output-options' class='btn btn-primary dropdown-toiggle' type='button' data-bs-toggle='dropdown' title='{% trans "Output Actions" %}'>
|
||||
<button id='output-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle='dropdown' title='{% trans "Output Actions" %}'>
|
||||
<span class='fas fa-tools'></span> <span class='caret'></span>
|
||||
</button>
|
||||
<ul class='dropdown-menu'>
|
||||
<li><a class='dropdown-item' href='#' id='multi-output-complete' title='{% trans "Complete selected items" %}'>
|
||||
<li><a class='dropdown-item' href='#' id='multi-output-complete' title='{% trans "Complete selected build outputs" %}'>
|
||||
<span class='fas fa-check-circle icon-green'></span> {% trans "Complete outputs" %}
|
||||
</a></li>
|
||||
<li><a class='dropdown-item' href='#' id='multi-output-delete' title='{% trans "Delete selected build outputs" %}'>
|
||||
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete outputs" %}
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% include "filter_list.html" with id='incompletebuilditems' %}
|
||||
@ -372,6 +375,7 @@ inventreeGet(
|
||||
[
|
||||
'#output-options',
|
||||
'#multi-output-complete',
|
||||
'#multi-output-delete',
|
||||
]
|
||||
);
|
||||
|
||||
@ -393,6 +397,24 @@ inventreeGet(
|
||||
);
|
||||
});
|
||||
|
||||
$('#multi-output-delete').click(function() {
|
||||
var outputs = $('#build-output-table').bootstrapTable('getSelections');
|
||||
|
||||
deleteBuildOutputs(
|
||||
build_info.pk,
|
||||
outputs,
|
||||
{
|
||||
success: function() {
|
||||
// Reload the "in progress" table
|
||||
$('#build-output-table').bootstrapTable('refresh');
|
||||
|
||||
// Reload the "completed" table
|
||||
$('#build-stock-table').bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if build.active and build.has_untracked_bom_items %}
|
||||
|
@ -10,7 +10,6 @@ build_detail_urls = [
|
||||
url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
|
||||
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'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
||||
]
|
||||
|
@ -12,7 +12,6 @@ from django.forms import HiddenInput
|
||||
|
||||
from .models import Build
|
||||
from . import forms
|
||||
from stock.models import StockItem
|
||||
|
||||
from InvenTree.views import AjaxUpdateView, AjaxDeleteView
|
||||
from InvenTree.views import InvenTreeRoleMixin
|
||||
@ -192,67 +191,6 @@ class BuildOutputCreate(AjaxUpdateView):
|
||||
return form
|
||||
|
||||
|
||||
class BuildOutputDelete(AjaxUpdateView):
|
||||
"""
|
||||
Delete a build output (StockItem) for a given build.
|
||||
|
||||
Form is a simple confirmation dialog
|
||||
"""
|
||||
|
||||
model = Build
|
||||
form_class = forms.BuildOutputDeleteForm
|
||||
ajax_form_title = _('Delete Build Output')
|
||||
|
||||
role_required = 'build.delete'
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initials = super().get_initial()
|
||||
|
||||
output = self.get_param('output')
|
||||
|
||||
initials['output_id'] = output
|
||||
|
||||
return initials
|
||||
|
||||
def validate(self, build, form, **kwargs):
|
||||
|
||||
data = form.cleaned_data
|
||||
|
||||
confirm = data.get('confirm', False)
|
||||
|
||||
if not confirm:
|
||||
form.add_error('confirm', _('Confirm unallocation of build stock'))
|
||||
form.add_error(None, _('Check the confirmation box'))
|
||||
|
||||
output_id = data.get('output_id', None)
|
||||
output = None
|
||||
|
||||
try:
|
||||
output = StockItem.objects.get(pk=output_id)
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
if output:
|
||||
if not output.build == build:
|
||||
form.add_error(None, _('Build output does not match build'))
|
||||
else:
|
||||
form.add_error(None, _('Build output must be specified'))
|
||||
|
||||
def save(self, build, form, **kwargs):
|
||||
|
||||
output_id = form.cleaned_data.get('output_id')
|
||||
|
||||
output = StockItem.objects.get(pk=output_id)
|
||||
|
||||
build.deleteBuildOutput(output)
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'danger': _('Build output deleted'),
|
||||
}
|
||||
|
||||
|
||||
class BuildDetail(InvenTreeRoleMixin, DetailView):
|
||||
"""
|
||||
Detail view of a single Build object.
|
||||
|
@ -417,6 +417,145 @@ function completeBuildOutputs(build_id, outputs, options={}) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Launch a modal form to delete selected build outputs
|
||||
*/
|
||||
function deleteBuildOutputs(build_id, outputs, options={}) {
|
||||
|
||||
if (outputs.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "Select Build Outputs" %}',
|
||||
'{% trans "At least one build output must be selected" %}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render a single build output (StockItem)
|
||||
function renderBuildOutput(output, opts={}) {
|
||||
var pk = output.pk;
|
||||
|
||||
var output_html = imageHoverIcon(output.part_detail.thumbnail);
|
||||
|
||||
if (output.quantity == 1 && output.serial) {
|
||||
output_html += `{% trans "Serial Number" %}: ${output.serial}`;
|
||||
} else {
|
||||
output_html += `{% trans "Quantity" %}: ${output.quantity}`;
|
||||
}
|
||||
|
||||
var buttons = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}');
|
||||
|
||||
buttons += '</div>';
|
||||
|
||||
var field = constructField(
|
||||
`outputs_output_${pk}`,
|
||||
{
|
||||
type: 'raw',
|
||||
html: output_html,
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
|
||||
var html = `
|
||||
<tr id='output_row_${pk}'>
|
||||
<td>${field}</td>
|
||||
<td>${output.part_detail.full_name}</td>
|
||||
<td>${buttons}</td>
|
||||
</tr>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// Construct table entries
|
||||
var table_entries = '';
|
||||
|
||||
outputs.forEach(function(output) {
|
||||
table_entries += renderBuildOutput(output);
|
||||
});
|
||||
|
||||
var html = `
|
||||
<table class='table table-striped table-condensed' id='build-complete-table'>
|
||||
<thead>
|
||||
<th colspan='2'>{% trans "Output" %}</th>
|
||||
<th><!-- Actions --></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
${table_entries}
|
||||
</tbody>
|
||||
</table>`;
|
||||
|
||||
constructForm(`/api/build/${build_id}/delete-outputs/`, {
|
||||
method: 'POST',
|
||||
preFormContent: html,
|
||||
fields: {},
|
||||
confirm: true,
|
||||
title: '{% trans "Delete Build Outputs" %}',
|
||||
afterRender: function(fields, opts) {
|
||||
// Setup callbacks to remove outputs
|
||||
$(opts.modal).find('.button-row-remove').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
$(opts.modal).find(`#output_row_${pk}`).remove();
|
||||
});
|
||||
},
|
||||
onSubmit: function(fields, opts) {
|
||||
var data = {
|
||||
outputs: [],
|
||||
};
|
||||
|
||||
var output_pk_values = [];
|
||||
|
||||
outputs.forEach(function(output) {
|
||||
var pk = output.pk;
|
||||
|
||||
var row = $(opts.modal).find(`#output_row_${pk}`);
|
||||
|
||||
if (row.exists()) {
|
||||
data.outputs.push({
|
||||
output: pk
|
||||
});
|
||||
output_pk_values.push(pk);
|
||||
}
|
||||
});
|
||||
|
||||
opts.nested = {
|
||||
'outputs': output_pk_values,
|
||||
};
|
||||
|
||||
inventreePut(
|
||||
opts.url,
|
||||
data,
|
||||
{
|
||||
method: 'POST',
|
||||
success: function(response) {
|
||||
$(opts.modal).modal('hide');
|
||||
|
||||
if (options.success) {
|
||||
options.success(response);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
switch (xhr.status) {
|
||||
case 400:
|
||||
handleFormErrors(xhr.responseJSON, fields, opts);
|
||||
break;
|
||||
default:
|
||||
$(opts.modal).modal('hide');
|
||||
showApiError(xhr, opts.url);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a table showing all the BuildOrder allocations for a given part
|
||||
*/
|
||||
@ -604,15 +743,17 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
$(table).find('.button-output-delete').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
// TODO: Move this to the API
|
||||
launchModalForm(
|
||||
`/build/${build_info.pk}/delete-output/`,
|
||||
var output = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||
|
||||
deleteBuildOutputs(
|
||||
build_info.pk,
|
||||
[
|
||||
output,
|
||||
],
|
||||
{
|
||||
data: {
|
||||
output: pk
|
||||
},
|
||||
success: function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
$('#build-stock-table').bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user