On order filter (#12476)

* Add "on order" filter for Part model

- Added API filter
- Added table filter
- Added table column

* Bump API version

* Add table sorting

* Adjust API text
This commit is contained in:
Oliver
2026-07-27 13:01:56 +10:00
committed by GitHub
parent e43390badb
commit 35f2f1aab6
6 changed files with 62 additions and 9 deletions
@@ -1,11 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 528
INVENTREE_API_VERSION = 529
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v529 -> 2026-07-26 : https://github.com/inventree/InvenTree/pull/12471
- Adds "on_order" filter to the Part API
- Enable sorting by "ordering" quantity on the Part API endpoint
v528 -> 2026-07-26 : https://github.com/inventree/InvenTree/pull/12469
- Additional ordering options for SalesOrderAllocation API endpoint
+10
View File
@@ -784,6 +784,15 @@ class PartFilter(FilterSet):
return queryset.filter(Q(unallocated_stock__gt=0))
return queryset.filter(Q(unallocated_stock__lte=0))
# on_order filter
on_order = rest_filters.BooleanFilter(label='On order', method='filter_on_order')
def filter_on_order(self, queryset, name, value):
"""Filter by whether the Part has any stock on order."""
if str2bool(value):
return queryset.filter(Q(ordering__gt=0))
return queryset.filter(Q(ordering__lte=0))
convert_from = rest_filters.ModelChoiceFilter(
label='Can convert from',
queryset=Part.objects.all(),
@@ -1096,6 +1105,7 @@ class PartList(
'name',
'creation_date',
'IPN',
'ordering',
'in_stock',
'total_in_stock',
'unallocated_stock',
+19
View File
@@ -2782,6 +2782,25 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
# The annotated quantity must also match the part.on_order quantity
self.assertEqual(on_order, p.on_order)
# Test the 'on_order' filter
response = self.get(
reverse('api-part-list'),
{'category': paint.pk, 'on_order': True},
expected_code=200,
)
for item in response.data:
self.assertGreater(item['ordering'], 0)
response = self.get(
reverse('api-part-list'),
{'category': paint.pk, 'on_order': False},
expected_code=200,
)
for item in response.data:
self.assertLessEqual(item['ordering'], 0)
def test_building(self):
"""Test the 'building' quantity annotations."""
# Create a new "buildable" part
@@ -29,7 +29,7 @@ export function renderPartStockCell(record: any): ReactNode {
if (min_stock > stock) {
extra.push(
<Text key='min-stock' c='orange'>
<Text key='min-stock' c='orange' size='sm'>
{`${t`Minimum stock`}: ${formatDecimal(min_stock)}`}
</Text>
);
@@ -39,7 +39,7 @@ export function renderPartStockCell(record: any): ReactNode {
if (max_stock > 0 && stock > max_stock) {
extra.push(
<Text key='max-stock' c='teal'>
<Text key='max-stock' c='teal' size='sm'>
{`${t`Maximum stock`}: ${formatDecimal(max_stock)}`}
</Text>
);
@@ -47,19 +47,25 @@ export function renderPartStockCell(record: any): ReactNode {
if (record.ordering > 0) {
extra.push(
<Text key='on-order'>{`${t`On Order`}: ${formatDecimal(record.ordering)}`}</Text>
<Text
key='on-order'
size='sm'
>{`${t`On Order`}: ${formatDecimal(record.ordering)}`}</Text>
);
}
if (record.building) {
extra.push(
<Text key='building'>{`${t`Building`}: ${formatDecimal(record.building)}`}</Text>
<Text
key='building'
size='sm'
>{`${t`Building`}: ${formatDecimal(record.building)}`}</Text>
);
}
if (record.allocated_to_build_orders > 0) {
extra.push(
<Text key='bo-allocations'>
<Text key='bo-allocations' size='sm'>
{`${t`Build Order Allocations`}: ${formatDecimal(record.allocated_to_build_orders)}`}
</Text>
);
@@ -67,7 +73,7 @@ export function renderPartStockCell(record: any): ReactNode {
if (record.allocated_to_sales_orders > 0) {
extra.push(
<Text key='so-allocations'>
<Text key='so-allocations' size='sm'>
{`${t`Sales Order Allocations`}: ${formatDecimal(record.allocated_to_sales_orders)}`}
</Text>
);
@@ -75,7 +81,7 @@ export function renderPartStockCell(record: any): ReactNode {
if (available != stock) {
extra.push(
<Text key='available'>
<Text key='available' size='sm'>
{t`Available`}: {formatDecimal(available)}
</Text>
);
@@ -83,7 +89,7 @@ export function renderPartStockCell(record: any): ReactNode {
if (record.external_stock > 0) {
extra.push(
<Text key='external'>
<Text key='external' size='sm'>
{t`External stock`}: {formatDecimal(record.external_stock)}
</Text>
);
@@ -23,6 +23,7 @@ import { ActionDropdown } from '../../components/items/ActionDropdown';
import {
BooleanColumn,
CategoryColumn,
DecimalColumn,
DefaultLocationColumn,
DescriptionColumn,
IPNColumn,
@@ -85,6 +86,13 @@ function partTableColumns(): TableColumn[] {
filter: ['has_stock', 'low_stock', 'high_stock'],
render: renderPartStockCell
},
DecimalColumn({
accessor: 'ordering',
title: t`On Order`,
filter: 'on_order',
sortable: true,
defaultVisible: false
}),
{
accessor: 'price_range',
title: t`Price Range`,
@@ -144,6 +144,12 @@ export function PartTableFilters(): TableFilter[] {
description: t`Filter by parts which have available stock`,
type: 'boolean'
},
{
name: 'on_order',
label: t`On Order`,
description: t`Filter by parts which are on order`,
type: 'boolean'
},
{
name: 'starred',
label: t`Subscribed`,