mirror of
https://github.com/inventree/InvenTree.git
synced 2025-08-06 20:11:37 +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 postFieldContent : Content to render after the field
|
||||
* @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 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
|
||||
@@ -106,6 +107,7 @@ export type ApiFormFieldType = {
|
||||
preFieldContent?: JSX.Element;
|
||||
postFieldContent?: JSX.Element;
|
||||
autoFill?: boolean;
|
||||
autoFillFilters?: any;
|
||||
adjustValue?: (value: any) => any;
|
||||
onValueChange?: (value: any, record?: any) => void;
|
||||
adjustFilters?: (value: ApiFormAdjustFilterType) => any;
|
||||
|
@@ -73,6 +73,7 @@ export function ApiFormField({
|
||||
return {
|
||||
...fieldDefinition,
|
||||
autoFill: undefined,
|
||||
autoFillFilters: undefined,
|
||||
onValueChange: undefined,
|
||||
adjustFilters: undefined,
|
||||
adjustValue: undefined,
|
||||
|
@@ -65,7 +65,11 @@ export function RelatedModelField({
|
||||
return;
|
||||
}
|
||||
|
||||
const params = definition?.filters ?? {};
|
||||
// Construct parameters for auto-filling the field
|
||||
const params = {
|
||||
...(definition?.filters ?? {}),
|
||||
...(definition?.autoFillFilters ?? {})
|
||||
};
|
||||
|
||||
api
|
||||
.get(definition.api_url, {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Alert, List, Stack, Table } from '@mantine/core';
|
||||
import { Alert, Divider, List, Stack, Table } from '@mantine/core';
|
||||
import {
|
||||
IconCalendar,
|
||||
IconInfoCircle,
|
||||
IconLink,
|
||||
IconList,
|
||||
IconSitemap,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
type TableFieldRowProps
|
||||
} from '../components/forms/fields/TableField';
|
||||
import { StatusRenderer } from '../components/render/StatusRenderer';
|
||||
import { RenderStockItem } from '../components/render/Stock';
|
||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||
import {
|
||||
useBatchCodeGenerator,
|
||||
@@ -473,10 +475,12 @@ export function useCancelBuildOutputsForm({
|
||||
// Construct a single row in the 'allocate stock to build' table
|
||||
function BuildAllocateLineRow({
|
||||
props,
|
||||
output,
|
||||
record,
|
||||
sourceLocation
|
||||
}: Readonly<{
|
||||
props: TableFieldRowProps;
|
||||
output: any;
|
||||
record: any;
|
||||
sourceLocation: number | undefined;
|
||||
}>) {
|
||||
@@ -485,6 +489,10 @@ function BuildAllocateLineRow({
|
||||
field_type: 'related field',
|
||||
api_url: apiUrl(ApiEndpoints.stock_item_list),
|
||||
model: ModelType.stockitem,
|
||||
autoFill: !!output?.serial,
|
||||
autoFillFilters: {
|
||||
serial: output?.serial
|
||||
},
|
||||
filters: {
|
||||
available: true,
|
||||
part_detail: true,
|
||||
@@ -564,12 +572,14 @@ function BuildAllocateLineRow({
|
||||
*/
|
||||
export function useAllocateStockToBuildForm({
|
||||
buildId,
|
||||
output,
|
||||
outputId,
|
||||
build,
|
||||
lineItems,
|
||||
onFormSuccess
|
||||
}: {
|
||||
buildId?: number;
|
||||
output?: any;
|
||||
outputId?: number | null;
|
||||
build?: any;
|
||||
lineItems: any[];
|
||||
@@ -598,6 +608,7 @@ export function useAllocateStockToBuildForm({
|
||||
return (
|
||||
<BuildAllocateLineRow
|
||||
key={row.idx}
|
||||
output={output}
|
||||
props={row}
|
||||
record={record}
|
||||
sourceLocation={sourceLocation}
|
||||
@@ -608,7 +619,7 @@ export function useAllocateStockToBuildForm({
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, [lineItems, sourceLocation]);
|
||||
}, [output, lineItems, sourceLocation]);
|
||||
|
||||
useEffect(() => {
|
||||
setSourceLocation(build?.take_from);
|
||||
@@ -633,10 +644,22 @@ export function useAllocateStockToBuildForm({
|
||||
const preFormContent = useMemo(() => {
|
||||
return (
|
||||
<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} />
|
||||
</Stack>
|
||||
);
|
||||
}, [sourceLocationField]);
|
||||
}, [output, sourceLocationField]);
|
||||
|
||||
return useCreateApiFormModal({
|
||||
url: ApiEndpoints.build_order_allocate,
|
||||
|
@@ -518,6 +518,7 @@ export default function BuildLineTable({
|
||||
|
||||
const allocateStock = useAllocateStockToBuildForm({
|
||||
build: build,
|
||||
output: output,
|
||||
outputId: output?.pk ?? null,
|
||||
buildId: build.pk,
|
||||
lineItems: selectedRows,
|
||||
|
@@ -160,7 +160,7 @@ export default function BuildOutputTable({
|
||||
const buildStatus = useStatusCodes({ modelType: ModelType.build });
|
||||
|
||||
// Fetch the test templates associated with the partId
|
||||
const { data: testTemplates } = useQuery({
|
||||
const { data: testTemplates, refetch: refetchTestTemplates } = useQuery({
|
||||
queryKey: ['buildoutputtests', partId, build],
|
||||
queryFn: async () => {
|
||||
if (!partId || partId < 0) {
|
||||
@@ -291,7 +291,12 @@ export default function BuildOutputTable({
|
||||
batch_code: build.batch,
|
||||
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[]>([]);
|
||||
|
@@ -326,6 +326,87 @@ test('Build Order - Allocation', async ({ browser }) => {
|
||||
.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 }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
|
Reference in New Issue
Block a user