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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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: {

View File

@ -35,3 +35,11 @@ The *Stock Item* detail view shows information regarding the particular stock it
Every time a *Stock Item* is adjusted, a *Stock Tracking* entry is automatically created. This ensures a complete history of the *Stock Item* is maintained as long as the item is in the system.
Each stock tracking historical item records the user who performed the action.
## Supplier Part Pack Size
Supplier parts can have a pack size defined. This value is defined when creating or editing a part. By default, the pack size is 1.
When buying parts, they are bought in packs. This is taken into account in Purchase Orders: if a supplier part with a pack size of 5 is bought in a quantity of 4, 20 parts will be added to stock when the parts are received.
When adding stock manually, the supplier part can be added in packs or in individual parts. This is to allow the addition of items in opened packages. Set the flag "Use pack size" (`use_pack_size` in the API) to True in order to add parts in packs.