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'] + )