2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-02 03:30:54 +00:00

Feature: Supplier part pack size (#3644)

* Adds 'pack_size' field to SupplierPart model

* Edit pack_size for SupplierPart via API

* Display pack size in supplier part page template

* Improve table ordering for SupplierPart table

* Fix for API filtering

- Need to use custom filter class

* Adds functionality to duplicate an existing SupplierPart

* Bump API version number

* Display annotation of pack size in purchase order line item table

* Display additional information in part purchase order table

* Add UOM to purchase order table

* Improve receive items functionality

* Indicate quantity which will be received in modal form

* Update the received quantity as the user changes the value

* Take  the pack_size into account when receiving line items

* Take supplierpart pack size into account when receiving line items

* Add "pack size" column to purchase order line item table

* Tweak supplier part table

* Update 'on_order' queryset annotation to take pack_size into account

- May god have mercy on my soul

* Adds a unit test to validate that the on_order queryset annotation is working as expected

* Update Part.on_order method to take pack_size into account

- Check in existing unit test also

* Fix existing unit tests

- Previous unit test was actually in error
- Logic for calculating "on_order" was broked

* More unit tests for receiving items against a purchase order

* Allow pack_size < 1

* Display pack size when adding / editing PurchaseOrderLineItem

* Fix bug in part purchase order table

* Update part purchase order table again

* Exclude notificationmessage when exporting dataset

* Also display pack size when ordering parts from secondary form

* javascript linting

* Change user facing strings to "Pack Quantity"
This commit is contained in:
Oliver
2022-09-08 09:49:14 +10:00
committed by GitHub
parent 890c998420
commit 198ac9b275
17 changed files with 567 additions and 60 deletions

View File

@ -475,6 +475,9 @@ class PurchaseOrder(Order):
# Create a new stock item
if line.part and quantity > 0:
# Take the 'pack_size' of the SupplierPart into account
pack_quantity = Decimal(quantity) * Decimal(line.part.pack_size)
# Determine if we should individually serialize the items, or not
if type(serials) is list and len(serials) > 0:
serialize = True
@ -488,7 +491,7 @@ class PurchaseOrder(Order):
part=line.part.part,
supplier_part=line.part,
location=location,
quantity=1 if serialize else quantity,
quantity=1 if serialize else pack_quantity,
purchase_order=self,
status=status,
batch=batch_code,
@ -515,6 +518,7 @@ class PurchaseOrder(Order):
)
# Update the number of parts received against the particular line item
# Note that this quantity does *not* take the pack_size into account, it is "number of packs"
line.received += quantity
line.save()

View File

