Skip to content

Commit

Permalink
Add flag to API which allows using pack size (#4741)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
miggland authored May 13, 2023
1 parent 017ccaa commit 634daa2
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 2 deletions.
5 changes: 4 additions & 1 deletion InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions InvenTree/company/fixtures/supplier_part.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 29 additions & 0 deletions InvenTree/stock/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,35 @@ def create(self, request, *args, **kwargs):
# 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:

Expand Down
21 changes: 20 additions & 1 deletion InvenTree/stock/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class Meta:
'updated',
'purchase_price',
'purchase_price_currency',
'use_pack_size',

'tags',
]
Expand All @@ -140,13 +141,31 @@ class Meta:
'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,
help_text=_("Base Part"),
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"""

Expand Down Expand Up @@ -231,7 +250,7 @@ def annotate_queryset(queryset):
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'))
Expand Down
108 changes: 108 additions & 0 deletions InvenTree/stock/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.urls import reverse

import tablib
from djmoney.money import Money
from rest_framework import status

import company.models
Expand Down Expand Up @@ -664,6 +665,113 @@ def test_stock_item_create(self):
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(
Expand Down
3 changes: 3 additions & 0 deletions InvenTree/templates/js/translated/stock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/stock/stock.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

0 comments on commit 634daa2

Please sign in to comment.