2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +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

@ -19,8 +19,8 @@ Relevant PRs:
from decimal import Decimal
from django.db import models
from django.db.models import (F, FloatField, Func, IntegerField, OuterRef, Q,
Subquery)
from django.db.models import (DecimalField, ExpressionWrapper, F, FloatField,
Func, IntegerField, OuterRef, Q, Subquery)
from django.db.models.functions import Coalesce
from sql_util.utils import SubquerySum
@ -32,19 +32,43 @@ from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
def annotate_on_order_quantity(reference: str = ''):
"""Annotate the 'on order' quantity for each part in a queryset"""
"""Annotate the 'on order' quantity for each part in a queryset.
Sum the 'remaining quantity' of each line item for any open purchase orders for each part:
- Purchase order must be 'active' or 'pending'
- Received quantity must be less than line item quantity
Note that in addition to the 'quantity' on order, we must also take into account 'pack_size'.
"""
# Filter only 'active' purhase orders
order_filter = Q(order__status__in=PurchaseOrderStatus.OPEN)
# Filter only line with outstanding quantity
order_filter = Q(
order__status__in=PurchaseOrderStatus.OPEN,
quantity__gt=F('received'),
)
return Coalesce(
SubquerySum(f'{reference}supplier_parts__purchase_order_line_items__quantity', filter=order_filter),
SubquerySum(
ExpressionWrapper(
F(f'{reference}supplier_parts__purchase_order_line_items__quantity') * F(f'{reference}supplier_parts__pack_size'),
output_field=DecimalField(),
),
filter=order_filter
),
Decimal(0),
output_field=models.DecimalField()
output_field=DecimalField()
) - Coalesce(
SubquerySum(f'{reference}supplier_parts__purchase_order_line_items__received', filter=order_filter),
SubquerySum(
ExpressionWrapper(
F(f'{reference}supplier_parts__purchase_order_line_items__received') * F(f'{reference}supplier_parts__pack_size'),
output_field=DecimalField(),
),
filter=order_filter
),
Decimal(0),
output_field=models.DecimalField(),
output_field=DecimalField(),
)

View File

@ -2036,22 +2036,30 @@ class Part(MetadataMixin, MPTTModel):
@property
def on_order(self):
"""Return the total number of items on order for this part."""
orders = self.supplier_parts.filter(purchase_order_line_items__order__status__in=PurchaseOrderStatus.OPEN).aggregate(
quantity=Sum('purchase_order_line_items__quantity'),
received=Sum('purchase_order_line_items__received')
)
"""Return the total number of items on order for this part.
quantity = orders['quantity']
received = orders['received']
Note that some supplier parts may have a different pack_size attribute,
and this needs to be taken into account!
"""
if quantity is None:
quantity = 0
quantity = 0
if received is None:
received = 0
# Iterate through all supplier parts
for sp in self.supplier_parts.all():
return quantity - received
# Look at any incomplete line item for open orders
lines = sp.purchase_order_line_items.filter(
order__status__in=PurchaseOrderStatus.OPEN,
quantity__gt=F('received'),
)
for line in lines:
remaining = line.quantity - line.received
if remaining > 0:
quantity += remaining * sp.pack_size
return quantity
def get_parameters(self):
"""Return all parameters for this part, ordered by name."""

View File

@ -1,5 +1,7 @@
"""Unit tests for the various part API endpoints"""
from random import randint
from django.urls import reverse
import PIL
@ -9,7 +11,7 @@ from rest_framework.test import APIClient
import build.models
import order.models
from common.models import InvenTreeSetting
from company.models import Company
from company.models import Company, SupplierPart
from InvenTree.api_tester import InvenTreeAPITestCase
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
StockStatus)
@ -1676,6 +1678,110 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
self.assertEqual(part.total_stock, 91)
self.assertEqual(part.available_stock, 56)
def test_on_order(self):
"""Test that the 'on_order' queryset annotation works as expected.
This queryset annotation takes into account any outstanding line items for active orders,
and should also use the 'pack_size' of the supplier part objects.
"""
supplier = Company.objects.create(
name='Paint Supplies',
description='A supplier of paints',
is_supplier=True
)
# First, create some parts
paint = PartCategory.objects.create(
parent=None,
name="Paint",
description="Paints and such",
)
for color in ['Red', 'Green', 'Blue', 'Orange', 'Yellow']:
p = Part.objects.create(
category=paint,
units='litres',
name=f"{color} Paint",
description=f"Paint which is {color} in color"
)
# Create multiple supplier parts in different sizes
for pk_sz in [1, 10, 25, 100]:
sp = SupplierPart.objects.create(
part=p,
supplier=supplier,
SKU=f"PNT-{color}-{pk_sz}L",
pack_size=pk_sz,
)
self.assertEqual(p.supplier_parts.count(), 4)
# Check that we have the right base data to start with
self.assertEqual(paint.parts.count(), 5)
self.assertEqual(supplier.supplied_parts.count(), 20)
supplier_parts = supplier.supplied_parts.all()
# Create multiple orders
for _ii in range(5):
po = order.models.PurchaseOrder.objects.create(
supplier=supplier,
description='ordering some paint',
)
# Order an assortment of items
for sp in supplier_parts:
# Generate random quantity to order
quantity = randint(10, 20)
# Mark up to half of the quantity as received
received = randint(0, quantity // 2)
# Add a line item
item = order.models.PurchaseOrderLineItem.objects.create(
part=sp,
order=po,
quantity=quantity,
received=received,
)
# Now grab a list of parts from the API
response = self.get(
reverse('api-part-list'),
{
'category': paint.pk,
},
expected_code=200,
)
# Check that the correct number of items have been returned
self.assertEqual(len(response.data), 5)
for item in response.data:
# Calculate the 'ordering' quantity from first principles
p = Part.objects.get(pk=item['pk'])
on_order = 0
for sp in p.supplier_parts.all():
for line_item in sp.purchase_order_line_items.all():
po = line_item.order
if po.status in PurchaseOrderStatus.OPEN:
remaining = line_item.quantity - line_item.received
if remaining > 0:
on_order += remaining * sp.pack_size
# The annotated quantity must be equal to the hand-calculated quantity
self.assertEqual(on_order, item['ordering'])
# The annotated quantity must also match the part.on_order quantity
self.assertEqual(on_order, p.on_order)
class BomItemTest(InvenTreeAPITestCase):
"""Unit tests for the BomItem API."""