Merge pull request #2103 from SchrodingersGat/stock-adjustment-api

Stock adjustment api
This commit is contained in:
Oliver 2021-10-06 09:52:25 +11:00 committed by GitHub
commit c2ad9c4765
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 367 additions and 333 deletions

View File

@ -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

View File

@ -372,7 +372,7 @@
{
success: function(items) {
adjustStock(action, items, {
onSuccess: function() {
success: function() {
location.reload();
}
});

View File

@ -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 = [

View File

@ -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
)

View File

@ -561,7 +561,7 @@ function itemAdjust(action) {
{
success: function(item) {
adjustStock(action, [item], {
onSuccess: function() {
success: function() {
location.reload();
}
});

View File

@ -287,7 +287,7 @@
{
success: function(items) {
adjustStock(action, items, {
onSuccess: function() {
success: function() {
location.reload();
}
});

View File

@ -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):

View File

@ -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.

View File

@ -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 += `
<tr id='stock_item_${pk}' class='stock-item-row'>
<td id='part_${pk}'><img src='${image}' class='hover-img-thumb'> ${item.part_detail.full_name}</td>
<td id='part_${pk}'>${thumb} ${item.part_detail.full_name}</td>
<td id='stock_${pk}'>${quantity}${status}</td>
<td id='location_${pk}'>${location}</td>
<td id='action_${pk}'>
@ -319,50 +320,89 @@ function adjustStock(action, items, options={}) {
html += `</tbody></table>`;
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');
}
});