Adds API endpoint to delete build outputs

This commit is contained in:
Oliver 2022-02-10 00:46:26 +11:00
parent 18ac1ceebe
commit 96af074365
6 changed files with 253 additions and 13 deletions

View File

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

View File

@ -245,6 +245,7 @@ class BuildOutputComplete(generics.CreateAPIView):
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))
@ -254,6 +255,29 @@ class BuildOutputComplete(generics.CreateAPIView):
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()
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)
@ -432,6 +456,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'),

View File

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

View File

@ -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,6 +156,8 @@ class BuildOutputSerializer(serializers.Serializer):
if not output.is_building:
raise ValidationError(_("This build output has already been completed"))
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"))
@ -165,6 +170,50 @@ 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

View File

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

View File

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