From c54f3f4a3016464f44e473717dd7d063197d43f2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 26 Oct 2025 16:45:27 +1100 Subject: [PATCH] [UI] Shipments table (#10675) * Display PendingShipments panel - Overview of all outstanding shipments * Update UI tests * Bump API version --- .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/order/serializers.py | 8 ++ src/frontend/src/forms/SalesOrderForms.tsx | 2 +- src/frontend/src/pages/sales/SalesIndex.tsx | 14 ++++ .../tables/sales/SalesOrderShipmentTable.tsx | 77 +++++++++++++++---- .../tests/pages/pui_sales_order.spec.ts | 9 +++ 6 files changed, 98 insertions(+), 17 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index a18e85c2c1..1468edc624 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 = 419 +INVENTREE_API_VERSION = 420 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v420 -> 2025-10-26 : https://github.com/inventree/InvenTree/pull/10675 + - Adds optional "customer_detail" filter to SalesOrderShipment API endpoint + v419 -> 2025-10-24 : https://github.com/inventree/InvenTree/pull/10659 - Fixes regression introduced in v417 which reverted the changes from v416 diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index ee35d51127..bc993581c1 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -1320,6 +1320,7 @@ class SalesOrderShipmentSerializer( 'notes', # Extra detail fields 'checked_by_detail', + 'customer_detail', 'order_detail', 'shipment_address_detail', ] @@ -1352,6 +1353,13 @@ class SalesOrderShipmentSerializer( True, ) + customer_detail = enable_filter( + CompanyBriefSerializer( + source='order.customer', many=False, read_only=True, allow_null=True + ), + False, + ) + shipment_address_detail = enable_filter( AddressBriefSerializer( source='shipment_address', many=False, read_only=True, allow_null=True diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index 7ca5f0d840..f696a731d3 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -378,7 +378,7 @@ export function useSalesOrderShipmentFields({ customerId, pending }: { - customerId: number; + customerId?: number; pending?: boolean; }): ApiFormFieldSet { return useMemo(() => { diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx index 154e98593d..ffa1e1d5d3 100644 --- a/src/frontend/src/pages/sales/SalesIndex.tsx +++ b/src/frontend/src/pages/sales/SalesIndex.tsx @@ -3,6 +3,7 @@ import { Stack } from '@mantine/core'; import { IconBuildingStore, IconCalendar, + IconCubeSend, IconTable, IconTruckDelivery, IconTruckReturn @@ -20,6 +21,7 @@ import { PanelGroup } from '../../components/panels/PanelGroup'; import { useUserState } from '../../states/UserState'; import { CompanyTable } from '../../tables/company/CompanyTable'; import { ReturnOrderTable } from '../../tables/sales/ReturnOrderTable'; +import SalesOrderShipmentTable from '../../tables/sales/SalesOrderShipmentTable'; import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; function SalesOrderOverview({ @@ -98,6 +100,18 @@ export default function SalesIndex() { ), hidden: !user.hasViewRole(UserRoles.sales_order) }, + { + name: 'shipments', + label: t`Pending Shipments`, + icon: , + content: ( + + ) + }, { name: 'returnorders', label: t`Return Orders`, diff --git a/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx b/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx index 4c6af541ea..7834c79e4c 100644 --- a/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx @@ -29,19 +29,30 @@ import { } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { useUserState } from '../../states/UserState'; -import { DateColumn, LinkColumn } from '../ColumnRenderers'; +import { + CompanyColumn, + DateColumn, + LinkColumn, + StatusColumn +} from '../ColumnRenderers'; import { InvenTreeTable } from '../InvenTreeTable'; export default function SalesOrderShipmentTable({ + showOrderInfo = false, + tableName, customerId, - orderId + orderId, + filters }: Readonly<{ - customerId: number; - orderId: number; + showOrderInfo?: boolean; + tableName?: string; + customerId?: number; + orderId?: number; + filters?: any; }>) { const user = useUserState(); const navigate = useNavigate(); - const table = useTable('sales-order-shipment'); + const table = useTable(tableName ?? 'sales-order-shipment'); const [selectedShipment, setSelectedShipment] = useState({}); @@ -95,6 +106,30 @@ export default function SalesOrderShipmentTable({ const tableColumns: TableColumn[] = useMemo(() => { return [ + { + accessor: 'customer', + title: t`Customer`, + switchable: true, + sortable: true, + hidden: !showOrderInfo, + render: (record: any) => ( + + ) + }, + { + switchable: false, + accessor: 'order_detail.reference', + title: t`Sales Order`, + hidden: !showOrderInfo, + sortable: false + }, + StatusColumn({ + switchable: true, + model: ModelType.salesorder, + accessor: 'order_detail.status', + title: t`Order Status`, + hidden: !showOrderInfo + }), { accessor: 'reference', title: t`Shipment Reference`, @@ -146,19 +181,13 @@ export default function SalesOrderShipmentTable({ accessor: 'link' }) ]; - }, []); + }, [showOrderInfo]); const rowActions = useCallback( (record: any): RowAction[] => { const shipped: boolean = !!record.shipment_date; return [ - RowViewAction({ - title: t`View Shipment`, - modelType: ModelType.salesordershipment, - modelId: record.pk, - navigate: navigate - }), { hidden: shipped || !user.hasChangeRole(UserRoles.sales_order), title: t`Complete Shipment`, @@ -184,13 +213,28 @@ export default function SalesOrderShipmentTable({ setSelectedShipment(record); deleteShipment.open(); } + }), + RowViewAction({ + title: t`View Sales Order`, + modelType: ModelType.salesorder, + modelId: record.order, + hidden: + !record.order || + !showOrderInfo || + !user.hasViewRole(UserRoles.sales_order), + navigate: navigate }) ]; }, - [user] + [showOrderInfo, user] ); const tableActions = useMemo(() => { + // No actions possible if no order is specified + if (!orderId) { + return []; + } + return [ ]; - }, [user]); + }, [orderId, user]); const tableFilters: TableFilter[] = useMemo(() => { return [ @@ -241,7 +285,10 @@ export default function SalesOrderShipmentTable({ enableReports: true, rowActions: rowActions, params: { - order: orderId + order: orderId, + order_detail: true, + customer_detail: showOrderInfo, + ...filters } }} /> diff --git a/src/frontend/tests/pages/pui_sales_order.spec.ts b/src/frontend/tests/pages/pui_sales_order.spec.ts index 2a9546f958..5efea50a48 100644 --- a/src/frontend/tests/pages/pui_sales_order.spec.ts +++ b/src/frontend/tests/pages/pui_sales_order.spec.ts @@ -14,9 +14,18 @@ test('Sales Orders - Tabs', async ({ browser }) => { await page.waitForURL('**/web/sales/**'); + // Sales Orders panel await loadTab(page, 'Sales Orders'); await page.waitForURL('**/web/sales/index/salesorders'); + + // Pending Shipments panel + await loadTab(page, 'Pending Shipments'); + await page.getByRole('cell', { name: 'SO0007' }).waitFor(); + await page.getByRole('button', { name: 'Shipment Reference' }).waitFor(); + + // Return Orders panel await loadTab(page, 'Return Orders'); + await page.getByRole('cell', { name: 'NOISE-COMPLAINT' }).waitFor(); // Customers await loadTab(page, 'Customers');