2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00
InvenTree/InvenTree/part/filters.py
Oliver 198ac9b275
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"
2022-09-08 09:49:14 +10:00

213 lines
7.1 KiB
Python

"""Custom query filters for the Part models
The code here makes heavy use of subquery annotations!
Useful References:
- https://hansonkd.medium.com/the-dramatic-benefits-of-django-subqueries-and-annotations-4195e0dafb16
- https://pypi.org/project/django-sql-utils/
- https://docs.djangoproject.com/en/4.0/ref/models/expressions/
- https://stackoverflow.com/questions/42543978/django-1-11-annotating-a-subquery-aggregate
Relevant PRs:
- https://github.com/inventree/InvenTree/pull/2797/
- https://github.com/inventree/InvenTree/pull/2827
"""
from decimal import Decimal
from django.db import models
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
import part.models
import stock.models
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
SalesOrderStatus)
def annotate_on_order_quantity(reference: str = ''):
"""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
# Filter only line with outstanding quantity
order_filter = Q(
order__status__in=PurchaseOrderStatus.OPEN,
quantity__gt=F('received'),
)
return Coalesce(
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=DecimalField()
) - Coalesce(
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=DecimalField(),
)
def annotate_total_stock(reference: str = ''):
"""Annotate 'total stock' quantity against a queryset:
- This function calculates the 'total stock' for a given part
- Finds all stock items associated with each part (using the provided filter)
- Aggregates the 'quantity' of each relevent stock item
Args:
reference: The relationship reference of the part from the current model e.g. 'part'
stock_filter: Q object which defines how to filter the stock items
"""
# Stock filter only returns 'in stock' items
stock_filter = stock.models.StockItem.IN_STOCK_FILTER
return Coalesce(
SubquerySum(
f'{reference}stock_items__quantity',
filter=stock_filter,
),
Decimal(0),
output_field=models.DecimalField(),
)
def annotate_build_order_allocations(reference: str = ''):
"""Annotate the total quantity of each part allocated to build orders:
- This function calculates the total part quantity allocated to open build orders
- Finds all build order allocations for each part (using the provided filter)
- Aggregates the 'allocated quantity' for each relevent build order allocation item
Args:
reference: The relationship reference of the part from the current model
build_filter: Q object which defines how to filter the allocation items
"""
# Build filter only returns 'active' build orders
build_filter = Q(build__status__in=BuildStatus.ACTIVE_CODES)
return Coalesce(
SubquerySum(
f'{reference}stock_items__allocations__quantity',
filter=build_filter,
),
Decimal(0),
output_field=models.DecimalField(),
)
def annotate_sales_order_allocations(reference: str = ''):
"""Annotate the total quantity of each part allocated to sales orders:
- This function calculates the total part quantity allocated to open sales orders"
- Finds all sales order allocations for each part (using the provided filter)
- Aggregates the 'allocated quantity' for each relevent sales order allocation item
Args:
reference: The relationship reference of the part from the current model
order_filter: Q object which defines how to filter the allocation items
"""
# Order filter only returns incomplete shipments for open orders
order_filter = Q(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None,
)
return Coalesce(
SubquerySum(
f'{reference}stock_items__sales_order_allocations__quantity',
filter=order_filter,
),
Decimal(0),
output_field=models.DecimalField(),
)
def variant_stock_query(reference: str = '', filter: Q = stock.models.StockItem.IN_STOCK_FILTER):
"""Create a queryset to retrieve all stock items for variant parts under the specified part
- Useful for annotating a queryset with aggregated information about variant parts
Args:
reference: The relationship reference of the part from the current model
filter: Q object which defines how to filter the returned StockItem instances
"""
return stock.models.StockItem.objects.filter(
part__tree_id=OuterRef(f'{reference}tree_id'),
part__lft__gt=OuterRef(f'{reference}lft'),
part__rght__lt=OuterRef(f'{reference}rght'),
).filter(filter)
def annotate_variant_quantity(subquery: Q, reference: str = 'quantity'):
"""Create a subquery annotation for all variant part stock items on the given parent query
Args:
subquery: A 'variant_stock_query' Q object
reference: The relationship reference of the variant stock items from the current queryset
"""
return Coalesce(
Subquery(
subquery.annotate(
total=Func(F(reference), function='SUM', output_field=FloatField())
).values('total')
),
0,
output_field=FloatField(),
)
def annotate_category_parts():
"""Construct a queryset annotation which returns the number of parts in a particular category.
- Includes parts in subcategories also
- Requires subquery to perform annotation
"""
# Construct a subquery to provide all parts in this category and any subcategories:
subquery = part.models.Part.objects.exclude(category=None).filter(
category__tree_id=OuterRef('tree_id'),
category__lft__gte=OuterRef('lft'),
category__rght__lte=OuterRef('rght'),
category__level__gte=OuterRef('level'),
)
return Coalesce(
Subquery(
subquery.annotate(
total=Func(F('pk'), function='COUNT', output_field=IntegerField())
).values('total'),
),
0,
output_field=IntegerField()
)