diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index cc897d6ec9..7920003d8b 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -21,7 +21,7 @@ from InvenTree.status_codes import BuildStatus from .models import Build, BuildItem, BuildOrderAttachment from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer -from .serializers import BuildAllocationSerializer +from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer class BuildFilter(rest_filters.FilterSet): @@ -184,6 +184,42 @@ class BuildDetail(generics.RetrieveUpdateAPIView): serializer_class = BuildSerializer +class BuildUnallocate(generics.CreateAPIView): + """ + API endpoint for unallocating stock items from a build order + + - The BuildOrder object is specified by the URL + - "output" (StockItem) can optionally be specified + - "bom_item" can optionally be specified + """ + + queryset = Build.objects.none() + + serializer_class = BuildUnallocationSerializer + + def get_build(self): + """ + Returns the BuildOrder associated with this API endpoint + """ + + pk = self.kwargs.get('pk', None) + + try: + build = Build.objects.get(pk=pk) + except (ValueError, Build.DoesNotExist): + raise ValidationError(_("Matching build order does not exist")) + + return build + + def get_serializer_context(self): + + ctx = super().get_serializer_context() + ctx['build'] = self.get_build() + ctx['request'] = self.request + + return ctx + + class BuildAllocate(generics.CreateAPIView): """ API endpoint to allocate stock items to a build order @@ -349,6 +385,7 @@ build_api_urls = [ # Build Detail url(r'^(?P\d+)/', include([ url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'), + url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'), url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'), ])), diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index b3f6cd92de..bc7bdd50f5 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -137,32 +137,6 @@ class BuildOutputDeleteForm(HelperForm): ] -class UnallocateBuildForm(HelperForm): - """ - Form for auto-de-allocation of stock from a build - """ - - confirm = forms.BooleanField(required=False, label=_('Confirm'), help_text=_('Confirm unallocation of stock')) - - output_id = forms.IntegerField( - required=False, - widget=forms.HiddenInput() - ) - - part_id = forms.IntegerField( - required=False, - widget=forms.HiddenInput(), - ) - - class Meta: - model = Build - fields = [ - 'confirm', - 'output_id', - 'part_id', - ] - - class CompleteBuildForm(HelperForm): """ Form for marking a build as complete diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 8f74ba1c06..50f78f8a3d 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -587,9 +587,13 @@ class Build(MPTTModel): self.save() @transaction.atomic - def unallocateOutput(self, output, part=None): + def unallocateStock(self, bom_item=None, output=None): """ - Unallocate all stock which are allocated against the provided "output" (StockItem) + Unallocate stock from this Build + + arguments: + - bom_item: Specify a particular BomItem to unallocate stock against + - output: Specify a particular StockItem (output) to unallocate stock against """ allocations = BuildItem.objects.filter( @@ -597,34 +601,8 @@ class Build(MPTTModel): install_into=output ) - if part: - allocations = allocations.filter(stock_item__part=part) - - allocations.delete() - - @transaction.atomic - def unallocateUntracked(self, part=None): - """ - Unallocate all "untracked" stock - """ - - allocations = BuildItem.objects.filter( - build=self, - install_into=None - ) - - if part: - allocations = allocations.filter(stock_item__part=part) - - allocations.delete() - - @transaction.atomic - def unallocateAll(self): - """ - Deletes all stock allocations for this build. - """ - - allocations = BuildItem.objects.filter(build=self) + if bom_item: + allocations = allocations.filter(bom_item=bom_item) allocations.delete() diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 53e71dbd27..547f565905 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -120,6 +120,61 @@ class BuildSerializer(InvenTreeModelSerializer): ] +class BuildUnallocationSerializer(serializers.Serializer): + """ + DRF serializer for unallocating stock from a BuildOrder + + Allocated stock can be unallocated with a number of filters: + + - output: Filter against a particular build output (blank = untracked stock) + - bom_item: Filter against a particular BOM line item + + """ + + bom_item = serializers.PrimaryKeyRelatedField( + queryset=BomItem.objects.all(), + many=False, + allow_null=True, + required=False, + label=_('BOM Item'), + ) + + output = serializers.PrimaryKeyRelatedField( + queryset=StockItem.objects.filter( + is_building=True, + ), + many=False, + allow_null=True, + required=False, + label=_("Build output"), + ) + + def validate_output(self, stock_item): + + # Stock item must point to the same build order! + build = self.context['build'] + + if stock_item and stock_item.build != build: + raise ValidationError(_("Build output must point to the same build")) + + return stock_item + + def save(self): + """ + 'Save' the serializer data. + This performs the actual unallocation against the build order + """ + + build = self.context['build'] + + data = self.validated_data + + build.unallocateStock( + bom_item=data['bom_item'], + output=data['output'] + ) + + class BuildAllocationItemSerializer(serializers.Serializer): """ A serializer for allocating a single stock item against a build order diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 0b109d1890..cfba2046e3 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -462,12 +462,9 @@ $("#btn-auto-allocate").on('click', function() { }); $('#btn-unallocate').on('click', function() { - launchModalForm( - "{% url 'build-unallocate' build.id %}", - { - success: reloadTable, - } - ); + unallocateStock({{ build.id }}, { + table: '#allocation-table-untracked', + }); }); $('#allocate-selected-items').click(function() { diff --git a/InvenTree/build/templates/build/unallocate.html b/InvenTree/build/templates/build/unallocate.html deleted file mode 100644 index a650e95718..0000000000 --- a/InvenTree/build/templates/build/unallocate.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "modal_form.html" %} -{% load i18n %} -{% load inventree_extras %} -{% block pre_form_content %} - -{{ block.super }} - - -
- {% trans "Are you sure you wish to unallocate all stock for this build?" %} -
- {% trans "All incomplete stock allocations will be removed from the build" %} -
- -{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 93c6bfd511..7b2568b1c7 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -323,22 +323,3 @@ class TestBuildViews(TestCase): b = Build.objects.get(pk=1) self.assertEqual(b.status, 30) # Build status is now CANCELLED - - def test_build_unallocate(self): - """ Test the build unallocation view (ajax form) """ - - url = reverse('build-unallocate', args=(1,)) - - # Test without confirmation - response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - data = json.loads(response.content) - self.assertFalse(data['form_valid']) - - # Test with confirmation - response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - data = json.loads(response.content) - self.assertTrue(data['form_valid']) diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 050c32209b..d80b16056c 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -12,7 +12,6 @@ build_detail_urls = [ 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'^complete-output/', views.BuildOutputComplete.as_view(), name='build-output-complete'), - url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'), url(r'^complete/', views.BuildComplete.as_view(), name='build-complete'), url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 702b3b3596..8c63c1296c 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -10,14 +10,13 @@ from django.core.exceptions import ValidationError from django.views.generic import DetailView, ListView from django.forms import HiddenInput -from part.models import Part from .models import Build from . import forms from stock.models import StockLocation, StockItem from InvenTree.views import AjaxUpdateView, AjaxDeleteView from InvenTree.views import InvenTreeRoleMixin -from InvenTree.helpers import str2bool, extract_serial_numbers, isNull +from InvenTree.helpers import str2bool, extract_serial_numbers from InvenTree.status_codes import BuildStatus, StockStatus @@ -246,88 +245,6 @@ class BuildOutputDelete(AjaxUpdateView): } -class BuildUnallocate(AjaxUpdateView): - """ View to un-allocate all parts from a build. - - Provides a simple confirmation dialog with a BooleanField checkbox. - """ - - model = Build - form_class = forms.UnallocateBuildForm - ajax_form_title = _("Unallocate Stock") - ajax_template_name = "build/unallocate.html" - - def get_initial(self): - - initials = super().get_initial() - - # Pointing to a particular build output? - output = self.get_param('output') - - if output: - initials['output_id'] = output - - # Pointing to a particular part? - part = self.get_param('part') - - if part: - initials['part_id'] = part - - return initials - - def post(self, request, *args, **kwargs): - - build = self.get_object() - form = self.get_form() - - confirm = request.POST.get('confirm', False) - - output_id = request.POST.get('output_id', None) - - if output_id: - - # If a "null" output is provided, we are trying to unallocate "untracked" stock - if isNull(output_id): - output = None - else: - try: - output = StockItem.objects.get(pk=output_id) - except (ValueError, StockItem.DoesNotExist): - output = None - - part_id = request.POST.get('part_id', None) - - try: - part = Part.objects.get(pk=part_id) - except (ValueError, Part.DoesNotExist): - part = None - - valid = False - - if confirm is False: - form.add_error('confirm', _('Confirm unallocation of build stock')) - form.add_error(None, _('Check the confirmation box')) - else: - - valid = True - - # Unallocate the entire build - if not output_id: - build.unallocateAll() - # Unallocate a single output - elif output: - build.unallocateOutput(output, part=part) - # Unallocate "untracked" parts - else: - build.unallocateUntracked(part=part) - - data = { - 'form_valid': valid, - } - - return self.renderJsonResponse(request, form, data) - - class BuildComplete(AjaxUpdateView): """ View to mark the build as complete. diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 4dad5dc454..d3499deedf 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -208,15 +208,10 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) { var pk = $(this).attr('pk'); - launchModalForm( - `/build/${buildId}/unallocate/`, - { - success: reloadTable, - data: { - output: pk, - } - } - ); + unallocateStock(buildId, { + output: pk, + table: table, + }); }); $(panel).find(`#button-output-delete-${outputId}`).click(function() { @@ -236,6 +231,49 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) { } +/* + * Unallocate stock against a particular build order + * + * Options: + * - output: pk value for a stock item "build output" + * - bom_item: pk value for a particular BOMItem (build item) + */ +function unallocateStock(build_id, options={}) { + + var url = `/api/build/${build_id}/unallocate/`; + + var html = ` +
+ {% trans "Are you sure you wish to unallocate stock items from this build?" %} + + `; + + constructForm(url, { + method: 'POST', + confirm: true, + preFormContent: html, + fields: { + output: { + hidden: true, + value: options.output, + }, + bom_item: { + hidden: true, + value: options.bom_item, + }, + }, + title: '{% trans "Unallocate Stock Items" %}', + onSuccess: function(response, opts) { + if (options.table) { + // Reload the parent table + $(options.table).bootstrapTable('refresh'); + } + } + }); + +} + + function loadBuildOrderAllocationTable(table, options={}) { /** * Load a table showing all the BuildOrder allocations for a given part @@ -469,17 +507,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Callback for 'unallocate' button $(table).find('.button-unallocate').click(function() { - var pk = $(this).attr('pk'); - launchModalForm(`/build/${buildId}/unallocate/`, - { - success: reloadTable, - data: { - output: outputId, - part: pk, - } - } - ); + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = $(table).bootstrapTable('getData')[idx]; + + unallocateStock(buildId, { + bom_item: row.pk, + output: outputId == 'untracked' ? null : outputId, + table: table, + }); }); }