2
0
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:
Oliver
2025-08-01 13:23:57 +10:00
committed by GitHub
parent 4895370d0d
commit 37c0322365
7 changed files with 123 additions and 6 deletions

View File

@@ -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;

View File

@@ -73,6 +73,7 @@ export function ApiFormField({
return {
...fieldDefinition,
autoFill: undefined,
autoFillFilters: undefined,
onValueChange: undefined,
adjustFilters: undefined,
adjustValue: undefined,

View File

@@ -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, {

View File

@@ -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,

View File

@@ -518,6 +518,7 @@ export default function BuildLineTable({
const allocateStock = useAllocateStockToBuildForm({
build: build,
output: output,
outputId: output?.pk ?? null,
buildId: build.pk,
lineItems: selectedRows,

View File

@@ -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[]>([]);

View File

@@ -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);