From c2c8e72f37ac5c5b9885b7cc25883acb443051a2 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Mishra <154746713+adityakrmishra@users.noreply.github.com> Date: Mon, 8 Jun 2026 03:19:32 +0530 Subject: [PATCH 1/4] UX: Disable standard allocation for trackable parts (#11811) (#11967) * UX: Disable standard allocation for trackable parts and add tooltip (#11811) * Test: Update Playwright tests for disabled trackable allocation * Fix: Align trackable allocation logic and tests based on review * Test: Fix incorrect assumption of trackable Red Widget in pui_build.spec.ts --------- Co-authored-by: Matthias Mair --- .../src/tables/build/BuildLineTable.tsx | 31 +++++++++++-------- src/frontend/tests/pages/pui_build.spec.ts | 22 +++++++++++-- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index ab9493ebd5..26ba76ba23 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -1,3 +1,14 @@ +import { ActionButton } from '@lib/components/ActionButton'; +import { ProgressBar } from '@lib/components/ProgressBar'; +import { RowEditAction, RowViewAction } from '@lib/components/RowActions'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; +import { UserRoles } from '@lib/enums/Roles'; +import { apiUrl } from '@lib/functions/Api'; +import { formatDecimal } from '@lib/functions/Formatting'; +import useTable from '@lib/hooks/UseTable'; +import type { TableFilter } from '@lib/types/Filters'; +import type { RowAction, TableColumn } from '@lib/types/Tables'; import { t } from '@lingui/core/macro'; import { Alert, Group, Paper, Text } from '@mantine/core'; import { @@ -13,18 +24,6 @@ import { import type { DataTableRowExpansionProps } from 'mantine-datatable'; import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; - -import { ActionButton } from '@lib/components/ActionButton'; -import { ProgressBar } from '@lib/components/ProgressBar'; -import { RowEditAction, RowViewAction } from '@lib/components/RowActions'; -import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; -import { ModelType } from '@lib/enums/ModelType'; -import { UserRoles } from '@lib/enums/Roles'; -import { apiUrl } from '@lib/functions/Api'; -import { formatDecimal } from '@lib/functions/Formatting'; -import useTable from '@lib/hooks/UseTable'; -import type { TableFilter } from '@lib/types/Filters'; -import type { RowAction, TableColumn } from '@lib/types/Tables'; import OrderPartsWizard from '../../components/wizards/OrderPartsWizard'; import { useAllocateStockToBuildForm, @@ -769,7 +768,9 @@ export default function BuildLineTable({ !consumable && user.hasChangeRole(UserRoles.build) && required > 0 && - record.trackable == hasOutput; + (hasOutput ? trackable : true); + + const disableAllocation = !hasOutput && trackable; // Can de-allocate const canDeallocate = @@ -792,6 +793,10 @@ export default function BuildLineTable({ icon: , title: t`Allocate Stock`, hidden: !canAllocate, + disabled: disableAllocation, + tooltip: disableAllocation + ? t`Trackable parts must be allocated via the Build Outputs tab` + : t`Allocate Stock`, color: 'green', onClick: () => { setSelectedRows([record]); diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index 8f27eb8322..785ecb5465 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -418,6 +418,18 @@ test('Build Order - Allocation', async ({ browser }) => { await row.getByText(/150 \/ 150/).waitFor(); + // Open the allocation menu for the red widget + const mainRedWidget = await page.getByRole('cell', { name: 'Red Widget' }); + const mainRedRow = await getRowFromCell(mainRedWidget); + + await mainRedRow.getByLabel(/row-action-menu-/i).click(); + + await page + .getByRole('menuitem', { name: 'Allocate Stock', exact: true }) + .waitFor(); + + await page.keyboard.press('Escape'); + // Expand this row await cell.click(); await page.getByRole('cell', { name: '2022-4-27', exact: true }).waitFor(); @@ -494,9 +506,13 @@ test('Build Order - Allocation', async ({ browser }) => { const redRow = await getRowFromCell(redWidget); await redRow.getByLabel(/row-action-menu-/i).click(); - await page - .getByRole('menuitem', { name: 'Allocate Stock', exact: true }) - .waitFor(); + + const allocateStockBtn = page.getByRole('menuitem', { + name: 'Allocate Stock', + exact: true + }); + await expect(allocateStockBtn).toBeEnabled(); + await page .getByRole('menuitem', { name: 'Deallocate Stock', exact: true }) .waitFor(); From a63ca93c3cf891bc05dfbb19a0a047bdeae43be1 Mon Sep 17 00:00:00 2001 From: NerosKi <138918774+NerosKi@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:06:24 +0200 Subject: [PATCH 2/4] plugin-validation-purchase-orders (#12094) Co-authored-by: Matthias Mair --- src/backend/InvenTree/order/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 1109e65a02..428f9f1d29 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -1191,6 +1191,10 @@ class PurchaseOrder(TotalPriceMixin, Order): for item in new_items: item.set_status(status, custom_values=custom_stock_status_values) + # run validation for serialized items plugin.validate_batch_code + item.validate_batch_code() + # run validation for serialized items plugin.validate_model_instance + item.run_plugin_validation() stock_items.append(item) else: @@ -1213,6 +1217,13 @@ class PurchaseOrder(TotalPriceMixin, Order): # Bulk create new stock items if len(bulk_create_items) > 0: + # bulk_create() bypasses save()/clean() methods, so manual validation is required for each item + for item in bulk_create_items: + # run validation for items plugin.validate_batch_code + item.validate_batch_code() + # run validation for items plugin.validate_model_instance + item.run_plugin_validation() + stock.models.StockItem.objects.bulk_create( bulk_create_items, batch_size=250 ) From e08280d9ac7230c20442b5ca4da49753dc0fdc2d Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 8 Jun 2026 13:50:04 +1000 Subject: [PATCH 3/4] Remove built-in dashboard history widgets (#12114) --- .../dashboard/DashboardWidgetLibrary.tsx | 22 --- .../dashboard/widgets/OrderHistoryWidget.tsx | 135 ------------------ 2 files changed, 157 deletions(-) delete mode 100644 src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx diff --git a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx index 76ada27f8d..57d99f9636 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx @@ -8,7 +8,6 @@ import ColorToggleDashboardWidget from './widgets/ColorToggleWidget'; import GetStartedWidget from './widgets/GetStartedWidget'; import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget'; import NewsWidget from './widgets/NewsWidget'; -import OrderHistoryWidget from './widgets/OrderHistoryWidget'; import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; import QueryDashboardWidget from './widgets/QueryDashboardWidget'; import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget'; @@ -218,26 +217,6 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { }); } -function BuiltinHistoryWidgets(): DashboardWidgetProps[] { - return [ - OrderHistoryWidget({ - modelType: ModelType.build - }), - OrderHistoryWidget({ - modelType: ModelType.salesorder - }), - OrderHistoryWidget({ - modelType: ModelType.purchaseorder - }), - OrderHistoryWidget({ - modelType: ModelType.returnorder - }), - OrderHistoryWidget({ - modelType: ModelType.transferorder - }) - ]; -} - function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { return [ { @@ -276,7 +255,6 @@ function BuiltinActionWidgets(): DashboardWidgetProps[] { export default function DashboardWidgetLibrary(): DashboardWidgetProps[] { return [ ...BuiltinQueryCountWidgets(), - ...BuiltinHistoryWidgets(), ...BuiltinGettingStartedWidgets(), ...BuiltinSettingsWidgets(), ...BuiltinActionWidgets() diff --git a/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx b/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx deleted file mode 100644 index 88569a9cee..0000000000 --- a/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { ModelInformationDict } from '@lib/enums/ModelInformation'; -import type { ModelType } from '@lib/enums/ModelType'; -import { apiUrl } from '@lib/functions/Api'; -import { StylishText } from '@lib/index'; -import { t } from '@lingui/core/macro'; -import { BarChart } from '@mantine/charts'; -import { Box, LoadingOverlay, Stack } from '@mantine/core'; -import { useDocumentVisibility } from '@mantine/hooks'; -import { useQuery } from '@tanstack/react-query'; -import dayjs from 'dayjs'; -import { useMemo } from 'react'; -import { useApi } from '../../../contexts/ApiContext'; -import { useUserState } from '../../../states/UserState'; -import type { DashboardWidgetProps } from '../DashboardWidget'; - -function OrderHistoryComponent({ - modelType, - title -}: { - modelType: ModelType; - title: string; -}) { - const modelInfo = useMemo(() => { - return ModelInformationDict[modelType]; - }, [modelType]); - - const url = useMemo(() => { - return apiUrl(modelInfo.api_endpoint); - }, [modelInfo]); - - const api = useApi(); - const visibility = useDocumentVisibility(); - - const endDate = dayjs().add(1, 'day').format('YYYY-MM-DD'); - const startDate = dayjs() - .subtract(12, 'month') - .subtract(1, 'day') - .format('YYYY-MM-DD'); - - const params = { - completed_after: startDate, - completed_before: endDate - }; - - const query = useQuery({ - queryKey: ['dashboard-order-summary', modelType, params, visibility], - enabled: visibility === 'visible', - refetchOnWindowFocus: true, - refetchOnMount: true, - refetchInterval: 10 * 60 * 1000, // 10 minute refetch interval - staleTime: 5 * 60 * 1000, // 5 minute stale time - queryFn: () => { - if (visibility !== 'visible') { - return []; - } - - return api.get(url, { params }).then((res) => { - return res.data ?? []; - }); - } - }); - - const chartData = useMemo(() => { - const months = Array.from({ length: 13 }, (_, i) => ({ - month: dayjs() - .subtract(12 - i, 'month') - .format('MMM YY'), - count: 0 - })); - - for (const order of query.data || []) { - // Build orders use `completion_date`; all other order types use `complete_date` - const dateStr = - order.complete_date ?? order.completion_date ?? order.shipment_date; - if (!dateStr) continue; - const label = dayjs(dateStr).format('MMM YY'); - const entry = months.find((m) => m.month === label); - if (entry) entry.count++; - } - - return months; - }, [query.data]); - - return ( - - {title} - - - - - - ); -} - -/** - * Display a simple chart of the number of completed orders per month, for the last 12 months. - */ -export default function OrderHistoryWidget({ - modelType -}: { - modelType: ModelType; -}): DashboardWidgetProps { - const user = useUserState(); - - const modelInfo = useMemo(() => { - return ModelInformationDict[modelType]; - }, [modelType]); - - // Extract translated model labels - const models = modelInfo.label_multiple(); - - return { - label: `${modelType}-history`, - title: t`Completed ${models}`, - description: t`Display number of completed ${models} per month`, - minHeight: 2, - minWidth: 3, - modelType: modelType, - icon: 'chart_bar', - visible: () => user.hasViewPermission(modelType), - render: () => ( - - ) - }; -} From 345fe1d67ecfa85c5c1bd8dfba4e97aff5fea38b Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 8 Jun 2026 13:51:21 +1000 Subject: [PATCH 4/4] Calendar tweaks (#12115) * Support initial filters for UseCalendar * [UI] Calendar updates - Allow calendars to display completed / cancelled items also --- src/frontend/src/components/calendar/OrderCalendar.tsx | 5 ++++- src/frontend/src/hooks/UseCalendar.tsx | 8 +++++--- src/frontend/src/pages/build/BuildIndex.tsx | 3 ++- src/frontend/src/pages/purchasing/PurchasingIndex.tsx | 3 ++- src/frontend/src/pages/sales/SalesIndex.tsx | 6 ++++-- src/frontend/src/pages/stock/LocationDetail.tsx | 3 ++- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/frontend/src/components/calendar/OrderCalendar.tsx b/src/frontend/src/components/calendar/OrderCalendar.tsx index 36923e48e2..0f468a7041 100644 --- a/src/frontend/src/components/calendar/OrderCalendar.tsx +++ b/src/frontend/src/components/calendar/OrderCalendar.tsx @@ -47,12 +47,14 @@ export default function OrderCalendar({ role, params, filters, + initialFilters, tooltip }: { model: ModelType; role: UserRoles; params: Record; filters?: TableFilter[]; + initialFilters?: TableFilter[]; tooltip?: (event: EventContentArg) => React.ReactNode; }) { const navigate = useNavigate(); @@ -94,7 +96,8 @@ export default function OrderCalendar({ const calendarState = useCalendar({ endpoint: modelInfo.api_endpoint, name: model.toString(), - queryParams: params + queryParams: params, + initialFilters: initialFilters }); // Build the events diff --git a/src/frontend/src/hooks/UseCalendar.tsx b/src/frontend/src/hooks/UseCalendar.tsx index 2f7ec5b226..ec1d32c7ab 100644 --- a/src/frontend/src/hooks/UseCalendar.tsx +++ b/src/frontend/src/hooks/UseCalendar.tsx @@ -2,7 +2,7 @@ import type FullCalendar from '@fullcalendar/react'; import type { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; import useFilterSet from '@lib/hooks/UseFilterSet'; -import type { FilterSetState } from '@lib/types/Filters'; +import type { FilterSetState, TableFilter } from '@lib/types/Filters'; import type { UseModalReturn } from '@lib/types/Modals'; import type { DateValue } from '@mantine/dates'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; @@ -54,15 +54,17 @@ export type CalendarState = { export default function useCalendar({ name, endpoint, - queryParams + queryParams, + initialFilters }: { name: string; endpoint: ApiEndpoints; queryParams?: any; + initialFilters?: TableFilter[]; }): CalendarState { const ref = useRef(null as any); - const filterSet = useFilterSet(`calendar-${name}`); + const filterSet = useFilterSet(`calendar-${name}`, initialFilters); const [searchTerm, setSearchTerm] = useState(''); diff --git a/src/frontend/src/pages/build/BuildIndex.tsx b/src/frontend/src/pages/build/BuildIndex.tsx index f52441923d..43456c6138 100644 --- a/src/frontend/src/pages/build/BuildIndex.tsx +++ b/src/frontend/src/pages/build/BuildIndex.tsx @@ -48,8 +48,9 @@ function BuildOrderCalendar() { ); diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx index bdaa271c0e..8e8887144e 100644 --- a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx +++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx @@ -51,8 +51,9 @@ function PurchaseOrderCalendar() { ); diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx index 0f1c0879b0..447229a747 100644 --- a/src/frontend/src/pages/sales/SalesIndex.tsx +++ b/src/frontend/src/pages/sales/SalesIndex.tsx @@ -49,8 +49,9 @@ function SalesOrderCalendar() { ); @@ -73,8 +74,9 @@ const ReturnOrderCalendar = () => { ); diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index eef0eb6e0b..5fbab60d46 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -70,8 +70,9 @@ function TransferOrderCalendar() { ); }