From a816c14b95ff67b44ae8f1b72d6e0668bd411abb Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 8 Jun 2022 07:45:42 +1000 Subject: [PATCH] Allow build orders to be deleted via the API (#3155) --- InvenTree/build/api.py | 16 ++++++++- .../build/templates/build/build_base.html | 6 ++-- .../build/templates/build/delete_build.html | 7 ---- InvenTree/build/test_api.py | 35 ++++++++++++++++++- InvenTree/build/urls.py | 9 ++--- InvenTree/build/views.py | 10 ------ 6 files changed, 56 insertions(+), 27 deletions(-) delete mode 100644 InvenTree/build/templates/build/delete_build.html diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 89e929256c..295035bd99 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -1,8 +1,10 @@ """JSON API for the Build app.""" from django.urls import include, re_path +from django.utils.translation import gettext_lazy as _ from rest_framework import filters, generics +from rest_framework.exceptions import ValidationError from django_filters.rest_framework import DjangoFilterBackend from django_filters import rest_framework as rest_filters @@ -198,12 +200,24 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) -class BuildDetail(generics.RetrieveUpdateAPIView): +class BuildDetail(generics.RetrieveUpdateDestroyAPIView): """API endpoint for detail view of a Build object.""" queryset = Build.objects.all() serializer_class = build.serializers.BuildSerializer + def destroy(self, request, *args, **kwargs): + """Only allow deletion of a BuildOrder if the build status is CANCELLED""" + + build = self.get_object() + + if build.status != BuildStatus.CANCELLED: + raise ValidationError({ + "non_field_errors": [_("Build must be cancelled before it can be deleted")] + }) + + return super().destroy(request, *args, **kwargs) + class BuildUnallocate(generics.CreateAPIView): """API endpoint for unallocating stock items from a build order. diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 5da87bea7c..d006d0b6ab 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -249,9 +249,11 @@ src="{% static 'img/blank_image.png' %}" {% endif %} $("#build-delete").on('click', function() { - launchModalForm( - "{% url 'build-delete' build.id %}", + constructForm( + '{% url "api-build-detail" build.pk %}', { + method: 'DELETE', + title: '{% trans "Delete Build Order" %}', redirect: "{% url 'build-index' %}", } ); diff --git a/InvenTree/build/templates/build/delete_build.html b/InvenTree/build/templates/build/delete_build.html deleted file mode 100644 index 62dab01da0..0000000000 --- a/InvenTree/build/templates/build/delete_build.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "modal_delete_form.html" %} -{% load i18n %} -{% block pre_form_content %} - -{% trans "Are you sure you want to delete this build?" %} - -{% endblock %} diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index c565506eae..abd25e7d42 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -90,7 +90,7 @@ class BuildAPITest(InvenTreeAPITestCase): # Required roles to access Build API endpoints roles = [ 'build.change', - 'build.add' + 'build.add', ] @@ -268,6 +268,39 @@ class BuildTest(BuildAPITest): self.assertEqual(bo.status, BuildStatus.CANCELLED) + def test_delete(self): + """Test that we can delete a BuildOrder via the API""" + + bo = Build.objects.get(pk=1) + + url = reverse('api-build-detail', kwargs={'pk': bo.pk}) + + # At first we do not have the required permissions + self.delete( + url, + expected_code=403, + ) + + self.assignRole('build.delete') + + # As build is currently not 'cancelled', it cannot be deleted + self.delete( + url, + expected_code=400, + ) + + bo.status = BuildStatus.CANCELLED + bo.save() + + # Now, we should be able to delete + self.delete( + url, + expected_code=204, + ) + + with self.assertRaises(Build.DoesNotExist): + Build.objects.get(pk=1) + def test_create_delete_output(self): """Test that we can create and delete build outputs via the API.""" bo = Build.objects.get(pk=1) diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index b524df5627..0b33c7d78e 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -4,15 +4,12 @@ from django.urls import include, re_path from . import views -build_detail_urls = [ - re_path(r'^delete/', views.BuildDelete.as_view(), name='build-delete'), - - re_path(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), -] build_urls = [ - re_path(r'^(?P\d+)/', include(build_detail_urls)), + re_path(r'^(?P\d+)/', include([ + re_path(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), + ])), re_path(r'.*$', views.BuildIndex.as_view(), name='build-index'), ] diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 9d01ddc3d6..bccd1b31e6 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -1,11 +1,9 @@ """Django views for interacting with Build objects.""" -from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, ListView from .models import Build -from InvenTree.views import AjaxDeleteView from InvenTree.views import InvenTreeRoleMixin from InvenTree.status_codes import BuildStatus @@ -49,11 +47,3 @@ class BuildDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): ctx['has_untracked_bom_items'] = build.has_untracked_bom_items() return ctx - - -class BuildDelete(AjaxDeleteView): - """View to delete a build.""" - - model = Build - ajax_template_name = 'build/delete_build.html' - ajax_form_title = _('Delete Build Order')