@ -515,11 +515,14 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
serial_numbers = data.get('serial_numbers', '').strip()
base_part = line_item.part.part
pack_size = line_item.part.pack_size
pack_quantity = pack_size * quantity
# Does the quantity need to be "integer" (for trackable parts?)
if base_part.trackable:
if Decimal(quantity) != int(quantity):
if Decimal(pack_quantity) != int(pack_quantity):
raise ValidationError({
'quantity': _('An integer quantity must be provided for trackable parts'),
})
@ -528,7 +531,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
if serial_numbers:
try:
# Pass the serial numbers through to the parent serializer once validated
data['serials'] = extract_serial_numbers(serial_numbers, quantity, base_part.getLatestSerialNumberInt())
data['serials'] = extract_serial_numbers(serial_numbers, pack_quantity, base_part.getLatestSerialNumberInt())
except DjangoValidationError as e:
raise ValidationError({
'serial_numbers': e.messages,

View File

@ -42,12 +42,18 @@
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu' role='menu'>
<li><a class='dropdown-item' href='#' id='edit-order'><span class='fas fa-edit icon-green'></span> {% trans "Edit order" %}</a></li>
<li><a class='dropdown-item' href='#' id='edit-order'>
<span class='fas fa-edit icon-green'></span> {% trans "Edit order" %}
</a></li>
{% if order.can_cancel %}
<li><a class='dropdown-item' href='#' id='cancel-order'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}</a></li>
<li><a class='dropdown-item' href='#' id='cancel-order'>
<span class='fas fa-times-circle icon-red'></span> {% trans "Cancel order" %}
</a></li>
{% endif %}
{% if roles.purchase_order.add %}
<li><a class='dropdown-item' href='#' id='duplicate-order'><span class='fas fa-clone'></span> {% trans "Duplicate order" %}</a></li>
<li><a class='dropdown-item' href='#' id='duplicate-order'>
<span class='fas fa-clone'></span> {% trans "Duplicate order" %}
</a></li>
{% endif %}
</ul>
</div>
@ -235,19 +241,11 @@ $("#edit-order").click(function() {
$("#receive-order").click(function() {
// Auto select items which have not been fully allocated
var items = $("#po-line-table").bootstrapTable('getData');
var items_to_receive = [];
items.forEach(function(item) {
if (item.received < item.quantity) {
items_to_receive.push(item);
}
});
var items = getTableData('#po-line-table');
receivePurchaseOrderItems(
{{ order.id }},
items_to_receive,
items,
{
success: function() {
$("#po-line-table").bootstrapTable('refresh');

View File

@ -1,6 +1,7 @@
"""Various unit tests for order models"""
from datetime import datetime, timedelta
from decimal import Decimal
import django.core.exceptions as django_exceptions
from django.contrib.auth import get_user_model
@ -194,11 +195,18 @@ class OrderTest(TestCase):
# Receive the rest of the items
order.receive_line_item(line, loc, 50, user=None)
self.assertEqual(part.on_order, 1300)
line = PurchaseOrderLineItem.objects.get(id=2)
in_stock = part.total_stock
order.receive_line_item(line, loc, 500, user=None)
self.assertEqual(part.on_order, 800)
# Check that the part stock quantity has increased by the correct amount
self.assertEqual(part.total_stock, in_stock + 500)
self.assertEqual(part.on_order, 1100)
self.assertEqual(order.status, PurchaseOrderStatus.PLACED)
for line in order.pending_line_items():
@ -206,6 +214,91 @@ class OrderTest(TestCase):
self.assertEqual(order.status, PurchaseOrderStatus.COMPLETE)
def test_receive_pack_size(self):
"""Test receiving orders from suppliers with different pack_size values"""
prt = Part.objects.get(pk=1)
sup = Company.objects.get(pk=1)
# Create a new supplier part with larger pack size
sp_1 = SupplierPart.objects.create(
part=prt,
supplier=sup,
SKU='SKUx10',
pack_size=10,
)
# Create a new supplier part with smaller pack size
sp_2 = SupplierPart.objects.create(
part=prt,
supplier=sup,
SKU='SKUx0.1',
pack_size=0.1,
)
# Record values before we start
on_order = prt.on_order
in_stock = prt.total_stock
n = PurchaseOrder.objects.count()
# Create a new PurchaseOrder
po = PurchaseOrder.objects.create(
supplier=sup,
reference=f"PO-{n + 1}",
description='Some PO',
)
# Add line items
# 3 x 10 = 30
line_1 = PurchaseOrderLineItem.objects.create(
order=po,
part=sp_1,
quantity=3
)
# 13 x 0.1 = 1.3
line_2 = PurchaseOrderLineItem.objects.create(
order=po,
part=sp_2,
quantity=13,
)
po.place_order()
# The 'on_order' quantity should have been increased by 31.3
self.assertEqual(prt.on_order, round(on_order + Decimal(31.3), 1))
loc = StockLocation.objects.get(id=1)
# Receive 1x item against line_1
po.receive_line_item(line_1, loc, 1, user=None)
# Receive 5x item against line_2
po.receive_line_item(line_2, loc, 5, user=None)
# Check that the line items have been updated correctly
self.assertEqual(line_1.quantity, 3)
self.assertEqual(line_1.received, 1)
self.assertEqual(line_1.remaining(), 2)
self.assertEqual(line_2.quantity, 13)
self.assertEqual(line_2.received, 5)
self.assertEqual(line_2.remaining(), 8)
# The 'on_order' quantity should have decreased by 10.5
self.assertEqual(
prt.on_order,
round(on_order + Decimal(31.3) - Decimal(10.5), 1)
)
# The 'in_stock' quantity should have increased by 10.5
self.assertEqual(
prt.total_stock,
round(in_stock + Decimal(10.5), 1)
)
def test_overdue_notification(self):
"""Test overdue purchase order notification