From f197d8b1da0ee3f3220fee8a8215d718fc5d3be3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 5 Oct 2021 21:55:05 +1100 Subject: [PATCH 1/7] Adds a DRF serializer for stock adjustments - Currently the "StockCount" action has been transferred --- InvenTree/stock/api.py | 55 +++++++--------- InvenTree/stock/serializers.py | 117 +++++++++++++++++++++++++++++---- 2 files changed, 128 insertions(+), 44 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 27c2426d53..62f70ff8fc 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -41,11 +41,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 +79,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 @@ -138,8 +134,6 @@ class StockAdjust(APIView): queryset = StockItem.objects.none() - allow_missing_quantity = False - def get_items(self, request): """ Return a list of items posted to the endpoint. @@ -206,23 +200,22 @@ class StockAdjust(APIView): self.notes = str(request.data.get('notes', '')) -class StockCount(StockAdjust): +class StockCount(generics.CreateAPIView): """ Endpoint for counting stock (performing a stocktake). """ - def post(self, request, *args, **kwargs): + queryset = StockItem.objects.none() - self.get_items(request) + serializer_class = StockSerializers.StockCountSerializer - n = 0 + def get_serializer_context(self): + + context = super().get_serializer_context() - for item in self.items: + context['request'] = self.request - if item['item'].stocktake(item['quantity'], request.user, notes=self.notes): - n += 1 - - return Response({'success': _('Updated stock for {n} items').format(n=n)}) + return context class StockAdd(StockAdjust): @@ -315,7 +308,7 @@ class StockLocationList(generics.ListCreateAPIView): """ queryset = StockLocation.objects.all() - serializer_class = LocationSerializer + serializer_class = StockSerializers.LocationSerializer def filter_queryset(self, queryset): """ @@ -517,7 +510,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 +632,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 +658,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 +947,7 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ queryset = StockItemAttachment.objects.all() - serializer_class = StockItemAttachmentSerializer + serializer_class = StockSerializers.StockItemAttachmentSerializer filter_backends = [ DjangoFilterBackend, @@ -973,7 +966,7 @@ class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMix """ queryset = StockItemAttachment.objects.all() - serializer_class = StockItemAttachmentSerializer + serializer_class = StockSerializers.StockItemAttachmentSerializer class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView): @@ -982,7 +975,7 @@ class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView): """ queryset = StockItemTestResult.objects.all() - serializer_class = StockItemTestResultSerializer + serializer_class = StockSerializers.StockItemTestResultSerializer class StockItemTestResultList(generics.ListCreateAPIView): @@ -991,7 +984,7 @@ class StockItemTestResultList(generics.ListCreateAPIView): """ queryset = StockItemTestResult.objects.all() - serializer_class = StockItemTestResultSerializer + serializer_class = StockSerializers.StockItemTestResultSerializer filter_backends = [ DjangoFilterBackend, @@ -1039,7 +1032,7 @@ class StockTrackingDetail(generics.RetrieveAPIView): """ queryset = StockItemTracking.objects.all() - serializer_class = StockTrackingSerializer + serializer_class = StockSerializers.StockTrackingSerializer class StockTrackingList(generics.ListAPIView): @@ -1052,7 +1045,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 +1081,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 +1090,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 +1172,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..801b9a9d94 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,92 @@ 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=_('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=False, + ) + + 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): + """ + Perform the database transactions to count the stock + """ + request = self.context['request'] + + data = self.validated_data + items = data['items'] + notes = data['notes'] + + with transaction.atomic(): + for item in items: + + stock_item = item['pk'] + quantity = item['quantity'] + + stock_item.stocktake( + quantity, + request.user, + notes=notes + ) From 102f886d81058cd784910bd0d128e1a71d01fdb7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 5 Oct 2021 22:26:21 +1100 Subject: [PATCH 2/7] All stock adjustment actions ported to new scheme - Bumped API version too --- InvenTree/InvenTree/version.py | 6 +- InvenTree/stock/api.py | 154 ++++----------------------------- InvenTree/stock/serializers.py | 113 ++++++++++++++++++++++-- 3 files changed, 128 insertions(+), 145 deletions(-) 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/stock/api.py b/InvenTree/stock/api.py index 62f70ff8fc..de8314b830 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -120,7 +120,7 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): instance.mark_for_deletion() -class StockAdjust(APIView): +class StockAdjustView(generics.CreateAPIView): """ A generic class for handling stocktake actions. @@ -134,174 +134,50 @@ class StockAdjust(APIView): queryset = StockItem.objects.none() - 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. - """ + def get_serializer_context(self): + + context = super().get_serializer_context() - _items = [] + context['request'] = self.request - 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(generics.CreateAPIView): +class StockCount(StockAdjustView): """ Endpoint for counting stock (performing a stocktake). """ - queryset = StockItem.objects.none() - serializer_class = StockSerializers.StockCountSerializer - def get_serializer_context(self): - - context = super().get_serializer_context() - context['request'] = self.request - - return context - - -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 diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 801b9a9d94..e6bbc72dd2 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -420,7 +420,8 @@ class StockAdjustmentItemSerializer(serializers.Serializer): many=False, allow_null=False, required=True, - label=_('StockItem primary key value') + label='stock_item', + help_text=_('StockItem primary key value') ) quantity = serializers.DecimalField( @@ -446,7 +447,9 @@ class StockAdjustmentSerializer(serializers.Serializer): notes = serializers.CharField( required=False, - allow_blank=False, + allow_blank=True, + label=_("Notes"), + help_text=_("Stock transaction notes"), ) def validate(self, data): @@ -467,9 +470,7 @@ class StockCountSerializer(StockAdjustmentSerializer): """ def save(self): - """ - Perform the database transactions to count the stock - """ + request = self.context['request'] data = self.validated_data @@ -487,3 +488,105 @@ class StockCountSerializer(StockAdjustmentSerializer): 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['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['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['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=item['quantity'] + ) From 0a2a81582ede69c0f4351f5d57cf3b169b4ca9d3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 5 Oct 2021 22:46:34 +1100 Subject: [PATCH 3/7] Handle case where notes are not provided --- InvenTree/stock/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index e6bbc72dd2..c4fa7fb902 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -475,7 +475,7 @@ class StockCountSerializer(StockAdjustmentSerializer): data = self.validated_data items = data['items'] - notes = data['notes'] + notes = data.get('notes', '') with transaction.atomic(): for item in items: @@ -500,7 +500,7 @@ class StockAddSerializer(StockAdjustmentSerializer): request = self.context['request'] data = self.validated_data - notes = data['notes'] + notes = data.get('notes', '') with transaction.atomic(): for item in data['items']: @@ -525,7 +525,7 @@ class StockRemoveSerializer(StockAdjustmentSerializer): request = self.context['request'] data = self.validated_data - notes = data['notes'] + notes = data.get('notes', '') with transaction.atomic(): for item in data['items']: @@ -575,7 +575,7 @@ class StockTransferSerializer(StockAdjustmentSerializer): data = self.validated_data items = data['items'] - notes = data['notes'] + notes = data.get('notes', '') location = data['location'] with transaction.atomic(): From 6fd1abb07ad53c4ea91108f382ecb2a94d17ad1c Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 5 Oct 2021 22:58:28 +1100 Subject: [PATCH 4/7] Remove unused view class --- InvenTree/stock/views.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 2f602a93e1..c9cc8e8724 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -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. From 758e402a66780e50580588925df2140e976cf797 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 5 Oct 2021 23:06:12 +1100 Subject: [PATCH 5/7] PEP style fixes --- InvenTree/stock/api.py | 8 +++----- InvenTree/stock/serializers.py | 5 +++-- InvenTree/stock/views.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index de8314b830..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 @@ -136,11 +134,11 @@ class StockAdjustView(generics.CreateAPIView): def get_serializer_context(self): - context = super().get_serializer_context() + context = super().get_serializer_context() - context['request'] = self.request + context['request'] = self.request - return context + return context class StockCount(StockAdjustView): diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index c4fa7fb902..c44dffe94f 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -466,7 +466,7 @@ class StockAdjustmentSerializer(serializers.Serializer): class StockCountSerializer(StockAdjustmentSerializer): """ - Serializer for counting stock items + Serializer for counting stock items """ def save(self): @@ -539,6 +539,7 @@ class StockRemoveSerializer(StockAdjustmentSerializer): notes=notes ) + class StockTransferSerializer(StockAdjustmentSerializer): """ Serializer for transferring (moving) stock item(s) @@ -588,5 +589,5 @@ class StockTransferSerializer(StockAdjustmentSerializer): location, notes, request.user, - quantity=item['quantity'] + quantity=quantity ) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index c9cc8e8724..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 From 95e7cc7a5d2d32164ff4d18a467ee003c112e618 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 08:56:24 +1100 Subject: [PATCH 6/7] Fixes for unit tests --- InvenTree/part/templates/part/part_base.html | 2 +- .../stock/templates/stock/item_base.html | 2 +- InvenTree/stock/templates/stock/location.html | 2 +- InvenTree/stock/test_api.py | 51 +++-- InvenTree/templates/js/translated/stock.js | 189 +++++++++--------- 5 files changed, 120 insertions(+), 126 deletions(-) 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/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/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 17c2598d1b..c6efd88f5f 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -247,7 +247,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 +268,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 +297,7 @@ function adjustStock(action, items, options={}) { html += ` - ${item.part_detail.full_name} + ${thumb} ${item.part_detail.full_name} ${quantity}${status} ${location} @@ -319,50 +323,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 +413,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 +455,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 +1257,7 @@ function loadStockTable(table, options) { var items = $(table).bootstrapTable('getSelections'); adjustStock(action, items, { - onSuccess: function() { + success: function() { $(table).bootstrapTable('refresh'); } }); From 05de802d1d09b69b7dcfa93e1e6f9f4b2cb370b5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 09:14:06 +1100 Subject: [PATCH 7/7] javascript linting --- InvenTree/templates/js/translated/stock.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index c6efd88f5f..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, @@ -326,7 +323,7 @@ function adjustStock(action, items, options={}) { var extraFields = {}; if (specifyLocation) { - extraFields.location = {}; + extraFields.location = {}; } if (action != 'delete') {