mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
017ccaa27a
commit
634daa2161
@ -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: {
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user