diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 09940c1bcc..d1759eb85b 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -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 diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index 9ceb18a424..a773440c26 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -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', diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index ccfc6f4367..46ebaf0bfd 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -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 diff --git a/src/frontend/src/components/tables/PartStockCell.tsx b/src/frontend/src/components/tables/PartStockCell.tsx index d5e8dc6246..1cab41376d 100644 --- a/src/frontend/src/components/tables/PartStockCell.tsx +++ b/src/frontend/src/components/tables/PartStockCell.tsx @@ -29,7 +29,7 @@ export function renderPartStockCell(record: any): ReactNode { if (min_stock > stock) { extra.push( - + {`${t`Minimum stock`}: ${formatDecimal(min_stock)}`} ); @@ -39,7 +39,7 @@ export function renderPartStockCell(record: any): ReactNode { if (max_stock > 0 && stock > max_stock) { extra.push( - + {`${t`Maximum stock`}: ${formatDecimal(max_stock)}`} ); @@ -47,19 +47,25 @@ export function renderPartStockCell(record: any): ReactNode { if (record.ordering > 0) { extra.push( - {`${t`On Order`}: ${formatDecimal(record.ordering)}`} + {`${t`On Order`}: ${formatDecimal(record.ordering)}`} ); } if (record.building) { extra.push( - {`${t`Building`}: ${formatDecimal(record.building)}`} + {`${t`Building`}: ${formatDecimal(record.building)}`} ); } if (record.allocated_to_build_orders > 0) { extra.push( - + {`${t`Build Order Allocations`}: ${formatDecimal(record.allocated_to_build_orders)}`} ); @@ -67,7 +73,7 @@ export function renderPartStockCell(record: any): ReactNode { if (record.allocated_to_sales_orders > 0) { extra.push( - + {`${t`Sales Order Allocations`}: ${formatDecimal(record.allocated_to_sales_orders)}`} ); @@ -75,7 +81,7 @@ export function renderPartStockCell(record: any): ReactNode { if (available != stock) { extra.push( - + {t`Available`}: {formatDecimal(available)} ); @@ -83,7 +89,7 @@ export function renderPartStockCell(record: any): ReactNode { if (record.external_stock > 0) { extra.push( - + {t`External stock`}: {formatDecimal(record.external_stock)} ); diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx index 5cf56f21e3..639c0f7b5c 100644 --- a/src/frontend/src/tables/part/PartTable.tsx +++ b/src/frontend/src/tables/part/PartTable.tsx @@ -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`, diff --git a/src/frontend/src/tables/part/PartTableFilters.tsx b/src/frontend/src/tables/part/PartTableFilters.tsx index ff3e2b25dd..baa0c2f68a 100644 --- a/src/frontend/src/tables/part/PartTableFilters.tsx +++ b/src/frontend/src/tables/part/PartTableFilters.tsx @@ -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`,