diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index 1f6786d296..09940c1bcc 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 527
+INVENTREE_API_VERSION = 528
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
+v528 -> 2026-07-26 : https://github.com/inventree/InvenTree/pull/12469
+ - Additional ordering options for SalesOrderAllocation API endpoint
+
v527 -> 2026-07-24 : https://github.com/inventree/InvenTree/pull/12454
- Extend API with dedicated login-by-code re-send endpoint
diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py
index f8874c1cea..057838c8ed 100644
--- a/src/backend/InvenTree/order/api.py
+++ b/src/backend/InvenTree/order/api.py
@@ -1087,6 +1087,8 @@ class SalesOrderLineItemList(
'sale_price',
'target_date',
'line',
+ 'status',
+ 'shipment_date',
]
ordering_field_aliases = {
@@ -1095,6 +1097,8 @@ class SalesOrderLineItemList(
'IPN': 'part__IPN',
'order': 'order__reference',
'line': ['line_int', 'line', 'part__name'],
+ 'status': 'order__status',
+ 'shipment_date': 'order__shipment_date',
}
search_fields = ['part__name', 'quantity', 'reference']
diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py
index 1203684ae1..28d42ea984 100644
--- a/src/backend/InvenTree/order/test_api.py
+++ b/src/backend/InvenTree/order/test_api.py
@@ -2307,6 +2307,60 @@ class SalesOrderLineItemTest(OrderTest):
self.filter({'allocated': 'true'}, 1)
self.filter({'allocated': 'false'}, n - 1)
+ def test_so_line_ordering(self):
+ """Test that the SalesOrderLineItem list can be ordered by order-related fields.
+
+ Regression test for aliased ordering fields ('status', 'shipment_date')
+ which are not present directly on the SalesOrderLineItem model,
+ but rather on the linked SalesOrder.
+ """
+ # Orders 1-3 are 'pending' (status=10), order 4 is 'shipped' (status=20),
+ # order 5 is 'returned' (status=60), order 6 is 'complete' (status=30)
+ # (refer to the 'sales_order' fixture data)
+
+ # Order by 'status' (aliased to 'order__status')
+ response = self.get(
+ self.url, {'ordering': 'status', 'order_detail': True}, expected_code=200
+ )
+
+ statuses = [item['order_detail']['status'] for item in response.data]
+ self.assertEqual(statuses, sorted(statuses))
+ self.assertEqual(statuses[0], SalesOrderStatus.PENDING.value)
+ self.assertEqual(statuses[-1], SalesOrderStatus.RETURNED.value)
+
+ # Reverse the ordering
+ response = self.get(
+ self.url, {'ordering': '-status', 'order_detail': True}, expected_code=200
+ )
+
+ statuses = [item['order_detail']['status'] for item in response.data]
+ self.assertEqual(statuses, sorted(statuses, reverse=True))
+ self.assertEqual(statuses[0], SalesOrderStatus.RETURNED.value)
+
+ # Order by 'shipment_date' (aliased to 'order__shipment_date')
+ order_a = models.SalesOrder.objects.get(pk=4)
+ order_b = models.SalesOrder.objects.get(pk=5)
+
+ order_a.shipment_date = date(2020, 1, 1)
+ order_a.save()
+
+ order_b.shipment_date = date(2024, 1, 1)
+ order_b.save()
+
+ response = self.get(self.url, {'ordering': 'shipment_date'}, expected_code=200)
+
+ order_ids = [item['order'] for item in response.data]
+
+ # Lines for 'order_a' (earlier shipment date) should sort before 'order_b'
+ self.assertLess(order_ids.index(order_a.pk), order_ids.index(order_b.pk))
+
+ # Reverse the ordering - 'order_b' should now come first
+ response = self.get(self.url, {'ordering': '-shipment_date'}, expected_code=200)
+
+ order_ids = [item['order'] for item in response.data]
+
+ self.assertLess(order_ids.index(order_b.pk), order_ids.index(order_a.pk))
+
def test_so_line_bulk_delete(self):
"""Test that we can bulk delete multiple SalesOrderLineItems via the API."""
n = models.SalesOrderLineItem.objects.count()
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 6fe4ea4de7..2bfcf93db9 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -89,12 +89,12 @@ import { UsedInTable } from '../../tables/bom/UsedInTable';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import { ParameterTable } from '../../tables/general/ParameterTable';
import PartPurchaseOrdersTable from '../../tables/part/PartPurchaseOrdersTable';
+import PartSalesOrdersTable from '../../tables/part/PartSalesOrdersTable';
import PartTestResultTable from '../../tables/part/PartTestResultTable';
import PartTestTemplateTable from '../../tables/part/PartTestTemplateTable';
import { PartVariantTable } from '../../tables/part/PartVariantTable';
import { RelatedPartTable } from '../../tables/part/RelatedPartTable';
import { ReturnOrderTable } from '../../tables/sales/ReturnOrderTable';
-import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
import { TransferOrderTable } from '../../tables/stock/TransferOrderTable';
import PartAllocationPanel from './PartAllocationPanel';
@@ -398,7 +398,11 @@ export default function PartDetail() {
label: t`Sales Orders`,
icon: ,
hidden: !part.salable || !user.hasViewRole(UserRoles.sales_order),
- content: part.pk ? :
+ content: part.pk ? (
+
+ ) : (
+
+ )
},
{
name: 'return_orders',
diff --git a/src/frontend/src/tables/part/PartSalesOrdersTable.tsx b/src/frontend/src/tables/part/PartSalesOrdersTable.tsx
new file mode 100644
index 0000000000..d8e8661e71
--- /dev/null
+++ b/src/frontend/src/tables/part/PartSalesOrdersTable.tsx
@@ -0,0 +1,129 @@
+import { t } from '@lingui/core/macro';
+import { useMemo } from 'react';
+
+import { ProgressBar } from '@lib/components/ProgressBar';
+import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
+import { ModelType } from '@lib/enums/ModelType';
+import { apiUrl } from '@lib/functions/Api';
+import useTable from '@lib/hooks/UseTable';
+import type { TableFilter } from '@lib/types/Filters';
+import type { TableColumn } from '@lib/types/Tables';
+import {
+ CompanyColumn,
+ DateColumn,
+ ReferenceColumn,
+ StatusColumn
+} from '../../components/tables/ColumnRenderers';
+import {
+ IncludeVariantsFilter,
+ StatusFilterOptions
+} from '../../components/tables/Filter';
+import { InvenTreeTable } from '../../components/tables/InvenTreeTable';
+import { formatCurrency } from '../../defaults/formatters';
+
+export default function PartSalesOrdersTable({
+ partId
+}: Readonly<{
+ partId: number;
+}>) {
+ const table = useTable('partsalesorders');
+
+ const tableColumns: TableColumn[] = useMemo(() => {
+ return [
+ ReferenceColumn({
+ accessor: 'order_detail.reference',
+ ordering: 'order',
+ sortable: true,
+ switchable: false,
+ filter: ['order_outstanding', 'completed'],
+ title: t`Sales Order`
+ }),
+ StatusColumn({
+ accessor: 'order_detail.status',
+ sortable: true,
+ ordering: 'status',
+ title: t`Status`,
+ filter: 'order_status',
+ model: ModelType.salesorder
+ }),
+ {
+ accessor: 'customer_detail.name',
+ title: t`Customer`,
+ sortable: false,
+ switchable: true,
+ render: (record: any) => (
+
+ )
+ },
+ {
+ accessor: 'quantity',
+ sortable: true,
+ switchable: false,
+ render: (record: any) => (
+
+ )
+ },
+ DateColumn({
+ accessor: 'target_date',
+ title: t`Target Date`
+ }),
+ DateColumn({
+ accessor: 'order_detail.shipment_date',
+ ordering: 'shipment_date',
+ title: t`Shipment Date`
+ }),
+ {
+ accessor: 'sale_price',
+ render: (record: any) =>
+ formatCurrency(record.sale_price, {
+ currency: record.sale_price_currency
+ })
+ }
+ ];
+ }, []);
+
+ const tableFilters: TableFilter[] = useMemo(() => {
+ return [
+ {
+ name: 'order_outstanding',
+ label: t`Outstanding`,
+ description: t`Show outstanding orders`
+ },
+ {
+ name: 'completed',
+ label: t`Completed`,
+ description: t`Show completed line items`
+ },
+ {
+ name: 'order_status',
+ label: t`Order Status`,
+ description: t`Filter by order status`,
+ choiceFunction: StatusFilterOptions(ModelType.salesorder)
+ },
+ IncludeVariantsFilter()
+ ];
+ }, []);
+
+ return (
+
+ );
+}