mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2103 from SchrodingersGat/stock-adjustment-api
Stock adjustment api
This commit is contained in:
commit
c2ad9c4765
@ -10,11 +10,15 @@ import common.models
|
|||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
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
|
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
|
v13 -> 2021-10-05
|
||||||
- Adds API endpoint to allocate stock items against a BuildOrder
|
- Adds API endpoint to allocate stock items against a BuildOrder
|
||||||
- Updates StockItem API with improved filtering against BomItem data
|
- Updates StockItem API with improved filtering against BomItem data
|
||||||
|
@ -372,7 +372,7 @@
|
|||||||
{
|
{
|
||||||
success: function(items) {
|
success: function(items) {
|
||||||
adjustStock(action, items, {
|
adjustStock(action, items, {
|
||||||
onSuccess: function() {
|
success: function() {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,6 @@ JSON API for the Stock app
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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 import status
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import generics, filters, permissions
|
from rest_framework import generics, filters, permissions
|
||||||
|
|
||||||
@ -41,11 +39,7 @@ from order.serializers import POSerializer
|
|||||||
import common.settings
|
import common.settings
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
from .serializers import StockItemSerializer
|
import stock.serializers as StockSerializers
|
||||||
from .serializers import LocationSerializer, LocationBriefSerializer
|
|
||||||
from .serializers import StockTrackingSerializer
|
|
||||||
from .serializers import StockItemAttachmentSerializer
|
|
||||||
from .serializers import StockItemTestResultSerializer
|
|
||||||
|
|
||||||
from InvenTree.views import TreeSerializer
|
from InvenTree.views import TreeSerializer
|
||||||
from InvenTree.helpers import str2bool, isNull
|
from InvenTree.helpers import str2bool, isNull
|
||||||
@ -83,12 +77,12 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockItem.objects.all()
|
queryset = StockItem.objects.all()
|
||||||
serializer_class = StockItemSerializer
|
serializer_class = StockSerializers.StockItemSerializer
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
queryset = StockItemSerializer.annotate_queryset(queryset)
|
queryset = StockSerializers.StockItemSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@ -124,7 +118,7 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
instance.mark_for_deletion()
|
instance.mark_for_deletion()
|
||||||
|
|
||||||
|
|
||||||
class StockAdjust(APIView):
|
class StockAdjustView(generics.CreateAPIView):
|
||||||
"""
|
"""
|
||||||
A generic class for handling stocktake actions.
|
A generic class for handling stocktake actions.
|
||||||
|
|
||||||
@ -138,184 +132,57 @@ class StockAdjust(APIView):
|
|||||||
|
|
||||||
queryset = StockItem.objects.none()
|
queryset = StockItem.objects.none()
|
||||||
|
|
||||||
allow_missing_quantity = False
|
def get_serializer_context(self):
|
||||||
|
|
||||||
|
context = super().get_serializer_context()
|
||||||
|
|
||||||
def get_items(self, request):
|
context['request'] = self.request
|
||||||
"""
|
|
||||||
Return a list of items posted to the endpoint.
|
|
||||||
Will raise validation errors if the items are not
|
|
||||||
correctly formatted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_items = []
|
return context
|
||||||
|
|
||||||
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', ''))
|
|
||||||
|
|
||||||
|
|
||||||
class StockCount(StockAdjust):
|
class StockCount(StockAdjustView):
|
||||||
"""
|
"""
|
||||||
Endpoint for counting stock (performing a stocktake).
|
Endpoint for counting stock (performing a stocktake).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
serializer_class = StockSerializers.StockCountSerializer
|
||||||
|
|
||||||
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)})
|
|
||||||
|
|
||||||
|
|
||||||
class StockAdd(StockAdjust):
|
class StockAdd(StockAdjustView):
|
||||||
"""
|
"""
|
||||||
Endpoint for adding a quantity of stock to an existing StockItem
|
Endpoint for adding a quantity of stock to an existing StockItem
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
serializer_class = StockSerializers.StockAddSerializer
|
||||||
|
|
||||||
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)})
|
|
||||||
|
|
||||||
|
|
||||||
class StockRemove(StockAdjust):
|
class StockRemove(StockAdjustView):
|
||||||
"""
|
"""
|
||||||
Endpoint for removing a quantity of stock from an existing StockItem.
|
Endpoint for removing a quantity of stock from an existing StockItem.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
serializer_class = StockSerializers.StockRemoveSerializer
|
||||||
|
|
||||||
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)})
|
|
||||||
|
|
||||||
|
|
||||||
class StockTransfer(StockAdjust):
|
class StockTransfer(StockAdjustView):
|
||||||
"""
|
"""
|
||||||
API endpoint for performing stock movements
|
API endpoint for performing stock movements
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allow_missing_quantity = True
|
serializer_class = StockSerializers.StockTransferSerializer
|
||||||
|
|
||||||
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),
|
|
||||||
)})
|
|
||||||
|
|
||||||
|
|
||||||
class StockLocationList(generics.ListCreateAPIView):
|
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
|
- GET: Return list of StockLocation objects
|
||||||
- POST: Create a new StockLocation
|
- POST: Create a new StockLocation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockLocation.objects.all()
|
queryset = StockLocation.objects.all()
|
||||||
serializer_class = LocationSerializer
|
serializer_class = StockSerializers.LocationSerializer
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
"""
|
"""
|
||||||
@ -517,7 +384,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
- POST: Create a new StockItem
|
- POST: Create a new StockItem
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = StockItemSerializer
|
serializer_class = StockSerializers.StockItemSerializer
|
||||||
queryset = StockItem.objects.all()
|
queryset = StockItem.objects.all()
|
||||||
filterset_class = StockFilter
|
filterset_class = StockFilter
|
||||||
|
|
||||||
@ -639,7 +506,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
# Serialize each StockLocation object
|
# Serialize each StockLocation object
|
||||||
for location in locations:
|
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
|
# Now update each StockItem with the related StockLocation data
|
||||||
for stock_item in data:
|
for stock_item in data:
|
||||||
@ -665,7 +532,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
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
|
# Do not expose StockItem objects which are scheduled for deletion
|
||||||
queryset = queryset.filter(scheduled_for_deletion=False)
|
queryset = queryset.filter(scheduled_for_deletion=False)
|
||||||
@ -954,7 +821,7 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockItemAttachment.objects.all()
|
queryset = StockItemAttachment.objects.all()
|
||||||
serializer_class = StockItemAttachmentSerializer
|
serializer_class = StockSerializers.StockItemAttachmentSerializer
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
DjangoFilterBackend,
|
DjangoFilterBackend,
|
||||||
@ -973,7 +840,7 @@ class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMix
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockItemAttachment.objects.all()
|
queryset = StockItemAttachment.objects.all()
|
||||||
serializer_class = StockItemAttachmentSerializer
|
serializer_class = StockSerializers.StockItemAttachmentSerializer
|
||||||
|
|
||||||
|
|
||||||
class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView):
|
class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
@ -982,7 +849,7 @@ class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockItemTestResult.objects.all()
|
queryset = StockItemTestResult.objects.all()
|
||||||
serializer_class = StockItemTestResultSerializer
|
serializer_class = StockSerializers.StockItemTestResultSerializer
|
||||||
|
|
||||||
|
|
||||||
class StockItemTestResultList(generics.ListCreateAPIView):
|
class StockItemTestResultList(generics.ListCreateAPIView):
|
||||||
@ -991,7 +858,7 @@ class StockItemTestResultList(generics.ListCreateAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockItemTestResult.objects.all()
|
queryset = StockItemTestResult.objects.all()
|
||||||
serializer_class = StockItemTestResultSerializer
|
serializer_class = StockSerializers.StockItemTestResultSerializer
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
DjangoFilterBackend,
|
DjangoFilterBackend,
|
||||||
@ -1039,7 +906,7 @@ class StockTrackingDetail(generics.RetrieveAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockItemTracking.objects.all()
|
queryset = StockItemTracking.objects.all()
|
||||||
serializer_class = StockTrackingSerializer
|
serializer_class = StockSerializers.StockTrackingSerializer
|
||||||
|
|
||||||
|
|
||||||
class StockTrackingList(generics.ListAPIView):
|
class StockTrackingList(generics.ListAPIView):
|
||||||
@ -1052,7 +919,7 @@ class StockTrackingList(generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockItemTracking.objects.all()
|
queryset = StockItemTracking.objects.all()
|
||||||
serializer_class = StockTrackingSerializer
|
serializer_class = StockSerializers.StockTrackingSerializer
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
@ -1088,7 +955,7 @@ class StockTrackingList(generics.ListAPIView):
|
|||||||
if 'location' in deltas:
|
if 'location' in deltas:
|
||||||
try:
|
try:
|
||||||
location = StockLocation.objects.get(pk=deltas['location'])
|
location = StockLocation.objects.get(pk=deltas['location'])
|
||||||
serializer = LocationSerializer(location)
|
serializer = StockSerializers.LocationSerializer(location)
|
||||||
deltas['location_detail'] = serializer.data
|
deltas['location_detail'] = serializer.data
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@ -1097,7 +964,7 @@ class StockTrackingList(generics.ListAPIView):
|
|||||||
if 'stockitem' in deltas:
|
if 'stockitem' in deltas:
|
||||||
try:
|
try:
|
||||||
stockitem = StockItem.objects.get(pk=deltas['stockitem'])
|
stockitem = StockItem.objects.get(pk=deltas['stockitem'])
|
||||||
serializer = StockItemSerializer(stockitem)
|
serializer = StockSerializers.StockItemSerializer(stockitem)
|
||||||
deltas['stockitem_detail'] = serializer.data
|
deltas['stockitem_detail'] = serializer.data
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@ -1179,7 +1046,7 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = StockLocation.objects.all()
|
queryset = StockLocation.objects.all()
|
||||||
serializer_class = LocationSerializer
|
serializer_class = StockSerializers.LocationSerializer
|
||||||
|
|
||||||
|
|
||||||
stock_api_urls = [
|
stock_api_urls = [
|
||||||
|
@ -2,27 +2,29 @@
|
|||||||
JSON serializers for Stock app
|
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.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 StockItem, StockLocation
|
||||||
from .models import StockItemTracking
|
from .models import StockItemTracking
|
||||||
from .models import StockItemAttachment
|
from .models import StockItemAttachment
|
||||||
from .models import StockItemTestResult
|
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
|
import common.models
|
||||||
from common.settings import currency_code_default, currency_code_mappings
|
from common.settings import currency_code_default, currency_code_mappings
|
||||||
|
|
||||||
@ -396,3 +398,196 @@ class StockTrackingSerializer(InvenTreeModelSerializer):
|
|||||||
'label',
|
'label',
|
||||||
'tracking_type',
|
'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
|
||||||
|
)
|
||||||
|
@ -561,7 +561,7 @@ function itemAdjust(action) {
|
|||||||
{
|
{
|
||||||
success: function(item) {
|
success: function(item) {
|
||||||
adjustStock(action, [item], {
|
adjustStock(action, [item], {
|
||||||
onSuccess: function() {
|
success: function() {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -287,7 +287,7 @@
|
|||||||
{
|
{
|
||||||
success: function(items) {
|
success: function(items) {
|
||||||
adjustStock(action, items, {
|
adjustStock(action, items, {
|
||||||
onSuccess: function() {
|
success: function() {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -513,31 +513,34 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
|
|
||||||
# POST with a valid action
|
# POST with a valid action
|
||||||
response = self.post(url, data)
|
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'] = [{
|
data['items'] = [{
|
||||||
'no': 'aa'
|
'no': 'aa'
|
||||||
}]
|
}]
|
||||||
|
|
||||||
# POST without a PK
|
# POST without a PK
|
||||||
response = self.post(url, data)
|
response = self.post(url, data, expected_code=400)
|
||||||
self.assertContains(response, 'must contain a valid integer primary-key', status_code=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
self.assertIn('This field is required', str(response.data))
|
||||||
|
|
||||||
# POST with an invalid PK
|
# POST with an invalid PK
|
||||||
data['items'] = [{
|
data['items'] = [{
|
||||||
'pk': 10
|
'pk': 10
|
||||||
}]
|
}]
|
||||||
|
|
||||||
response = self.post(url, data)
|
response = self.post(url, data, expected_code=400)
|
||||||
self.assertContains(response, 'does not match valid stock item', status_code=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
self.assertContains(response, 'object does not exist', status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# POST with missing quantity value
|
# POST with missing quantity value
|
||||||
data['items'] = [{
|
data['items'] = [{
|
||||||
'pk': 1234
|
'pk': 1234
|
||||||
}]
|
}]
|
||||||
|
|
||||||
response = self.post(url, data)
|
response = self.post(url, data, expected_code=400)
|
||||||
self.assertContains(response, 'Invalid quantity value', status_code=status.HTTP_400_BAD_REQUEST)
|
self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# POST with an invalid quantity value
|
# POST with an invalid quantity value
|
||||||
data['items'] = [{
|
data['items'] = [{
|
||||||
@ -546,7 +549,7 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
response = self.post(url, data)
|
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'] = [{
|
data['items'] = [{
|
||||||
'pk': 1234,
|
'pk': 1234,
|
||||||
@ -554,18 +557,7 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
response = self.post(url, data)
|
response = self.post(url, data)
|
||||||
self.assertContains(response, 'must not be less than zero', status_code=status.HTTP_400_BAD_REQUEST)
|
self.assertContains(response, 'Ensure this value is greater than or equal to 0', 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)
|
|
||||||
|
|
||||||
def test_transfer(self):
|
def test_transfer(self):
|
||||||
"""
|
"""
|
||||||
@ -573,24 +565,27 @@ class StocktakeTest(StockAPITestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'item': {
|
'items': [
|
||||||
'pk': 1234,
|
{
|
||||||
'quantity': 10,
|
'pk': 1234,
|
||||||
},
|
'quantity': 10,
|
||||||
|
}
|
||||||
|
],
|
||||||
'location': 1,
|
'location': 1,
|
||||||
'notes': "Moving to a new location"
|
'notes': "Moving to a new location"
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('api-stock-transfer')
|
url = reverse('api-stock-transfer')
|
||||||
|
|
||||||
response = self.post(url, data)
|
# This should succeed
|
||||||
self.assertContains(response, "Moved 1 parts to", status_code=status.HTTP_200_OK)
|
response = self.post(url, data, expected_code=201)
|
||||||
|
|
||||||
# Now try one which will fail due to a bad location
|
# Now try one which will fail due to a bad location
|
||||||
data['location'] = 'not a location'
|
data['location'] = 'not a location'
|
||||||
|
|
||||||
response = self.post(url, data)
|
response = self.post(url, data, expected_code=400)
|
||||||
self.assertContains(response, 'Valid location must be specified', status_code=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
self.assertContains(response, 'Incorrect type. Expected pk value', status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class StockItemDeletionTest(StockAPITestCase):
|
class StockItemDeletionTest(StockAPITestCase):
|
||||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.views.generic.edit import FormMixin
|
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.models import model_to_dict
|
||||||
from django.forms import HiddenInput
|
from django.forms import HiddenInput
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -145,29 +145,6 @@ class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
return super().get(request, *args, **kwargs)
|
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):
|
class StockLocationEdit(AjaxUpdateView):
|
||||||
"""
|
"""
|
||||||
View for editing details of a StockLocation.
|
View for editing details of a StockLocation.
|
||||||
|
@ -4,15 +4,12 @@
|
|||||||
|
|
||||||
/* globals
|
/* globals
|
||||||
attachSelect,
|
attachSelect,
|
||||||
attachToggle,
|
|
||||||
blankImage,
|
|
||||||
enableField,
|
enableField,
|
||||||
clearField,
|
clearField,
|
||||||
clearFieldOptions,
|
clearFieldOptions,
|
||||||
closeModal,
|
closeModal,
|
||||||
|
constructField,
|
||||||
constructFormBody,
|
constructFormBody,
|
||||||
constructNumberInput,
|
|
||||||
createNewModal,
|
|
||||||
getFormFieldValue,
|
getFormFieldValue,
|
||||||
global_settings,
|
global_settings,
|
||||||
handleFormErrors,
|
handleFormErrors,
|
||||||
@ -247,7 +244,7 @@ function adjustStock(action, items, options={}) {
|
|||||||
break;
|
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, {
|
var status = stockStatusDisplay(item.status, {
|
||||||
classes: 'float-right'
|
classes: 'float-right'
|
||||||
@ -268,14 +265,18 @@ function adjustStock(action, items, options={}) {
|
|||||||
var actionInput = '';
|
var actionInput = '';
|
||||||
|
|
||||||
if (actionTitle != null) {
|
if (actionTitle != null) {
|
||||||
actionInput = constructNumberInput(
|
actionInput = constructField(
|
||||||
item.pk,
|
`items_quantity_${pk}`,
|
||||||
{
|
{
|
||||||
value: value,
|
type: 'decimal',
|
||||||
min_value: minValue,
|
min_value: minValue,
|
||||||
max_value: maxValue,
|
max_value: maxValue,
|
||||||
read_only: readonly,
|
value: value,
|
||||||
title: readonly ? '{% trans "Quantity cannot be adjusted for serialized stock" %}' : '{% trans "Specify stock quantity" %}',
|
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 += `
|
html += `
|
||||||
<tr id='stock_item_${pk}' class='stock-item-row'>
|
<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='stock_${pk}'>${quantity}${status}</td>
|
||||||
<td id='location_${pk}'>${location}</td>
|
<td id='location_${pk}'>${location}</td>
|
||||||
<td id='action_${pk}'>
|
<td id='action_${pk}'>
|
||||||
@ -319,50 +320,89 @@ function adjustStock(action, items, options={}) {
|
|||||||
|
|
||||||
html += `</tbody></table>`;
|
html += `</tbody></table>`;
|
||||||
|
|
||||||
var modal = createNewModal({
|
var extraFields = {};
|
||||||
title: formTitle,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extra fields
|
if (specifyLocation) {
|
||||||
var extraFields = {
|
extraFields.location = {};
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructFormBody({}, {
|
if (action != 'delete') {
|
||||||
preFormContent: html,
|
extraFields.notes = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructForm(url, {
|
||||||
|
method: 'POST',
|
||||||
fields: extraFields,
|
fields: extraFields,
|
||||||
|
preFormContent: html,
|
||||||
confirm: true,
|
confirm: true,
|
||||||
confirmMessage: '{% trans "Confirm stock adjustment" %}',
|
confirmMessage: '{% trans "Confirm stock adjustment" %}',
|
||||||
modal: modal,
|
title: formTitle,
|
||||||
onSubmit: function(fields) {
|
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') {
|
if (action == 'delete') {
|
||||||
|
|
||||||
var requests = [];
|
var requests = [];
|
||||||
|
|
||||||
items.forEach(function(item) {
|
item_pk_values.forEach(function(pk) {
|
||||||
requests.push(
|
requests.push(
|
||||||
inventreeDelete(
|
inventreeDelete(
|
||||||
`/api/stock/${item.pk}/`,
|
`/api/stock/${pk}/`,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -370,72 +410,40 @@ function adjustStock(action, items, options={}) {
|
|||||||
// Wait for *all* the requests to complete
|
// Wait for *all* the requests to complete
|
||||||
$.when.apply($, requests).done(function() {
|
$.when.apply($, requests).done(function() {
|
||||||
// Destroy the modal window
|
// Destroy the modal window
|
||||||
$(modal).modal('hide');
|
$(opts.modal).modal('hide');
|
||||||
|
|
||||||
if (options.onSuccess) {
|
if (options.success) {
|
||||||
options.onSuccess();
|
options.success();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data to transmit
|
opts.nested = {
|
||||||
var data = {
|
'items': item_pk_values,
|
||||||
items: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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(
|
inventreePut(
|
||||||
url,
|
url,
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
success: function() {
|
success: function(response) {
|
||||||
|
// Hide the modal
|
||||||
|
$(opts.modal).modal('hide');
|
||||||
|
|
||||||
// Destroy the modal window
|
if (options.success) {
|
||||||
$(modal).modal('hide');
|
options.success(response);
|
||||||
|
|
||||||
if (options.onSuccess) {
|
|
||||||
options.onSuccess();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(xhr) {
|
error: function(xhr) {
|
||||||
switch (xhr.status) {
|
switch (xhr.status) {
|
||||||
case 400:
|
case 400:
|
||||||
|
handleFormErrors(xhr.responseJSON, fields, opts);
|
||||||
// Handle errors for standard fields
|
|
||||||
handleFormErrors(
|
|
||||||
xhr.responseJSON,
|
|
||||||
extraFields,
|
|
||||||
{
|
|
||||||
modal: modal,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$(modal).modal('hide');
|
$(opts.modal).modal('hide');
|
||||||
showApiError(xhr);
|
showApiError(xhr);
|
||||||
break;
|
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');
|
var items = $(table).bootstrapTable('getSelections');
|
||||||
|
|
||||||
adjustStock(action, items, {
|
adjustStock(action, items, {
|
||||||
onSuccess: function() {
|
success: function() {
|
||||||
$(table).bootstrapTable('refresh');
|
$(table).bootstrapTable('refresh');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user