From bd3d6f47a12efeba8601eadab4451ce52370944b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 4 May 2022 16:30:46 +1000 Subject: [PATCH] Refactor CancelBuild form --- InvenTree/build/api.py | 8 +++ InvenTree/build/forms.py | 19 ------- InvenTree/build/models.py | 49 +++++++++++++++++-- InvenTree/build/serializers.py | 46 +++++++++++++++++ .../build/templates/build/build_base.html | 12 +++-- InvenTree/build/templates/build/cancel.html | 7 --- InvenTree/build/test_build.py | 2 +- InvenTree/build/tests.py | 2 +- InvenTree/build/urls.py | 1 - InvenTree/build/views.py | 31 ------------ InvenTree/templates/js/translated/build.js | 44 +++++++++++++++++ 11 files changed, 153 insertions(+), 68 deletions(-) delete mode 100644 InvenTree/build/templates/build/cancel.html diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 3d8d3f984c..e32b404ae2 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -322,6 +322,13 @@ class BuildAllocate(BuildOrderContextMixin, generics.CreateAPIView): 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): """ 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'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'), 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'^.*$', BuildDetail.as_view(), name='api-build-detail'), ])), diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 08714b0f3c..77a42571d8 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -5,22 +5,3 @@ Django Forms for interacting with Build objects # -*- coding: utf-8 -*- 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' - ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 43bca0e238..464ef33f8d 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -479,6 +479,16 @@ class Build(MPTTModel, ReferenceIndexingMixin): return outputs + @property + def complete_count(self): + + quantity = 0 + + for output in self.complete_outputs: + quantity += output.quantity + + return quantity + @property def incomplete_outputs(self): """ @@ -588,7 +598,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): trigger_event('build.completed', id=self.pk) @transaction.atomic - def cancelBuild(self, user): + def cancel_build(self, user, **kwargs): """ Mark the Build as CANCELLED - Delete any pending BuildItem objects (but do not remove items from stock) @@ -596,8 +606,23 @@ class Build(MPTTModel, ReferenceIndexingMixin): - Save the Build object """ - for item in self.allocated_stock.all(): - item.delete() + remove_allocated_stock = kwargs.get('remove_allocated_stock', False) + 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 self.completion_date = datetime.now().date() @@ -1025,6 +1050,24 @@ class Build(MPTTModel, ReferenceIndexingMixin): # All parts must be fully allocated! 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): """ Returns True if the un-tracked parts are fully allocated for this BuildOrder diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index bed4b59203..0f1703750c 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -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): """ DRF serializer for marking a BuildOrder as complete diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 4d2c77278c..558c3dc3f2 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -214,11 +214,13 @@ src="{% static 'img/blank_image.png' %}" }); $("#build-cancel").click(function() { - launchModalForm("{% url 'build-cancel' build.id %}", - { - reload: true, - submit_text: '{% trans "Cancel Build" %}', - }); + + cancelBuildOrder( + {{ build.pk }}, + { + reload: true, + } + ); }); $("#build-complete").on('click', function() { diff --git a/InvenTree/build/templates/build/cancel.html b/InvenTree/build/templates/build/cancel.html deleted file mode 100644 index 48d8ca09bd..0000000000 --- a/InvenTree/build/templates/build/cancel.html +++ /dev/null @@ -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 %} \ No newline at end of file diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index 914cef29ab..e2cda00ec9 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -304,7 +304,7 @@ class BuildTest(BuildTestBase): """ self.allocate_stock(50, 50, 200, self.output_1) - self.build.cancelBuild(None) + self.build.cancel_build(None) self.assertEqual(BuildItem.objects.count(), 0) """ diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 7afd078ce9..27b7720973 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -107,7 +107,7 @@ class BuildTestSimple(TestCase): self.assertEqual(build.status, BuildStatus.PENDING) - build.cancelBuild(self.user) + build.cancel_build(self.user) self.assertEqual(build.status, BuildStatus.CANCELLED) diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 520d97ef6a..0788a1de37 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -7,7 +7,6 @@ from django.urls import include, re_path from . import views 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'^.*$', views.BuildDetail.as_view(), name='build-detail'), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 2b8629afe9..ff12d2f211 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -43,37 +43,6 @@ class BuildIndex(InvenTreeRoleMixin, ListView): 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): """ Detail view of a single Build object. diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index d68b319a25..a636cfeec8 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -21,6 +21,7 @@ /* exported allocateStockToBuild, autoAllocateStockToBuild, + cancelBuildOrder, completeBuildOrder, createBuildOutput, 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 = ` +
+ {% trans "Are you sure you wish to cancel this build?" %} +
`; + + if (opts.context.has_allocated_stock) { + html += ` +
+ {% trans "Stock items have been allocated to this build order" %} +
`; + } + + if (opts.context.incomplete_outputs) { + html += ` +
+ {% trans "There are incomplete outputs remaining for this build order" %} +
`; + } + + return html; + }, + onSuccess: function(response) { + handleFormSuccess(response, options); + } + } + ); +} + + /* Construct a form to "complete" (finish) a build order */ function completeBuildOrder(build_id, options={}) {