mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 13:05:42 +00:00
Queryset annotation refactor (#3117)
* Refactor out 'ordering' serializer annotation field * Refactor BomItem serializer annotations * Factor out MPTT OuterRef query * Add 'available_stock' annotation to SalesOrderLineItem serializer - Allows for better rendering of stock availability in sales order table * Improve 'available quantity' rendering of salesorderlineitem table * Bump API version * Add docstring
This commit is contained in:
141
InvenTree/part/filters.py
Normal file
141
InvenTree/part/filters.py
Normal file
@ -0,0 +1,141 @@
|
||||
"""Custom query filters for the Part model
|
||||
|
||||
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 OuterRef, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
from sql_util.utils import SubquerySum
|
||||
|
||||
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"""
|
||||
|
||||
# Filter only 'active' purhase orders
|
||||
order_filter = Q(order__status__in=PurchaseOrderStatus.OPEN)
|
||||
|
||||
return Coalesce(
|
||||
SubquerySum(f'{reference}supplier_parts__purchase_order_line_items__quantity', filter=order_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField()
|
||||
) - Coalesce(
|
||||
SubquerySum(f'{reference}supplier_parts__purchase_order_line_items__received', filter=order_filter),
|
||||
Decimal(0),
|
||||
output_field=models.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)
|
@ -4,8 +4,8 @@ import imghdr
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.db.models import (ExpressionWrapper, F, FloatField, Func, OuterRef,
|
||||
Q, Subquery)
|
||||
from django.db.models import (ExpressionWrapper, F, FloatField, Func, Q,
|
||||
Subquery)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -14,6 +14,7 @@ from djmoney.contrib.django_rest_framework import MoneyField
|
||||
from rest_framework import serializers
|
||||
from sql_util.utils import SubqueryCount, SubquerySum
|
||||
|
||||
import part.filters
|
||||
from common.settings import currency_code_default, currency_code_mappings
|
||||
from InvenTree.serializers import (DataFileExtractSerializer,
|
||||
DataFileUploadSerializer,
|
||||
@ -23,9 +24,7 @@ from InvenTree.serializers import (DataFileExtractSerializer,
|
||||
InvenTreeImageSerializerField,
|
||||
InvenTreeModelSerializer,
|
||||
InvenTreeMoneySerializer)
|
||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
||||
SalesOrderStatus)
|
||||
from stock.models import StockItem
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
|
||||
from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
|
||||
PartCategory, PartCategoryParameterTemplate,
|
||||
@ -311,14 +310,6 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
|
||||
Performing database queries as efficiently as possible, to reduce database trips.
|
||||
"""
|
||||
# Annotate with the total 'in stock' quantity
|
||||
queryset = queryset.annotate(
|
||||
in_stock=Coalesce(
|
||||
SubquerySum('stock_items__quantity', filter=StockItem.IN_STOCK_FILTER),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
)
|
||||
|
||||
# Annotate with the total number of stock items
|
||||
queryset = queryset.annotate(
|
||||
@ -326,11 +317,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
)
|
||||
|
||||
# Annotate with the total variant stock quantity
|
||||
variant_query = StockItem.objects.filter(
|
||||
part__tree_id=OuterRef('tree_id'),
|
||||
part__lft__gt=OuterRef('lft'),
|
||||
part__rght__lt=OuterRef('rght'),
|
||||
).filter(StockItem.IN_STOCK_FILTER)
|
||||
variant_query = part.filters.variant_stock_query()
|
||||
|
||||
queryset = queryset.annotate(
|
||||
variant_stock=Coalesce(
|
||||
@ -357,24 +344,6 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
)
|
||||
)
|
||||
|
||||
# Filter to limit orders to "open"
|
||||
order_filter = Q(
|
||||
order__status__in=PurchaseOrderStatus.OPEN
|
||||
)
|
||||
|
||||
# Annotate with the total 'on order' quantity
|
||||
queryset = queryset.annotate(
|
||||
ordering=Coalesce(
|
||||
SubquerySum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
) - Coalesce(
|
||||
SubquerySum('supplier_parts__purchase_order_line_items__received', filter=order_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
# Annotate with the number of 'suppliers'
|
||||
queryset = queryset.annotate(
|
||||
suppliers=Coalesce(
|
||||
@ -384,40 +353,11 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
),
|
||||
)
|
||||
|
||||
"""
|
||||
Annotate with the number of stock items allocated to sales orders.
|
||||
This annotation is modeled on Part.sales_order_allocations() method:
|
||||
|
||||
- Only look for "open" orders
|
||||
- Stock items have not been "shipped"
|
||||
"""
|
||||
so_allocation_filter = Q(
|
||||
line__order__status__in=SalesOrderStatus.OPEN, # LineItem points to an OPEN order
|
||||
shipment__shipment_date=None, # Allocated item has *not* been shipped out
|
||||
)
|
||||
|
||||
queryset = queryset.annotate(
|
||||
allocated_to_sales_orders=Coalesce(
|
||||
SubquerySum('stock_items__sales_order_allocations__quantity', filter=so_allocation_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
"""
|
||||
Annotate with the number of stock items allocated to build orders.
|
||||
This annotation is modeled on Part.build_order_allocations() method
|
||||
"""
|
||||
bo_allocation_filter = Q(
|
||||
build__status__in=BuildStatus.ACTIVE_CODES,
|
||||
)
|
||||
|
||||
queryset = queryset.annotate(
|
||||
allocated_to_build_orders=Coalesce(
|
||||
SubquerySum('stock_items__allocations__quantity', filter=bo_allocation_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
ordering=part.filters.annotate_on_order_quantity(),
|
||||
in_stock=part.filters.annotate_total_stock(),
|
||||
allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(),
|
||||
allocated_to_build_orders=part.filters.annotate_build_order_allocations(),
|
||||
)
|
||||
|
||||
# Annotate with the total 'available stock' quantity
|
||||
@ -659,40 +599,16 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
Construct an "available stock" quantity:
|
||||
available_stock = total_stock - build_order_allocations - sales_order_allocations
|
||||
"""
|
||||
build_order_filter = Q(build__status__in=BuildStatus.ACTIVE_CODES)
|
||||
sales_order_filter = Q(
|
||||
line__order__status__in=SalesOrderStatus.OPEN,
|
||||
shipment__shipment_date=None,
|
||||
)
|
||||
|
||||
ref = 'sub_part__'
|
||||
|
||||
# Calculate "total stock" for the referenced sub_part
|
||||
# Calculate the "build_order_allocations" for the sub_part
|
||||
# Note that these fields are only aliased, not annotated
|
||||
queryset = queryset.alias(
|
||||
total_stock=Coalesce(
|
||||
SubquerySum(
|
||||
'sub_part__stock_items__quantity',
|
||||
filter=StockItem.IN_STOCK_FILTER
|
||||
),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
allocated_to_sales_orders=Coalesce(
|
||||
SubquerySum(
|
||||
'sub_part__stock_items__sales_order_allocations__quantity',
|
||||
filter=sales_order_filter,
|
||||
),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
allocated_to_build_orders=Coalesce(
|
||||
SubquerySum(
|
||||
'sub_part__stock_items__allocations__quantity',
|
||||
filter=build_order_filter,
|
||||
),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
total_stock=part.filters.annotate_total_stock(reference=ref),
|
||||
allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(reference=ref),
|
||||
allocated_to_build_orders=part.filters.annotate_build_order_allocations(reference=ref),
|
||||
)
|
||||
|
||||
# Calculate 'available_stock' based on previously annotated fields
|
||||
@ -703,32 +619,13 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
)
|
||||
)
|
||||
|
||||
ref = 'substitutes__part__'
|
||||
|
||||
# Extract similar information for any 'substitute' parts
|
||||
queryset = queryset.alias(
|
||||
substitute_stock=Coalesce(
|
||||
SubquerySum(
|
||||
'substitutes__part__stock_items__quantity',
|
||||
filter=StockItem.IN_STOCK_FILTER,
|
||||
),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
substitute_build_allocations=Coalesce(
|
||||
SubquerySum(
|
||||
'substitutes__part__stock_items__allocations__quantity',
|
||||
filter=build_order_filter,
|
||||
),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
substitute_sales_allocations=Coalesce(
|
||||
SubquerySum(
|
||||
'substitutes__part__stock_items__sales_order_allocations__quantity',
|
||||
filter=sales_order_filter,
|
||||
),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
substitute_stock=part.filters.annotate_total_stock(reference=ref),
|
||||
substitute_build_allocations=part.filters.annotate_build_order_allocations(reference=ref),
|
||||
substitute_sales_allocations=part.filters.annotate_sales_order_allocations(reference=ref)
|
||||
)
|
||||
|
||||
# Calculate 'available_substitute_stock' field
|
||||
@ -740,11 +637,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
)
|
||||
|
||||
# Annotate the queryset with 'available variant stock' information
|
||||
variant_stock_query = StockItem.objects.filter(
|
||||
part__tree_id=OuterRef('sub_part__tree_id'),
|
||||
part__lft__gt=OuterRef('sub_part__lft'),
|
||||
part__rght__lt=OuterRef('sub_part__rght'),
|
||||
).filter(StockItem.IN_STOCK_FILTER)
|
||||
variant_stock_query = part.filters.variant_stock_query(reference='sub_part__')
|
||||
|
||||
queryset = queryset.alias(
|
||||
variant_stock_total=Coalesce(
|
||||
|
Reference in New Issue
Block a user