mirror of
https://github.com/inventree/InvenTree.git
synced 2025-08-07 20:32:12 +00:00
[build order] Auto select SN (#10110)
* Add build output information * Auto-allocate tracked items * Add playwright testing for workflow * Fix display issue when creating new build output
This commit is contained in:
@@ -57,6 +57,7 @@ export type ApiFormFieldHeader = {
|
|||||||
* @param preFieldContent : Content to render before the field
|
* @param preFieldContent : Content to render before the field
|
||||||
* @param postFieldContent : Content to render after the field
|
* @param postFieldContent : Content to render after the field
|
||||||
* @param autoFill: Whether to automatically fill the field with data from the API
|
* @param autoFill: Whether to automatically fill the field with data from the API
|
||||||
|
* @param autoFillFilters: Optional filters to apply when auto-filling the field
|
||||||
* @param onValueChange : Callback function to call when the field value changes
|
* @param onValueChange : Callback function to call when the field value changes
|
||||||
* @param adjustFilters : Callback function to adjust the filters for a related field before a query is made
|
* @param adjustFilters : Callback function to adjust the filters for a related field before a query is made
|
||||||
* @param adjustValue : Callback function to adjust the value of the field before it is sent to the API
|
* @param adjustValue : Callback function to adjust the value of the field before it is sent to the API
|
||||||
@@ -106,6 +107,7 @@ export type ApiFormFieldType = {
|
|||||||
preFieldContent?: JSX.Element;
|
preFieldContent?: JSX.Element;
|
||||||
postFieldContent?: JSX.Element;
|
postFieldContent?: JSX.Element;
|
||||||
autoFill?: boolean;
|
autoFill?: boolean;
|
||||||
|
autoFillFilters?: any;
|
||||||
adjustValue?: (value: any) => any;
|
adjustValue?: (value: any) => any;
|
||||||
onValueChange?: (value: any, record?: any) => void;
|
onValueChange?: (value: any, record?: any) => void;
|
||||||
adjustFilters?: (value: ApiFormAdjustFilterType) => any;
|
adjustFilters?: (value: ApiFormAdjustFilterType) => any;
|
||||||
|
@@ -73,6 +73,7 @@ export function ApiFormField({
|
|||||||
return {
|
return {
|
||||||
...fieldDefinition,
|
...fieldDefinition,
|
||||||
autoFill: undefined,
|
autoFill: undefined,
|
||||||
|
autoFillFilters: undefined,
|
||||||
onValueChange: undefined,
|
onValueChange: undefined,
|
||||||
adjustFilters: undefined,
|
adjustFilters: undefined,
|
||||||
adjustValue: undefined,
|
adjustValue: undefined,
|
||||||
|
@@ -65,7 +65,11 @@ export function RelatedModelField({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = definition?.filters ?? {};
|
// Construct parameters for auto-filling the field
|
||||||
|
const params = {
|
||||||
|
...(definition?.filters ?? {}),
|
||||||
|
...(definition?.autoFillFilters ?? {})
|
||||||
|
};
|
||||||
|
|
||||||
api
|
api
|
||||||
.get(definition.api_url, {
|
.get(definition.api_url, {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Alert, List, Stack, Table } from '@mantine/core';
|
import { Alert, Divider, List, Stack, Table } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
|
IconInfoCircle,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconList,
|
IconList,
|
||||||
IconSitemap,
|
IconSitemap,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
type TableFieldRowProps
|
type TableFieldRowProps
|
||||||
} from '../components/forms/fields/TableField';
|
} from '../components/forms/fields/TableField';
|
||||||
import { StatusRenderer } from '../components/render/StatusRenderer';
|
import { StatusRenderer } from '../components/render/StatusRenderer';
|
||||||
|
import { RenderStockItem } from '../components/render/Stock';
|
||||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||||
import {
|
import {
|
||||||
useBatchCodeGenerator,
|
useBatchCodeGenerator,
|
||||||
@@ -473,10 +475,12 @@ export function useCancelBuildOutputsForm({
|
|||||||
// Construct a single row in the 'allocate stock to build' table
|
// Construct a single row in the 'allocate stock to build' table
|
||||||
function BuildAllocateLineRow({
|
function BuildAllocateLineRow({
|
||||||
props,
|
props,
|
||||||
|
output,
|
||||||
record,
|
record,
|
||||||
sourceLocation
|
sourceLocation
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
props: TableFieldRowProps;
|
props: TableFieldRowProps;
|
||||||
|
output: any;
|
||||||
record: any;
|
record: any;
|
||||||
sourceLocation: number | undefined;
|
sourceLocation: number | undefined;
|
||||||
}>) {
|
}>) {
|
||||||
@@ -485,6 +489,10 @@ function BuildAllocateLineRow({
|
|||||||
field_type: 'related field',
|
field_type: 'related field',
|
||||||
api_url: apiUrl(ApiEndpoints.stock_item_list),
|
api_url: apiUrl(ApiEndpoints.stock_item_list),
|
||||||
model: ModelType.stockitem,
|
model: ModelType.stockitem,
|
||||||
|
autoFill: !!output?.serial,
|
||||||
|
autoFillFilters: {
|
||||||
|
serial: output?.serial
|
||||||
|
},
|
||||||
filters: {
|
filters: {
|
||||||
available: true,
|
available: true,
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
@@ -564,12 +572,14 @@ function BuildAllocateLineRow({
|
|||||||
*/
|
*/
|
||||||
export function useAllocateStockToBuildForm({
|
export function useAllocateStockToBuildForm({
|
||||||
buildId,
|
buildId,
|
||||||
|
output,
|
||||||
outputId,
|
outputId,
|
||||||
build,
|
build,
|
||||||
lineItems,
|
lineItems,
|
||||||
onFormSuccess
|
onFormSuccess
|
||||||
}: {
|
}: {
|
||||||
buildId?: number;
|
buildId?: number;
|
||||||
|
output?: any;
|
||||||
outputId?: number | null;
|
outputId?: number | null;
|
||||||
build?: any;
|
build?: any;
|
||||||
lineItems: any[];
|
lineItems: any[];
|
||||||
@@ -598,6 +608,7 @@ export function useAllocateStockToBuildForm({
|
|||||||
return (
|
return (
|
||||||
<BuildAllocateLineRow
|
<BuildAllocateLineRow
|
||||||
key={row.idx}
|
key={row.idx}
|
||||||
|
output={output}
|
||||||
props={row}
|
props={row}
|
||||||
record={record}
|
record={record}
|
||||||
sourceLocation={sourceLocation}
|
sourceLocation={sourceLocation}
|
||||||
@@ -608,7 +619,7 @@ export function useAllocateStockToBuildForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, [lineItems, sourceLocation]);
|
}, [output, lineItems, sourceLocation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSourceLocation(build?.take_from);
|
setSourceLocation(build?.take_from);
|
||||||
@@ -633,10 +644,22 @@ export function useAllocateStockToBuildForm({
|
|||||||
const preFormContent = useMemo(() => {
|
const preFormContent = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
|
{output?.pk && (
|
||||||
|
<Stack gap='xs'>
|
||||||
|
<Alert
|
||||||
|
color='blue'
|
||||||
|
icon={<IconInfoCircle />}
|
||||||
|
title={t`Build Output`}
|
||||||
|
>
|
||||||
|
<RenderStockItem instance={output} />
|
||||||
|
</Alert>
|
||||||
|
<Divider />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
<StandaloneField fieldDefinition={sourceLocationField} />
|
<StandaloneField fieldDefinition={sourceLocationField} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}, [sourceLocationField]);
|
}, [output, sourceLocationField]);
|
||||||
|
|
||||||
return useCreateApiFormModal({
|
return useCreateApiFormModal({
|
||||||
url: ApiEndpoints.build_order_allocate,
|
url: ApiEndpoints.build_order_allocate,
|
||||||
|
@@ -518,6 +518,7 @@ export default function BuildLineTable({
|
|||||||
|
|
||||||
const allocateStock = useAllocateStockToBuildForm({
|
const allocateStock = useAllocateStockToBuildForm({
|
||||||
build: build,
|
build: build,
|
||||||
|
output: output,
|
||||||
outputId: output?.pk ?? null,
|
outputId: output?.pk ?? null,
|
||||||
buildId: build.pk,
|
buildId: build.pk,
|
||||||
lineItems: selectedRows,
|
lineItems: selectedRows,
|
||||||
|
@@ -160,7 +160,7 @@ export default function BuildOutputTable({
|
|||||||
const buildStatus = useStatusCodes({ modelType: ModelType.build });
|
const buildStatus = useStatusCodes({ modelType: ModelType.build });
|
||||||
|
|
||||||
// Fetch the test templates associated with the partId
|
// Fetch the test templates associated with the partId
|
||||||
const { data: testTemplates } = useQuery({
|
const { data: testTemplates, refetch: refetchTestTemplates } = useQuery({
|
||||||
queryKey: ['buildoutputtests', partId, build],
|
queryKey: ['buildoutputtests', partId, build],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!partId || partId < 0) {
|
if (!partId || partId < 0) {
|
||||||
@@ -291,7 +291,12 @@ export default function BuildOutputTable({
|
|||||||
batch_code: build.batch,
|
batch_code: build.batch,
|
||||||
location: build.destination ?? build.part_detail?.default_location
|
location: build.destination ?? build.part_detail?.default_location
|
||||||
},
|
},
|
||||||
table: table
|
onFormSuccess: () => {
|
||||||
|
// Refresh all associated table data
|
||||||
|
refetchTrackedItems();
|
||||||
|
refetchTestTemplates();
|
||||||
|
table.refreshTable(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
|
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
|
||||||
|
@@ -326,6 +326,87 @@ test('Build Order - Allocation', async ({ browser }) => {
|
|||||||
.waitFor();
|
.waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Build Order - Tracked Outputs', async ({ browser }) => {
|
||||||
|
const page = await doCachedLogin(browser, {
|
||||||
|
url: 'manufacturing/build-order/10/incomplete-outputs'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a new build output, serial number 15
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'action-button-add-build-output' })
|
||||||
|
.click();
|
||||||
|
await page.getByLabel('number-field-quantity').fill('1');
|
||||||
|
await page.getByLabel('text-field-serial_numbers').fill('15');
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('Build output created').waitFor();
|
||||||
|
|
||||||
|
const cell = await page.getByRole('cell', { name: '# 15' });
|
||||||
|
const row = await getRowFromCell(cell);
|
||||||
|
|
||||||
|
// Open allocation menu for this output
|
||||||
|
await clickOnRowMenu(cell);
|
||||||
|
await page.getByRole('menuitem', { name: 'Allocate', exact: true }).click();
|
||||||
|
|
||||||
|
// Select a particular tracked item to allocate
|
||||||
|
const allocationCell = await page.getByRole('cell', { name: '002.01-PCBA' });
|
||||||
|
const allocationRow = await getRowFromCell(allocationCell);
|
||||||
|
await clickOnRowMenu(allocationCell);
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: 'Allocate Stock', exact: true })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Check for expected text
|
||||||
|
await page
|
||||||
|
.getByLabel('Build Output', { exact: true })
|
||||||
|
.getByText('Serial Number: 15')
|
||||||
|
.waitFor();
|
||||||
|
|
||||||
|
// The stock item should be pre-filled based on serial number
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
await allocationRow.getByText('1 / 1').waitFor();
|
||||||
|
|
||||||
|
// Close the allocation wizard
|
||||||
|
await page.getByRole('banner').getByRole('button').click();
|
||||||
|
|
||||||
|
// Check that the output is now allocated as expected
|
||||||
|
await row.getByText('1 / 6').waitFor();
|
||||||
|
await row.getByText('0 / 2').waitFor();
|
||||||
|
|
||||||
|
// Cancel the build output to return to the original state
|
||||||
|
await clickOnRowMenu(cell);
|
||||||
|
await page.getByRole('menuitem', { name: 'Cancel' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('Build outputs have been cancelled').waitFor();
|
||||||
|
|
||||||
|
// Next, complete a new output and auto-allocate items based on serial number
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'action-button-add-build-output' })
|
||||||
|
.click();
|
||||||
|
await page.getByLabel('number-field-quantity').fill('1');
|
||||||
|
await page.getByLabel('text-field-serial_numbers').fill('16');
|
||||||
|
await page
|
||||||
|
.locator('label')
|
||||||
|
.filter({ hasText: 'Auto Allocate Serial' })
|
||||||
|
.locator('div')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
const newCell = await page.getByRole('cell', { name: '# 16' });
|
||||||
|
const newRow = await getRowFromCell(newCell);
|
||||||
|
|
||||||
|
await newRow.getByText('1 / 6').waitFor();
|
||||||
|
await newRow.getByText('0 / 2').waitFor();
|
||||||
|
|
||||||
|
// Cancel this output too
|
||||||
|
await clickOnRowMenu(newCell);
|
||||||
|
await page.getByRole('menuitem', { name: 'Cancel' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('Build outputs have been cancelled').waitFor();
|
||||||
|
});
|
||||||
|
|
||||||
test('Build Order - Filters', async ({ browser }) => {
|
test('Build Order - Filters', async ({ browser }) => {
|
||||||
const page = await doCachedLogin(browser);
|
const page = await doCachedLogin(browser);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user