mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
Add flag to API which allows using pack size (#4741)
* Add flag to API which allows using pack size when adding stock items manually * Check for use_pack_size before pop * Add test data and tests * Improve data handling * Add form field for use_pack_size when adding stock * Add description of pack size to docs * Don't check for supplier part if it is None * Move form field to after supplier part, for better logic * Fix wrong function * Fix tests * Adjust purchase price when using pack size * Adjust help text for purchase price * Adjust help text for purchase price some more * Fix tests for purchase price of added stock * Update api_version.py
This commit is contained in:
@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 111
|
||||
INVENTREE_API_VERSION = 112
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v112 -> 2023-05-13: https://github.com/inventree/InvenTree/pull/4741
|
||||
- Adds flag use_pack_size to the stock addition API, which allows addings packs
|
||||
|
||||
v111 -> 2023-05-02 : https://github.com/inventree/InvenTree/pull/4367
|
||||
- Adds tags to the Part serializer
|
||||
- Adds tags to the SupplierPart serializer
|
||||
|
@ -59,3 +59,11 @@
|
||||
part: 4
|
||||
supplier: 2
|
||||
SKU: 'R_4K7_0603'
|
||||
|
||||
- model: company.supplierpart
|
||||
pk: 6
|
||||
fields:
|
||||
part: 4
|
||||
supplier: 2
|
||||
SKU: 'R_4K7_0603.100PCK'
|
||||
pack_size: 100
|
||||
|
@ -602,6 +602,35 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
# Check if a set of serial numbers was provided
|
||||
serial_numbers = data.get('serial_numbers', '')
|
||||
|
||||
# Check if the supplier_part has a package size defined, which is not 1
|
||||
if 'supplier_part' in data and data['supplier_part'] is not None:
|
||||
try:
|
||||
supplier_part = SupplierPart.objects.get(pk=data.get('supplier_part', None))
|
||||
except (ValueError, SupplierPart.DoesNotExist):
|
||||
raise ValidationError({
|
||||
'supplier_part': _('The given supplier part does not exist'),
|
||||
})
|
||||
|
||||
if supplier_part.pack_size != 1:
|
||||
# Skip this check if pack size is 1 - makes no difference
|
||||
# use_pack_size = True -> Multiply quantity by pack size
|
||||
# use_pack_size = False -> Use quantity as is
|
||||
if 'use_pack_size' not in data:
|
||||
raise ValidationError({
|
||||
'use_pack_size': _('The supplier part has a pack size defined, but flag use_pack_size not set'),
|
||||
})
|
||||
else:
|
||||
if bool(data.get('use_pack_size')):
|
||||
data['quantity'] = int(quantity) * float(supplier_part.pack_size)
|
||||
quantity = data.get('quantity', None)
|
||||
# Divide purchase price by pack size, to save correct price per stock item
|
||||
data['purchase_price'] = float(data['purchase_price']) / float(supplier_part.pack_size)
|
||||
|
||||
# Now remove the flag from data, so that it doesn't interfere with saving
|
||||
# Do this regardless of results above
|
||||
if 'use_pack_size' in data:
|
||||
data.pop('use_pack_size')
|
||||
|
||||
# Assign serial numbers for a trackable part
|
||||
if serial_numbers:
|
||||
|
||||
|
@ -124,6 +124,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
'updated',
|
||||
'purchase_price',
|
||||
'purchase_price_currency',
|
||||
'use_pack_size',
|
||||
|
||||
'tags',
|
||||
]
|
||||
@ -140,6 +141,13 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
'updated',
|
||||
]
|
||||
|
||||
"""
|
||||
Fields used when creating a stock item
|
||||
"""
|
||||
extra_kwargs = {
|
||||
'use_pack_size': {'write_only': True},
|
||||
}
|
||||
|
||||
part = serializers.PrimaryKeyRelatedField(
|
||||
queryset=part_models.Part.objects.all(),
|
||||
many=False, allow_null=False,
|
||||
@ -147,6 +155,17 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
label=_("Part"),
|
||||
)
|
||||
|
||||
"""
|
||||
Field used when creating a stock item
|
||||
"""
|
||||
use_pack_size = serializers.BooleanField(
|
||||
write_only=True,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text=_("Use pack size when adding: the quantity defined is the number of packs"),
|
||||
label=("Use pack size"),
|
||||
)
|
||||
|
||||
def validate_part(self, part):
|
||||
"""Ensure the provided Part instance is valid"""
|
||||
|
||||
@ -231,7 +250,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
||||
purchase_price = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
label=_('Purchase Price'),
|
||||
allow_null=True,
|
||||
help_text=_('Purchase price of this stock item'),
|
||||
help_text=_('Purchase price of this stock item, per unit or pack'),
|
||||
)
|
||||
|
||||
purchase_price_currency = InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item'))
|
||||
|
@ -10,6 +10,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
import tablib
|
||||
from djmoney.money import Money
|
||||
from rest_framework import status
|
||||
|
||||
import company.models
|
||||
@ -664,6 +665,113 @@ class StockItemTest(StockAPITestCase):
|
||||
expected_code=201
|
||||
)
|
||||
|
||||
def test_stock_item_create_withsupplierpart(self):
|
||||
"""Test creation of a StockItem via the API, including SupplierPart data."""
|
||||
|
||||
# POST with non-existent supplier part
|
||||
response = self.post(
|
||||
self.list_url,
|
||||
data={
|
||||
'part': 1,
|
||||
'location': 1,
|
||||
'quantity': 4,
|
||||
'supplier_part': 1000991
|
||||
},
|
||||
expected_code=400
|
||||
)
|
||||
|
||||
self.assertIn('The given supplier part does not exist', str(response.data))
|
||||
|
||||
# POST with valid supplier part, no pack size defined
|
||||
# Get current count of number of parts
|
||||
part_4 = part.models.Part.objects.get(pk=4)
|
||||
current_count = part_4.available_stock
|
||||
response = self.post(
|
||||
self.list_url,
|
||||
data={
|
||||
'part': 4,
|
||||
'location': 1,
|
||||
'quantity': 3,
|
||||
'supplier_part': 5,
|
||||
'purchase_price': 123.45,
|
||||
'purchase_price_currency': 'USD',
|
||||
},
|
||||
expected_code=201
|
||||
)
|
||||
# Reload part, count stock again
|
||||
part_4 = part.models.Part.objects.get(pk=4)
|
||||
self.assertEqual(part_4.available_stock, current_count + 3)
|
||||
stock_4 = StockItem.objects.get(pk=response.data['pk'])
|
||||
self.assertEqual(stock_4.purchase_price, Money('123.450000', 'USD'))
|
||||
|
||||
# POST with valid supplier part, no pack size defined
|
||||
# Send use_pack_size along, make sure this doesn't break stuff
|
||||
# Get current count of number of parts
|
||||
part_4 = part.models.Part.objects.get(pk=4)
|
||||
current_count = part_4.available_stock
|
||||
response = self.post(
|
||||
self.list_url,
|
||||
data={
|
||||
'part': 4,
|
||||
'location': 1,
|
||||
'quantity': 12,
|
||||
'supplier_part': 5,
|
||||
'use_pack_size': True,
|
||||
'purchase_price': 123.45,
|
||||
'purchase_price_currency': 'USD',
|
||||
},
|
||||
expected_code=201
|
||||
)
|
||||
# Reload part, count stock again
|
||||
part_4 = part.models.Part.objects.get(pk=4)
|
||||
self.assertEqual(part_4.available_stock, current_count + 12)
|
||||
stock_4 = StockItem.objects.get(pk=response.data['pk'])
|
||||
self.assertEqual(stock_4.purchase_price, Money('123.450000', 'USD'))
|
||||
|
||||
# POST with valid supplier part, WITH pack size defined - but ignore
|
||||
# Supplier part 6 is a 100-pack, otherwise same as SP 5
|
||||
current_count = part_4.available_stock
|
||||
response = self.post(
|
||||
self.list_url,
|
||||
data={
|
||||
'part': 4,
|
||||
'location': 1,
|
||||
'quantity': 3,
|
||||
'supplier_part': 6,
|
||||
'use_pack_size': False,
|
||||
'purchase_price': 123.45,
|
||||
'purchase_price_currency': 'USD',
|
||||
},
|
||||
expected_code=201
|
||||
)
|
||||
# Reload part, count stock again
|
||||
part_4 = part.models.Part.objects.get(pk=4)
|
||||
self.assertEqual(part_4.available_stock, current_count + 3)
|
||||
stock_4 = StockItem.objects.get(pk=response.data['pk'])
|
||||
self.assertEqual(stock_4.purchase_price, Money('123.450000', 'USD'))
|
||||
|
||||
# POST with valid supplier part, WITH pack size defined and used
|
||||
# Supplier part 6 is a 100-pack, otherwise same as SP 5
|
||||
current_count = part_4.available_stock
|
||||
response = self.post(
|
||||
self.list_url,
|
||||
data={
|
||||
'part': 4,
|
||||
'location': 1,
|
||||
'quantity': 3,
|
||||
'supplier_part': 6,
|
||||
'use_pack_size': True,
|
||||
'purchase_price': 123.45,
|
||||
'purchase_price_currency': 'USD',
|
||||
},
|
||||
expected_code=201
|
||||
)
|
||||
# Reload part, count stock again
|
||||
part_4 = part.models.Part.objects.get(pk=4)
|
||||
self.assertEqual(part_4.available_stock, current_count + 3 * 100)
|
||||
stock_4 = StockItem.objects.get(pk=response.data['pk'])
|
||||
self.assertEqual(stock_4.purchase_price, Money('1.234500', 'USD'))
|
||||
|
||||
def test_creation_with_serials(self):
|
||||
"""Test that serialized stock items can be created via the API."""
|
||||
trackable_part = part.models.Part.objects.create(
|
||||
|
@ -289,6 +289,9 @@ function stockItemFields(options={}) {
|
||||
return query;
|
||||
}
|
||||
},
|
||||
use_pack_size: {
|
||||
help_text: '{% trans "Add given quantity as packs instead of individual items" %}',
|
||||
},
|
||||
location: {
|
||||
icon: 'fa-sitemap',
|
||||
filters: {
|
||||
|
Reference in New Issue
Block a user