From 96af07436526394644a0f1aebd560bbf6dd6ace1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 10 Feb 2022 00:46:26 +1100 Subject: [PATCH 1/4] Adds API endpoint to delete build outputs --- InvenTree/InvenTree/version.py | 5 +- InvenTree/build/api.py | 25 ++++ InvenTree/build/models.py | 2 +- InvenTree/build/serializers.py | 55 ++++++- InvenTree/build/templates/build/detail.html | 26 +++- InvenTree/templates/js/translated/build.js | 153 +++++++++++++++++++- 6 files changed, 253 insertions(+), 13 deletions(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 1f8e372d39..19235f0e0a 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -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 diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 733799f890..54204de845 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -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\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'), diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 0eeffd107d..e5bb812083 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -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: diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 2508b02927..fe01844520 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -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,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 diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index b548632b56..ff335d139c 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -243,13 +243,16 @@ {% 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 %} diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 3868ac1b09..7a5860d285 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -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 = `
`; + + buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}'); + + buttons += '
'; + + var field = constructField( + `outputs_output_${pk}`, + { + type: 'raw', + html: output_html, + }, + { + hideLabels: true, + } + ); + + var html = ` + + ${field} + ${output.part_detail.full_name} + ${buttons} + `; + + return html; + } + + // Construct table entries + var table_entries = ''; + + outputs.forEach(function(output) { + table_entries += renderBuildOutput(output); + }); + + var html = ` + + + + + + + ${table_entries} + +
{% trans "Output" %}
`; + + 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'); } } ); From 0d7b94fbfa9f5638bd18dfc2f64b044afdd28032 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 10 Feb 2022 00:48:06 +1100 Subject: [PATCH 2/4] Remove old form code which is no longer used --- InvenTree/build/api.py | 1 - InvenTree/build/forms.py | 24 ------------- InvenTree/build/serializers.py | 2 +- InvenTree/build/urls.py | 1 - InvenTree/build/views.py | 62 ---------------------------------- 5 files changed, 1 insertion(+), 89 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 54204de845..57ffe88cf3 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -277,7 +277,6 @@ class BuildOutputDelete(generics.CreateAPIView): return ctx - class BuildFinish(generics.CreateAPIView): """ API endpoint for marking a build as finished (completed) diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 43899ba819..d242586b3c 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -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 """ diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index fe01844520..efc4665d00 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -157,7 +157,7 @@ class BuildOutputSerializer(serializers.Serializer): 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")) diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index fecece232e..30a9470ee2 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -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'), ] diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 21cf5dda99..a8cf72f5a6 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -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. From 71f9399760b7957259fa7712d00ea5020f308e4b Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 10 Feb 2022 00:50:21 +1100 Subject: [PATCH 3/4] Cleanup --- InvenTree/build/serializers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index efc4665d00..bc9d018cbe 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -208,9 +208,7 @@ class BuildOutputDeleteSerializer(serializers.Serializer): with transaction.atomic(): for item in outputs: - output = item['output'] - build.delete_output(output) From 6b52a07e71858742448d0269fabde4980c130baf Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 10 Feb 2022 00:53:38 +1100 Subject: [PATCH 4/4] js linting --- InvenTree/templates/js/translated/build.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 7a5860d285..5782218780 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -497,9 +497,9 @@ function deleteBuildOutputs(build_id, outputs, options={}) { afterRender: function(fields, opts) { // Setup callbacks to remove outputs $(opts.modal).find('.button-row-remove').click(function() { - var pk = $(this).attr('pk'); + var pk = $(this).attr('pk'); - $(opts.modal).find(`#output_row_${pk}`).remove(); + $(opts.modal).find(`#output_row_${pk}`).remove(); }); }, onSubmit: function(fields, opts) { @@ -550,7 +550,7 @@ function deleteBuildOutputs(build_id, outputs, options={}) { } } } - ) + ); } }); }