2
0
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:
miggland
2023-05-13 13:32:25 +02:00
committed by GitHub
parent 017ccaa27a
commit 634daa2161
7 changed files with 180 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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