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:
@ -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()
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user