mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Adds API endpoint to delete build outputs
This commit is contained in:
parent
18ac1ceebe
commit
96af074365
@ -12,11 +12,14 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# 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
|
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
|
v23 -> 2022-02-02
|
||||||
- Adds API endpoints for managing plugin classes
|
- Adds API endpoints for managing plugin classes
|
||||||
- Adds API endpoints for managing plugin settings
|
- Adds API endpoints for managing plugin settings
|
||||||
|
@ -245,6 +245,7 @@ class BuildOutputComplete(generics.CreateAPIView):
|
|||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
|
|
||||||
ctx['request'] = self.request
|
ctx['request'] = self.request
|
||||||
|
ctx['to_complete'] = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
||||||
@ -254,6 +255,29 @@ class BuildOutputComplete(generics.CreateAPIView):
|
|||||||
return ctx
|
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):
|
class BuildFinish(generics.CreateAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint for marking a build as finished (completed)
|
API endpoint for marking a build as finished (completed)
|
||||||
@ -432,6 +456,7 @@ build_api_urls = [
|
|||||||
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/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
|
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'^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'),
|
||||||
|
@ -708,7 +708,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def deleteBuildOutput(self, output):
|
def delete_output(self, output):
|
||||||
"""
|
"""
|
||||||
Remove a build output from the database:
|
Remove a build output from the database:
|
||||||
|
|
||||||
|
@ -141,6 +141,9 @@ class BuildOutputSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
build = self.context['build']
|
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
|
# The stock item must point to the build
|
||||||
if output.build != build:
|
if output.build != build:
|
||||||
raise ValidationError(_("Build output does not match the parent build"))
|
raise ValidationError(_("Build output does not match the parent build"))
|
||||||
@ -153,9 +156,11 @@ class BuildOutputSerializer(serializers.Serializer):
|
|||||||
if not output.is_building:
|
if not output.is_building:
|
||||||
raise ValidationError(_("This build output has already been completed"))
|
raise ValidationError(_("This build output has already been completed"))
|
||||||
|
|
||||||
# The build output must have all tracked parts allocated
|
if to_complete:
|
||||||
if not build.isFullyAllocated(output):
|
|
||||||
raise ValidationError(_("This build output is not fully allocated"))
|
# 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
|
return output
|
||||||
|
|
||||||
@ -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):
|
class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
DRF serializer for completing one or more build outputs
|
DRF serializer for completing one or more build outputs
|
||||||
|
@ -243,13 +243,16 @@
|
|||||||
|
|
||||||
<!-- Build output actions -->
|
<!-- Build output actions -->
|
||||||
<div class='btn-group'>
|
<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>
|
<span class='fas fa-tools'></span> <span class='caret'></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class='dropdown-menu'>
|
<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" %}
|
<span class='fas fa-check-circle icon-green'></span> {% trans "Complete outputs" %}
|
||||||
</a></li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% include "filter_list.html" with id='incompletebuilditems' %}
|
{% include "filter_list.html" with id='incompletebuilditems' %}
|
||||||
@ -372,6 +375,7 @@ inventreeGet(
|
|||||||
[
|
[
|
||||||
'#output-options',
|
'#output-options',
|
||||||
'#multi-output-complete',
|
'#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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if build.active and build.has_untracked_bom_items %}
|
{% if build.active and build.has_untracked_bom_items %}
|
||||||
|
@ -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
|
* 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() {
|
$(table).find('.button-output-delete').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
// TODO: Move this to the API
|
var output = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
launchModalForm(
|
|
||||||
`/build/${build_info.pk}/delete-output/`,
|
deleteBuildOutputs(
|
||||||
|
build_info.pk,
|
||||||
|
[
|
||||||
|
output,
|
||||||
|
],
|
||||||
{
|
{
|
||||||
data: {
|
|
||||||
output: pk
|
|
||||||
},
|
|
||||||
success: function() {
|
success: function() {
|
||||||
$(table).bootstrapTable('refresh');
|
$(table).bootstrapTable('refresh');
|
||||||
|
$('#build-stock-table').bootstrapTable('refresh');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user