diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 1d9423371f..03ee877cb2 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -10,11 +10,15 @@ import common.models INVENTREE_SW_VERSION = "0.6.0 dev" -INVENTREE_API_VERSION = 13 +INVENTREE_API_VERSION = 14 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v14 -> 2021-20-05 + - Stock adjustment actions API is improved, using native DRF serializer support + - However adjustment actions now only support 'pk' as a lookup field + v13 -> 2021-10-05 - Adds API endpoint to allocate stock items against a BuildOrder - Updates StockItem API with improved filtering against BomItem data diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 847baf8ab5..a81f918013 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -372,7 +372,7 @@ { success: function(items) { adjustStock(action, items, { - onSuccess: function() { + success: function() { location.reload(); } }); diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 27c2426d53..ad487c7a5a 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -5,7 +5,6 @@ JSON API for the Stock app # -*- coding: utf-8 -*- from __future__ import unicode_literals -from decimal import Decimal, InvalidOperation from datetime import datetime, timedelta from django.utils.translation import ugettext_lazy as _ @@ -17,7 +16,6 @@ from django.db.models import Q from rest_framework import status from rest_framework.serializers import ValidationError -from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import generics, filters, permissions @@ -41,11 +39,7 @@ from order.serializers import POSerializer import common.settings import common.models -from .serializers import StockItemSerializer -from .serializers import LocationSerializer, LocationBriefSerializer -from .serializers import StockTrackingSerializer -from .serializers import StockItemAttachmentSerializer -from .serializers import StockItemTestResultSerializer +import stock.serializers as StockSerializers from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool, isNull @@ -83,12 +77,12 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): """ queryset = StockItem.objects.all() - serializer_class = StockItemSerializer + serializer_class = StockSerializers.StockItemSerializer def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) - queryset = StockItemSerializer.annotate_queryset(queryset) + queryset = StockSerializers.StockItemSerializer.annotate_queryset(queryset) return queryset @@ -124,7 +118,7 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): instance.mark_for_deletion() -class StockAdjust(APIView): +class StockAdjustView(generics.CreateAPIView): """ A generic class for handling stocktake actions. @@ -138,184 +132,57 @@ class StockAdjust(APIView): queryset = StockItem.objects.none() - allow_missing_quantity = False + def get_serializer_context(self): + + context = super().get_serializer_context() - def get_items(self, request): - """ - Return a list of items posted to the endpoint. - Will raise validation errors if the items are not - correctly formatted. - """ + context['request'] = self.request - _items = [] - - if 'item' in request.data: - _items = [request.data['item']] - elif 'items' in request.data: - _items = request.data['items'] - else: - _items = [] - - if len(_items) == 0: - raise ValidationError(_('Request must contain list of stock items')) - - # List of validated items - self.items = [] - - for entry in _items: - - if not type(entry) == dict: - raise ValidationError(_('Improperly formatted data')) - - # Look for a 'pk' value (use 'id' as a backup) - pk = entry.get('pk', entry.get('id', None)) - - try: - pk = int(pk) - except (ValueError, TypeError): - raise ValidationError(_('Each entry must contain a valid integer primary-key')) - - try: - item = StockItem.objects.get(pk=pk) - except (StockItem.DoesNotExist): - raise ValidationError({ - pk: [_('Primary key does not match valid stock item')] - }) - - if self.allow_missing_quantity and 'quantity' not in entry: - entry['quantity'] = item.quantity - - try: - quantity = Decimal(str(entry.get('quantity', None))) - except (ValueError, TypeError, InvalidOperation): - raise ValidationError({ - pk: [_('Invalid quantity value')] - }) - - if quantity < 0: - raise ValidationError({ - pk: [_('Quantity must not be less than zero')] - }) - - self.items.append({ - 'item': item, - 'quantity': quantity - }) - - # Extract 'notes' field - self.notes = str(request.data.get('notes', '')) + return context -class StockCount(StockAdjust): +class StockCount(StockAdjustView): """ Endpoint for counting stock (performing a stocktake). """ - def post(self, request, *args, **kwargs): - - self.get_items(request) - - n = 0 - - for item in self.items: - - if item['item'].stocktake(item['quantity'], request.user, notes=self.notes): - n += 1 - - return Response({'success': _('Updated stock for {n} items').format(n=n)}) + serializer_class = StockSerializers.StockCountSerializer -class StockAdd(StockAdjust): +class StockAdd(StockAdjustView): """ Endpoint for adding a quantity of stock to an existing StockItem """ - def post(self, request, *args, **kwargs): - - self.get_items(request) - - n = 0 - - for item in self.items: - if item['item'].add_stock(item['quantity'], request.user, notes=self.notes): - n += 1 - - return Response({"success": "Added stock for {n} items".format(n=n)}) + serializer_class = StockSerializers.StockAddSerializer -class StockRemove(StockAdjust): +class StockRemove(StockAdjustView): """ Endpoint for removing a quantity of stock from an existing StockItem. """ - def post(self, request, *args, **kwargs): - - self.get_items(request) - - n = 0 - - for item in self.items: - - if item['quantity'] > item['item'].quantity: - raise ValidationError({ - item['item'].pk: [_('Specified quantity exceeds stock quantity')] - }) - - if item['item'].take_stock(item['quantity'], request.user, notes=self.notes): - n += 1 - - return Response({"success": "Removed stock for {n} items".format(n=n)}) + serializer_class = StockSerializers.StockRemoveSerializer -class StockTransfer(StockAdjust): +class StockTransfer(StockAdjustView): """ API endpoint for performing stock movements """ - allow_missing_quantity = True - - def post(self, request, *args, **kwargs): - - data = request.data - - try: - location = StockLocation.objects.get(pk=data.get('location', None)) - except (ValueError, StockLocation.DoesNotExist): - raise ValidationError({'location': [_('Valid location must be specified')]}) - - n = 0 - - self.get_items(request) - - for item in self.items: - - if item['quantity'] > item['item'].quantity: - raise ValidationError({ - item['item'].pk: [_('Specified quantity exceeds stock quantity')] - }) - - # If quantity is not specified, move the entire stock - if item['quantity'] in [0, None]: - item['quantity'] = item['item'].quantity - - if item['item'].move(location, self.notes, request.user, quantity=item['quantity']): - n += 1 - - return Response({'success': _('Moved {n} parts to {loc}').format( - n=n, - loc=str(location), - )}) + serializer_class = StockSerializers.StockTransferSerializer class StockLocationList(generics.ListCreateAPIView): - """ API endpoint for list view of StockLocation objects: + """ + API endpoint for list view of StockLocation objects: - GET: Return list of StockLocation objects - POST: Create a new StockLocation """ queryset = StockLocation.objects.all() - serializer_class = LocationSerializer + serializer_class = StockSerializers.LocationSerializer def filter_queryset(self, queryset): """ @@ -517,7 +384,7 @@ class StockList(generics.ListCreateAPIView): - POST: Create a new StockItem """ - serializer_class = StockItemSerializer + serializer_class = StockSerializers.StockItemSerializer queryset = StockItem.objects.all() filterset_class = StockFilter @@ -639,7 +506,7 @@ class StockList(generics.ListCreateAPIView): # Serialize each StockLocation object for location in locations: - location_map[location.pk] = LocationBriefSerializer(location).data + location_map[location.pk] = StockSerializers.LocationBriefSerializer(location).data # Now update each StockItem with the related StockLocation data for stock_item in data: @@ -665,7 +532,7 @@ class StockList(generics.ListCreateAPIView): queryset = super().get_queryset(*args, **kwargs) - queryset = StockItemSerializer.annotate_queryset(queryset) + queryset = StockSerializers.StockItemSerializer.annotate_queryset(queryset) # Do not expose StockItem objects which are scheduled for deletion queryset = queryset.filter(scheduled_for_deletion=False) @@ -954,7 +821,7 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ queryset = StockItemAttachment.objects.all() - serializer_class = StockItemAttachmentSerializer + serializer_class = StockSerializers.StockItemAttachmentSerializer filter_backends = [ DjangoFilterBackend, @@ -973,7 +840,7 @@ class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMix """ queryset = StockItemAttachment.objects.all() - serializer_class = StockItemAttachmentSerializer + serializer_class = StockSerializers.StockItemAttachmentSerializer class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView): @@ -982,7 +849,7 @@ class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView): """ queryset = StockItemTestResult.objects.all() - serializer_class = StockItemTestResultSerializer + serializer_class = StockSerializers.StockItemTestResultSerializer class StockItemTestResultList(generics.ListCreateAPIView): @@ -991,7 +858,7 @@ class StockItemTestResultList(generics.ListCreateAPIView): """ queryset = StockItemTestResult.objects.all() - serializer_class = StockItemTestResultSerializer + serializer_class = StockSerializers.StockItemTestResultSerializer filter_backends = [ DjangoFilterBackend, @@ -1039,7 +906,7 @@ class StockTrackingDetail(generics.RetrieveAPIView): """ queryset = StockItemTracking.objects.all() - serializer_class = StockTrackingSerializer + serializer_class = StockSerializers.StockTrackingSerializer class StockTrackingList(generics.ListAPIView): @@ -1052,7 +919,7 @@ class StockTrackingList(generics.ListAPIView): """ queryset = StockItemTracking.objects.all() - serializer_class = StockTrackingSerializer + serializer_class = StockSerializers.StockTrackingSerializer def get_serializer(self, *args, **kwargs): try: @@ -1088,7 +955,7 @@ class StockTrackingList(generics.ListAPIView): if 'location' in deltas: try: location = StockLocation.objects.get(pk=deltas['location']) - serializer = LocationSerializer(location) + serializer = StockSerializers.LocationSerializer(location) deltas['location_detail'] = serializer.data except: pass @@ -1097,7 +964,7 @@ class StockTrackingList(generics.ListAPIView): if 'stockitem' in deltas: try: stockitem = StockItem.objects.get(pk=deltas['stockitem']) - serializer = StockItemSerializer(stockitem) + serializer = StockSerializers.StockItemSerializer(stockitem) deltas['stockitem_detail'] = serializer.data except: pass @@ -1179,7 +1046,7 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView): """ queryset = StockLocation.objects.all() - serializer_class = LocationSerializer + serializer_class = StockSerializers.LocationSerializer stock_api_urls = [ diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 70dd55a4eb..c44dffe94f 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -2,27 +2,29 @@ JSON serializers for Stock app """ -from rest_framework import serializers +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from decimal import Decimal +from datetime import datetime, timedelta +from django.db import transaction from django.utils.translation import ugettext_lazy as _ +from django.db.models.functions import Coalesce +from django.db.models import Case, When, Value +from django.db.models import BooleanField +from django.db.models import Q + +from rest_framework import serializers +from rest_framework.serializers import ValidationError + +from sql_util.utils import SubquerySum, SubqueryCount from .models import StockItem, StockLocation from .models import StockItemTracking from .models import StockItemAttachment from .models import StockItemTestResult -from django.db.models.functions import Coalesce - -from django.db.models import Case, When, Value -from django.db.models import BooleanField -from django.db.models import Q - -from sql_util.utils import SubquerySum, SubqueryCount - -from decimal import Decimal - -from datetime import datetime, timedelta - import common.models from common.settings import currency_code_default, currency_code_mappings @@ -396,3 +398,196 @@ class StockTrackingSerializer(InvenTreeModelSerializer): 'label', 'tracking_type', ] + + +class StockAdjustmentItemSerializer(serializers.Serializer): + """ + Serializer for a single StockItem within a stock adjument request. + + Fields: + - item: StockItem object + - quantity: Numerical quantity + """ + + class Meta: + fields = [ + 'item', + 'quantity' + ] + + pk = serializers.PrimaryKeyRelatedField( + queryset=StockItem.objects.all(), + many=False, + allow_null=False, + required=True, + label='stock_item', + help_text=_('StockItem primary key value') + ) + + quantity = serializers.DecimalField( + max_digits=15, + decimal_places=5, + min_value=0, + required=True + ) + + +class StockAdjustmentSerializer(serializers.Serializer): + """ + Base class for managing stock adjustment actions via the API + """ + + class Meta: + fields = [ + 'items', + 'notes', + ] + + items = StockAdjustmentItemSerializer(many=True) + + notes = serializers.CharField( + required=False, + allow_blank=True, + label=_("Notes"), + help_text=_("Stock transaction notes"), + ) + + def validate(self, data): + + super().validate(data) + + items = data.get('items', []) + + if len(items) == 0: + raise ValidationError(_("A list of stock items must be provided")) + + return data + + +class StockCountSerializer(StockAdjustmentSerializer): + """ + Serializer for counting stock items + """ + + def save(self): + + request = self.context['request'] + + data = self.validated_data + items = data['items'] + notes = data.get('notes', '') + + with transaction.atomic(): + for item in items: + + stock_item = item['pk'] + quantity = item['quantity'] + + stock_item.stocktake( + quantity, + request.user, + notes=notes + ) + + +class StockAddSerializer(StockAdjustmentSerializer): + """ + Serializer for adding stock to stock item(s) + """ + + def save(self): + + request = self.context['request'] + + data = self.validated_data + notes = data.get('notes', '') + + with transaction.atomic(): + for item in data['items']: + + stock_item = item['pk'] + quantity = item['quantity'] + + stock_item.add_stock( + quantity, + request.user, + notes=notes + ) + + +class StockRemoveSerializer(StockAdjustmentSerializer): + """ + Serializer for removing stock from stock item(s) + """ + + def save(self): + + request = self.context['request'] + + data = self.validated_data + notes = data.get('notes', '') + + with transaction.atomic(): + for item in data['items']: + + stock_item = item['pk'] + quantity = item['quantity'] + + stock_item.take_stock( + quantity, + request.user, + notes=notes + ) + + +class StockTransferSerializer(StockAdjustmentSerializer): + """ + Serializer for transferring (moving) stock item(s) + """ + + location = serializers.PrimaryKeyRelatedField( + queryset=StockLocation.objects.all(), + many=False, + required=True, + allow_null=False, + label=_('Location'), + help_text=_('Destination stock location'), + ) + + class Meta: + fields = [ + 'items', + 'notes', + 'location', + ] + + def validate(self, data): + + super().validate(data) + + # TODO: Any specific validation of location field? + + return data + + def save(self): + + request = self.context['request'] + + data = self.validated_data + + items = data['items'] + notes = data.get('notes', '') + location = data['location'] + + with transaction.atomic(): + for item in items: + + stock_item = item['pk'] + quantity = item['quantity'] + + stock_item.move( + location, + notes, + request.user, + quantity=quantity + ) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 759732fe6e..3addeacde2 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -561,7 +561,7 @@ function itemAdjust(action) { { success: function(item) { adjustStock(action, [item], { - onSuccess: function() { + success: function() { location.reload(); } }); diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 9a5aeb6a7e..3afaf45635 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -287,7 +287,7 @@ { success: function(items) { adjustStock(action, items, { - onSuccess: function() { + success: function() { location.reload(); } }); diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index 21c355fae2..d07c35aaf7 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -513,31 +513,34 @@ class StocktakeTest(StockAPITestCase): # POST with a valid action response = self.post(url, data) - self.assertContains(response, "must contain list", status_code=status.HTTP_400_BAD_REQUEST) + + self.assertIn("This field is required", str(response.data["items"])) data['items'] = [{ 'no': 'aa' }] # POST without a PK - response = self.post(url, data) - self.assertContains(response, 'must contain a valid integer primary-key', status_code=status.HTTP_400_BAD_REQUEST) + response = self.post(url, data, expected_code=400) + + self.assertIn('This field is required', str(response.data)) # POST with an invalid PK data['items'] = [{ 'pk': 10 }] - response = self.post(url, data) - self.assertContains(response, 'does not match valid stock item', status_code=status.HTTP_400_BAD_REQUEST) + response = self.post(url, data, expected_code=400) + + self.assertContains(response, 'object does not exist', status_code=status.HTTP_400_BAD_REQUEST) # POST with missing quantity value data['items'] = [{ 'pk': 1234 }] - response = self.post(url, data) - self.assertContains(response, 'Invalid quantity value', status_code=status.HTTP_400_BAD_REQUEST) + response = self.post(url, data, expected_code=400) + self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST) # POST with an invalid quantity value data['items'] = [{ @@ -546,7 +549,7 @@ class StocktakeTest(StockAPITestCase): }] response = self.post(url, data) - self.assertContains(response, 'Invalid quantity value', status_code=status.HTTP_400_BAD_REQUEST) + self.assertContains(response, 'A valid number is required', status_code=status.HTTP_400_BAD_REQUEST) data['items'] = [{ 'pk': 1234, @@ -554,18 +557,7 @@ class StocktakeTest(StockAPITestCase): }] response = self.post(url, data) - self.assertContains(response, 'must not be less than zero', status_code=status.HTTP_400_BAD_REQUEST) - - # Test with a single item - data = { - 'item': { - 'pk': 1234, - 'quantity': '10', - } - } - - response = self.post(url, data) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, 'Ensure this value is greater than or equal to 0', status_code=status.HTTP_400_BAD_REQUEST) def test_transfer(self): """ @@ -573,24 +565,27 @@ class StocktakeTest(StockAPITestCase): """ data = { - 'item': { - 'pk': 1234, - 'quantity': 10, - }, + 'items': [ + { + 'pk': 1234, + 'quantity': 10, + } + ], 'location': 1, 'notes': "Moving to a new location" } url = reverse('api-stock-transfer') - response = self.post(url, data) - self.assertContains(response, "Moved 1 parts to", status_code=status.HTTP_200_OK) + # This should succeed + response = self.post(url, data, expected_code=201) # Now try one which will fail due to a bad location data['location'] = 'not a location' - response = self.post(url, data) - self.assertContains(response, 'Valid location must be specified', status_code=status.HTTP_400_BAD_REQUEST) + response = self.post(url, data, expected_code=400) + + self.assertContains(response, 'Incorrect type. Expected pk value', status_code=status.HTTP_400_BAD_REQUEST) class StockItemDeletionTest(StockAPITestCase): diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 2f602a93e1..eb5fabcc25 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from django.core.exceptions import ValidationError from django.views.generic.edit import FormMixin -from django.views.generic import DetailView, ListView, UpdateView +from django.views.generic import DetailView, ListView from django.forms.models import model_to_dict from django.forms import HiddenInput from django.urls import reverse @@ -145,29 +145,6 @@ class StockItemDetail(InvenTreeRoleMixin, DetailView): return super().get(request, *args, **kwargs) -class StockItemNotes(InvenTreeRoleMixin, UpdateView): - """ View for editing the 'notes' field of a StockItem object """ - - context_object_name = 'item' - template_name = 'stock/item_notes.html' - model = StockItem - - role_required = 'stock.view' - - fields = ['notes'] - - def get_success_url(self): - return reverse('stock-item-notes', kwargs={'pk': self.get_object().id}) - - def get_context_data(self, **kwargs): - - ctx = super().get_context_data(**kwargs) - - ctx['editing'] = str2bool(self.request.GET.get('edit', '')) - - return ctx - - class StockLocationEdit(AjaxUpdateView): """ View for editing details of a StockLocation. diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 17c2598d1b..b88f5f1862 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -4,15 +4,12 @@ /* globals attachSelect, - attachToggle, - blankImage, enableField, clearField, clearFieldOptions, closeModal, + constructField, constructFormBody, - constructNumberInput, - createNewModal, getFormFieldValue, global_settings, handleFormErrors, @@ -247,7 +244,7 @@ function adjustStock(action, items, options={}) { break; } - var image = item.part_detail.thumbnail || item.part_detail.image || blankImage(); + var thumb = thumbnailImage(item.part_detail.thumbnail || item.part_detail.image); var status = stockStatusDisplay(item.status, { classes: 'float-right' @@ -268,14 +265,18 @@ function adjustStock(action, items, options={}) { var actionInput = ''; if (actionTitle != null) { - actionInput = constructNumberInput( - item.pk, + actionInput = constructField( + `items_quantity_${pk}`, { - value: value, + type: 'decimal', min_value: minValue, max_value: maxValue, - read_only: readonly, + value: value, title: readonly ? '{% trans "Quantity cannot be adjusted for serialized stock" %}' : '{% trans "Specify stock quantity" %}', + required: true, + }, + { + hideLabels: true, } ); } @@ -293,7 +294,7 @@ function adjustStock(action, items, options={}) { html += ` - ${item.part_detail.full_name} + ${thumb} ${item.part_detail.full_name} ${quantity}${status} ${location} @@ -319,50 +320,89 @@ function adjustStock(action, items, options={}) { html += ``; - var modal = createNewModal({ - title: formTitle, - }); + var extraFields = {}; - // Extra fields - var extraFields = { - location: { - label: '{% trans "Location" %}', - help_text: '{% trans "Select destination stock location" %}', - type: 'related field', - required: true, - api_url: `/api/stock/location/`, - model: 'stocklocation', - name: 'location', - }, - notes: { - label: '{% trans "Notes" %}', - help_text: '{% trans "Stock transaction notes" %}', - type: 'string', - name: 'notes', - } - }; - - if (!specifyLocation) { - delete extraFields.location; + if (specifyLocation) { + extraFields.location = {}; } - constructFormBody({}, { - preFormContent: html, + if (action != 'delete') { + extraFields.notes = {}; + } + + constructForm(url, { + method: 'POST', fields: extraFields, + preFormContent: html, confirm: true, confirmMessage: '{% trans "Confirm stock adjustment" %}', - modal: modal, - onSubmit: function(fields) { + title: formTitle, + afterRender: function(fields, opts) { + // Add button callbacks to remove rows + $(opts.modal).find('.button-stock-item-remove').click(function() { + var pk = $(this).attr('pk'); - // "Delete" action gets handled differently + $(opts.modal).find(`#stock_item_${pk}`).remove(); + }); + + // Initialize "location" field + if (specifyLocation) { + initializeRelatedField( + { + name: 'location', + type: 'related field', + model: 'stocklocation', + required: true, + }, + null, + opts + ); + } + }, + onSubmit: function(fields, opts) { + + // Extract data elements from the form + var data = { + items: [], + }; + + if (action != 'delete') { + data.notes = getFormFieldValue('notes', {}, opts); + } + + if (specifyLocation) { + data.location = getFormFieldValue('location', {}, opts); + } + + var item_pk_values = []; + + items.forEach(function(item) { + var pk = item.pk; + + // Does the row exist in the form? + var row = $(opts.modal).find(`#stock_item_${pk}`); + + if (row) { + + item_pk_values.push(pk); + + var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts); + + data.items.push({ + pk: pk, + quantity: quantity, + }); + } + }); + + // Delete action is handled differently if (action == 'delete') { - var requests = []; - items.forEach(function(item) { + item_pk_values.forEach(function(pk) { requests.push( inventreeDelete( - `/api/stock/${item.pk}/`, + `/api/stock/${pk}/`, ) ); }); @@ -370,72 +410,40 @@ function adjustStock(action, items, options={}) { // Wait for *all* the requests to complete $.when.apply($, requests).done(function() { // Destroy the modal window - $(modal).modal('hide'); + $(opts.modal).modal('hide'); - if (options.onSuccess) { - options.onSuccess(); + if (options.success) { + options.success(); } }); return; } - // Data to transmit - var data = { - items: [], + opts.nested = { + 'items': item_pk_values, }; - // Add values for each selected stock item - items.forEach(function(item) { - - var q = getFormFieldValue(item.pk, {}, {modal: modal}); - - if (q != null) { - data.items.push({pk: item.pk, quantity: q}); - } - }); - - // Add in extra field data - for (var field_name in extraFields) { - data[field_name] = getFormFieldValue( - field_name, - fields[field_name], - { - modal: modal, - } - ); - } - inventreePut( url, data, { method: 'POST', - success: function() { + success: function(response) { + // Hide the modal + $(opts.modal).modal('hide'); - // Destroy the modal window - $(modal).modal('hide'); - - if (options.onSuccess) { - options.onSuccess(); + if (options.success) { + options.success(response); } }, error: function(xhr) { switch (xhr.status) { case 400: - - // Handle errors for standard fields - handleFormErrors( - xhr.responseJSON, - extraFields, - { - modal: modal, - } - ); - + handleFormErrors(xhr.responseJSON, fields, opts); break; default: - $(modal).modal('hide'); + $(opts.modal).modal('hide'); showApiError(xhr); break; } @@ -444,18 +452,6 @@ function adjustStock(action, items, options={}) { ); } }); - - // Attach callbacks for the action buttons - $(modal).find('.button-stock-item-remove').click(function() { - var pk = $(this).attr('pk'); - - $(modal).find(`#stock_item_${pk}`).remove(); - }); - - attachToggle(modal); - - $(modal + ' .select2-container').addClass('select-full-width'); - $(modal + ' .select2-container').css('width', '100%'); } @@ -1258,7 +1254,7 @@ function loadStockTable(table, options) { var items = $(table).bootstrapTable('getSelections'); adjustStock(action, items, { - onSuccess: function() { + success: function() { $(table).bootstrapTable('refresh'); } });