Merge branch 'master' into block-notes
This commit is contained in:
commit
fb621c3bb8
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,12 +47,14 @@ export default function OrderCalendar({
|
|||
role,
|
||||
params,
|
||||
filters,
|
||||
initialFilters,
|
||||
tooltip
|
||||
}: {
|
||||
model: ModelType;
|
||||
role: UserRoles;
|
||||
params: Record<string, any>;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Stack gap='xs'>
|
||||
<StylishText size='md'>{title}</StylishText>
|
||||
<Box>
|
||||
<LoadingOverlay visible={query.isLoading || query.isFetching} />
|
||||
<BarChart
|
||||
h={200}
|
||||
data={chartData}
|
||||
dataKey='month'
|
||||
series={[{ name: 'count', label: t`Completed`, color: 'blue.6' }]}
|
||||
withYAxis={false}
|
||||
yAxisProps={{ domain: [0, 'auto'] }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: () => (
|
||||
<OrderHistoryComponent
|
||||
modelType={modelType}
|
||||
title={t`Completed ${models}`}
|
||||
/>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
|
@ -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<FullCalendar>(null as any);
|
||||
|
||||
const filterSet = useFilterSet(`calendar-${name}`);
|
||||
const filterSet = useFilterSet(`calendar-${name}`, initialFilters);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
|
||||
|
|
|
|||
|
|
@ -48,8 +48,9 @@ function BuildOrderCalendar() {
|
|||
<OrderCalendar
|
||||
model={ModelType.build}
|
||||
role={UserRoles.build}
|
||||
params={{ outstanding: true, part_detail: true }}
|
||||
params={{ part_detail: true }}
|
||||
filters={calendarFilters}
|
||||
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||
tooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ function PurchaseOrderCalendar() {
|
|||
<OrderCalendar
|
||||
model={ModelType.purchaseorder}
|
||||
role={UserRoles.purchase_order}
|
||||
params={{ outstanding: true, supplier_detail: true }}
|
||||
params={{ supplier_detail: true }}
|
||||
filters={calendarFilters}
|
||||
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||
tooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -49,8 +49,9 @@ function SalesOrderCalendar() {
|
|||
<OrderCalendar
|
||||
model={ModelType.salesorder}
|
||||
role={UserRoles.sales_order}
|
||||
params={{ outstanding: true, customer_detail: true }}
|
||||
params={{ customer_detail: true }}
|
||||
filters={calendarFilters}
|
||||
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||
tooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
|
|
@ -73,8 +74,9 @@ const ReturnOrderCalendar = () => {
|
|||
<OrderCalendar
|
||||
model={ModelType.returnorder}
|
||||
role={UserRoles.return_order}
|
||||
params={{ outstanding: true, customer_detail: true }}
|
||||
params={{ customer_detail: true }}
|
||||
filters={calendarFilters}
|
||||
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||
tooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -70,8 +70,9 @@ function TransferOrderCalendar() {
|
|||
<OrderCalendar
|
||||
model={ModelType.transferorder}
|
||||
role={UserRoles.transfer_order}
|
||||
params={{ outstanding: true }}
|
||||
params={{}}
|
||||
filters={calendarFilters}
|
||||
initialFilters={[{ name: 'outstanding', value: 'true' }]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: <IconArrowRight />,
|
||||
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]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue