mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
[PUI] Fix Build Output Forms (#8184)
* Enhancements for stock item form * Edit stock item from "build output" table * Rearrange menu items * Fix build order line complete action * Fix for other modals * Cleanup dead code * Reload build details after output state change * Logic fix for plugin table * Bump API version * Adds hook for generating placeholder serial numbers * Add playwright tests * Remove unused imports * Cleanup playwright tests
This commit is contained in:
parent
194640f55a
commit
4f06918c36
@ -1,13 +1,16 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 260
|
INVENTREE_API_VERSION = 261
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
261 - 2024-09;26 : https://github.com/inventree/InvenTree/pull/8184
|
||||||
|
- Fixes for BuildOrder API serializers
|
||||||
|
|
||||||
v260 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
|
v260 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
|
||||||
- Adds facility for server-side context data to be passed to client-side plugins
|
- Adds facility for server-side context data to be passed to client-side plugins
|
||||||
|
|
||||||
|
@ -555,7 +555,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'outputs',
|
'outputs',
|
||||||
'location',
|
'location',
|
||||||
'status',
|
'status_custom_key',
|
||||||
'accept_incomplete_allocation',
|
'accept_incomplete_allocation',
|
||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
@ -573,7 +573,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
help_text=_("Location for completed build outputs"),
|
help_text=_("Location for completed build outputs"),
|
||||||
)
|
)
|
||||||
|
|
||||||
status = serializers.ChoiceField(
|
status_custom_key = serializers.ChoiceField(
|
||||||
choices=StockStatus.items(),
|
choices=StockStatus.items(),
|
||||||
default=StockStatus.OK.value,
|
default=StockStatus.OK.value,
|
||||||
label=_("Status"),
|
label=_("Status"),
|
||||||
@ -621,8 +621,8 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
data = self.validated_data
|
data = self.validated_data
|
||||||
|
|
||||||
location = data['location']
|
location = data.get('location', None)
|
||||||
status = data['status']
|
status = data.get('status_custom_key', StockStatus.OK.value)
|
||||||
notes = data.get('notes', '')
|
notes = data.get('notes', '')
|
||||||
|
|
||||||
outputs = data.get('outputs', [])
|
outputs = data.get('outputs', [])
|
||||||
|
@ -30,7 +30,7 @@ export default function TextField({
|
|||||||
|
|
||||||
const [rawText, setRawText] = useState<string>(value || '');
|
const [rawText, setRawText] = useState<string>(value || '');
|
||||||
|
|
||||||
const [debouncedText] = useDebouncedValue(rawText, 250);
|
const [debouncedText] = useDebouncedValue(rawText, 100);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRawText(value || '');
|
setRawText(value || '');
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Alert, Stack, Table, Text } from '@mantine/core';
|
import { Stack, Table } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconLink,
|
IconLink,
|
||||||
@ -9,11 +9,8 @@ import {
|
|||||||
IconUser,
|
IconUser,
|
||||||
IconUsersGroup
|
IconUsersGroup
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { DataTable } from 'mantine-datatable';
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '../App';
|
|
||||||
import { ActionButton } from '../components/buttons/ActionButton';
|
|
||||||
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||||
import { StandaloneField } from '../components/forms/StandaloneField';
|
import { StandaloneField } from '../components/forms/StandaloneField';
|
||||||
import {
|
import {
|
||||||
@ -22,15 +19,15 @@ import {
|
|||||||
} from '../components/forms/fields/ApiFormField';
|
} from '../components/forms/fields/ApiFormField';
|
||||||
import { TableFieldRowProps } from '../components/forms/fields/TableField';
|
import { TableFieldRowProps } from '../components/forms/fields/TableField';
|
||||||
import { ProgressBar } from '../components/items/ProgressBar';
|
import { ProgressBar } from '../components/items/ProgressBar';
|
||||||
|
import { StatusRenderer } from '../components/render/StatusRenderer';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../enums/ModelType';
|
import { ModelType } from '../enums/ModelType';
|
||||||
import { InvenTreeIcon } from '../functions/icons';
|
|
||||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||||
import { useBatchCodeGenerator } from '../hooks/UseGenerator';
|
import { useBatchCodeGenerator } from '../hooks/UseGenerator';
|
||||||
import { useSelectedRows } from '../hooks/UseSelectedRows';
|
import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder';
|
||||||
import { apiUrl } from '../states/ApiState';
|
import { apiUrl } from '../states/ApiState';
|
||||||
import { useGlobalSettingsState } from '../states/SettingsState';
|
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||||
import { PartColumn, StatusColumn } from '../tables/ColumnRenderers';
|
import { PartColumn } from '../tables/ColumnRenderers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field set for BuildOrder forms
|
* Field set for BuildOrder forms
|
||||||
@ -160,32 +157,11 @@ export function useBuildOrderOutputFields({
|
|||||||
setQuantity(Math.max(0, build_quantity - build_complete));
|
setQuantity(Math.max(0, build_quantity - build_complete));
|
||||||
}, [build]);
|
}, [build]);
|
||||||
|
|
||||||
const [serialPlaceholder, setSerialPlaceholder] = useState<string>('');
|
const serialPlaceholder = useSerialNumberPlaceholder({
|
||||||
|
partId: build.part_detail?.pk,
|
||||||
useEffect(() => {
|
key: 'build-output',
|
||||||
if (trackable) {
|
enabled: build.part_detail?.trackable
|
||||||
api
|
|
||||||
.get(apiUrl(ApiEndpoints.part_serial_numbers, build.part_detail.pk))
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data?.next) {
|
|
||||||
setSerialPlaceholder(
|
|
||||||
t`Next serial number` + ' - ' + response.data.next
|
|
||||||
);
|
|
||||||
} else if (response.data?.latest) {
|
|
||||||
setSerialPlaceholder(
|
|
||||||
t`Latest serial number` + ' - ' + response.data.latest
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSerialPlaceholder('');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setSerialPlaceholder('');
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
setSerialPlaceholder('');
|
|
||||||
}
|
|
||||||
}, [build, trackable]);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@ -213,48 +189,37 @@ export function useBuildOrderOutputFields({
|
|||||||
}, [quantity, serialPlaceholder, trackable]);
|
}, [quantity, serialPlaceholder, trackable]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
function BuildOutputFormRow({
|
||||||
* Construct a table of build outputs, for displaying at the top of a form
|
props,
|
||||||
*/
|
record
|
||||||
function buildOutputFormTable(outputs: any[], onRemove: (output: any) => void) {
|
}: Readonly<{
|
||||||
return (
|
props: TableFieldRowProps;
|
||||||
<DataTable
|
record: any;
|
||||||
idAccessor="pk"
|
}>) {
|
||||||
records={outputs}
|
const serial = useMemo(() => {
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
accessor: 'part',
|
|
||||||
title: t`Part`,
|
|
||||||
render: (record: any) => PartColumn(record.part_detail)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: 'quantity',
|
|
||||||
title: t`Quantity`,
|
|
||||||
render: (record: any) => {
|
|
||||||
if (record.serial) {
|
if (record.serial) {
|
||||||
return `# ${record.serial}`;
|
return `# ${record.serial}`;
|
||||||
} else {
|
} else {
|
||||||
return record.quantity;
|
return t`Quantity` + `: ${record.quantity}`;
|
||||||
}
|
}
|
||||||
}
|
}, [record]);
|
||||||
},
|
|
||||||
StatusColumn({ model: ModelType.stockitem, sortable: false }),
|
return (
|
||||||
{
|
<>
|
||||||
accessor: 'actions',
|
<Table.Tr>
|
||||||
title: '',
|
<Table.Td>
|
||||||
render: (record: any) => (
|
<PartColumn part={record.part_detail} />
|
||||||
<ActionButton
|
</Table.Td>
|
||||||
key={`remove-output-${record.pk}`}
|
<Table.Td>{serial}</Table.Td>
|
||||||
tooltip={t`Remove output`}
|
<Table.Td>{record.batch}</Table.Td>
|
||||||
icon={<InvenTreeIcon icon="cancel" />}
|
<Table.Td>
|
||||||
color="red"
|
<StatusRenderer status={record.status} type={ModelType.stockitem} />{' '}
|
||||||
onClick={() => onRemove(record.pk)}
|
</Table.Td>
|
||||||
disabled={outputs.length <= 1}
|
<Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
|
||||||
/>
|
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
||||||
)
|
</Table.Td>
|
||||||
}
|
</Table.Tr>
|
||||||
]}
|
</>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,10 +234,6 @@ export function useCompleteBuildOutputsForm({
|
|||||||
}) {
|
}) {
|
||||||
const [location, setLocation] = useState<number | null>(null);
|
const [location, setLocation] = useState<number | null>(null);
|
||||||
|
|
||||||
const { selectedRows, removeRow } = useSelectedRows({
|
|
||||||
rows: outputs
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location) {
|
if (location) {
|
||||||
return;
|
return;
|
||||||
@ -283,19 +244,22 @@ export function useCompleteBuildOutputsForm({
|
|||||||
);
|
);
|
||||||
}, [location, build.destination, build.part_detail]);
|
}, [location, build.destination, build.part_detail]);
|
||||||
|
|
||||||
const preFormContent = useMemo(() => {
|
|
||||||
return buildOutputFormTable(selectedRows, removeRow);
|
|
||||||
}, [selectedRows, removeRow]);
|
|
||||||
|
|
||||||
const buildOutputCompleteFields: ApiFormFieldSet = useMemo(() => {
|
const buildOutputCompleteFields: ApiFormFieldSet = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
outputs: {
|
outputs: {
|
||||||
hidden: true,
|
field_type: 'table',
|
||||||
value: selectedRows.map((output: any) => {
|
value: outputs.map((output: any) => {
|
||||||
return {
|
return {
|
||||||
output: output.pk
|
output: output.pk
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
|
const record = outputs.find((output) => output.pk == row.item.output);
|
||||||
|
return (
|
||||||
|
<BuildOutputFormRow props={row} record={record} key={record.pk} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
headers: [t`Part`, t`Stock Item`, t`Batch`, t`Status`]
|
||||||
},
|
},
|
||||||
status_custom_key: {},
|
status_custom_key: {},
|
||||||
location: {
|
location: {
|
||||||
@ -303,14 +267,14 @@ export function useCompleteBuildOutputsForm({
|
|||||||
structural: false
|
structural: false
|
||||||
},
|
},
|
||||||
value: location,
|
value: location,
|
||||||
onValueChange: (value) => {
|
onValueChange: (value: any) => {
|
||||||
setLocation(value);
|
setLocation(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
notes: {},
|
notes: {},
|
||||||
accept_incomplete_allocation: {}
|
accept_incomplete_allocation: {}
|
||||||
};
|
};
|
||||||
}, [selectedRows, location]);
|
}, [location, outputs]);
|
||||||
|
|
||||||
return useCreateApiFormModal({
|
return useCreateApiFormModal({
|
||||||
url: apiUrl(ApiEndpoints.build_output_complete, build.pk),
|
url: apiUrl(ApiEndpoints.build_output_complete, build.pk),
|
||||||
@ -318,8 +282,8 @@ export function useCompleteBuildOutputsForm({
|
|||||||
title: t`Complete Build Outputs`,
|
title: t`Complete Build Outputs`,
|
||||||
fields: buildOutputCompleteFields,
|
fields: buildOutputCompleteFields,
|
||||||
onFormSuccess: onFormSuccess,
|
onFormSuccess: onFormSuccess,
|
||||||
preFormContent: preFormContent,
|
successMessage: t`Build outputs have been completed`,
|
||||||
successMessage: t`Build outputs have been completed`
|
size: '80%'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,10 +301,6 @@ export function useScrapBuildOutputsForm({
|
|||||||
}) {
|
}) {
|
||||||
const [location, setLocation] = useState<number | null>(null);
|
const [location, setLocation] = useState<number | null>(null);
|
||||||
|
|
||||||
const { selectedRows, removeRow } = useSelectedRows({
|
|
||||||
rows: outputs
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location) {
|
if (location) {
|
||||||
return;
|
return;
|
||||||
@ -351,20 +311,23 @@ export function useScrapBuildOutputsForm({
|
|||||||
);
|
);
|
||||||
}, [location, build.destination, build.part_detail]);
|
}, [location, build.destination, build.part_detail]);
|
||||||
|
|
||||||
const preFormContent = useMemo(() => {
|
|
||||||
return buildOutputFormTable(selectedRows, removeRow);
|
|
||||||
}, [selectedRows, removeRow]);
|
|
||||||
|
|
||||||
const buildOutputScrapFields: ApiFormFieldSet = useMemo(() => {
|
const buildOutputScrapFields: ApiFormFieldSet = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
outputs: {
|
outputs: {
|
||||||
hidden: true,
|
field_type: 'table',
|
||||||
value: selectedRows.map((output: any) => {
|
value: outputs.map((output: any) => {
|
||||||
return {
|
return {
|
||||||
output: output.pk,
|
output: output.pk,
|
||||||
quantity: output.quantity
|
quantity: output.quantity
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
|
const record = outputs.find((output) => output.pk == row.item.output);
|
||||||
|
return (
|
||||||
|
<BuildOutputFormRow props={row} record={record} key={record.pk} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
headers: [t`Part`, t`Stock Item`, t`Batch`, t`Status`]
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
value: location,
|
value: location,
|
||||||
@ -375,7 +338,7 @@ export function useScrapBuildOutputsForm({
|
|||||||
notes: {},
|
notes: {},
|
||||||
discard_allocations: {}
|
discard_allocations: {}
|
||||||
};
|
};
|
||||||
}, [location, selectedRows]);
|
}, [location, outputs]);
|
||||||
|
|
||||||
return useCreateApiFormModal({
|
return useCreateApiFormModal({
|
||||||
url: apiUrl(ApiEndpoints.build_output_scrap, build.pk),
|
url: apiUrl(ApiEndpoints.build_output_scrap, build.pk),
|
||||||
@ -383,8 +346,8 @@ export function useScrapBuildOutputsForm({
|
|||||||
title: t`Scrap Build Outputs`,
|
title: t`Scrap Build Outputs`,
|
||||||
fields: buildOutputScrapFields,
|
fields: buildOutputScrapFields,
|
||||||
onFormSuccess: onFormSuccess,
|
onFormSuccess: onFormSuccess,
|
||||||
preFormContent: preFormContent,
|
successMessage: t`Build outputs have been scrapped`,
|
||||||
successMessage: t`Build outputs have been scrapped`
|
size: '80%'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,89 +360,37 @@ export function useCancelBuildOutputsForm({
|
|||||||
outputs: any[];
|
outputs: any[];
|
||||||
onFormSuccess: (response: any) => void;
|
onFormSuccess: (response: any) => void;
|
||||||
}) {
|
}) {
|
||||||
const { selectedRows, removeRow } = useSelectedRows({
|
|
||||||
rows: outputs
|
|
||||||
});
|
|
||||||
|
|
||||||
const preFormContent = useMemo(() => {
|
|
||||||
return (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Alert color="red" title={t`Cancel Build Outputs`}>
|
|
||||||
<Text>{t`Selected build outputs will be deleted`}</Text>
|
|
||||||
</Alert>
|
|
||||||
{buildOutputFormTable(selectedRows, removeRow)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}, [selectedRows, removeRow]);
|
|
||||||
|
|
||||||
const buildOutputCancelFields: ApiFormFieldSet = useMemo(() => {
|
const buildOutputCancelFields: ApiFormFieldSet = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
outputs: {
|
outputs: {
|
||||||
hidden: true,
|
field_type: 'table',
|
||||||
value: selectedRows.map((output: any) => {
|
value: outputs.map((output: any) => {
|
||||||
return {
|
return {
|
||||||
output: output.pk
|
output: output.pk
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
|
const record = outputs.find((output) => output.pk == row.item.output);
|
||||||
|
return (
|
||||||
|
<BuildOutputFormRow props={row} record={record} key={record.pk} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
headers: [t`Part`, t`Stock Item`, t`Batch`, t`Status`]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [selectedRows]);
|
}, [outputs]);
|
||||||
|
|
||||||
return useCreateApiFormModal({
|
return useCreateApiFormModal({
|
||||||
url: apiUrl(ApiEndpoints.build_output_delete, build.pk),
|
url: apiUrl(ApiEndpoints.build_output_delete, build.pk),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
title: t`Cancel Build Outputs`,
|
title: t`Cancel Build Outputs`,
|
||||||
fields: buildOutputCancelFields,
|
fields: buildOutputCancelFields,
|
||||||
preFormContent: preFormContent,
|
|
||||||
onFormSuccess: onFormSuccess,
|
onFormSuccess: onFormSuccess,
|
||||||
successMessage: t`Build outputs have been cancelled`
|
successMessage: t`Build outputs have been cancelled`,
|
||||||
|
size: '80%'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAllocationFormTable(
|
|
||||||
outputs: any[],
|
|
||||||
onRemove: (output: any) => void
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<DataTable
|
|
||||||
idAccessor="pk"
|
|
||||||
records={outputs}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
accessor: 'part',
|
|
||||||
title: t`Part`,
|
|
||||||
render: (record: any) => PartColumn(record.part_detail)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: 'allocated',
|
|
||||||
title: t`Allocated`,
|
|
||||||
render: (record: any) => (
|
|
||||||
<ProgressBar
|
|
||||||
value={record.allocated}
|
|
||||||
maximum={record.quantity}
|
|
||||||
progressLabel
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: 'actions',
|
|
||||||
title: '',
|
|
||||||
render: (record: any) => (
|
|
||||||
<ActionButton
|
|
||||||
key={`remove-line-${record.pk}`}
|
|
||||||
tooltip={t`Remove line`}
|
|
||||||
icon={<InvenTreeIcon icon="cancel" />}
|
|
||||||
color="red"
|
|
||||||
onClick={() => onRemove(record.pk)}
|
|
||||||
disabled={outputs.length <= 1}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
||||||
@ -534,14 +445,11 @@ function BuildAllocateLineRow({
|
|||||||
};
|
};
|
||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
const partDetail = useMemo(
|
|
||||||
() => PartColumn(record.part_detail),
|
|
||||||
[record.part_detail]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Tr key={`table-row-${record.pk}`}>
|
<Table.Tr key={`table-row-${record.pk}`}>
|
||||||
<Table.Td>{partDetail}</Table.Td>
|
<Table.Td>
|
||||||
|
<PartColumn part={record.part_detail} />
|
||||||
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
value={record.allocated}
|
value={record.allocated}
|
||||||
|
@ -2,6 +2,14 @@ import { t } from '@lingui/macro';
|
|||||||
import { Flex, Group, Skeleton, Table, Text } from '@mantine/core';
|
import { Flex, Group, Skeleton, Table, Text } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { modals } from '@mantine/modals';
|
import { modals } from '@mantine/modals';
|
||||||
|
import {
|
||||||
|
IconCalendarExclamation,
|
||||||
|
IconCoins,
|
||||||
|
IconCurrencyDollar,
|
||||||
|
IconLink,
|
||||||
|
IconPackage,
|
||||||
|
IconUsersGroup
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { Suspense, useMemo, useState } from 'react';
|
import { Suspense, useMemo, useState } from 'react';
|
||||||
|
|
||||||
@ -39,10 +47,16 @@ import { useGlobalSettingsState } from '../states/SettingsState';
|
|||||||
* Construct a set of fields for creating / editing a StockItem instance
|
* Construct a set of fields for creating / editing a StockItem instance
|
||||||
*/
|
*/
|
||||||
export function useStockFields({
|
export function useStockFields({
|
||||||
|
item_detail,
|
||||||
|
part_detail,
|
||||||
create = false
|
create = false
|
||||||
}: {
|
}: {
|
||||||
|
item_detail?: any;
|
||||||
|
part_detail?: any;
|
||||||
create: boolean;
|
create: boolean;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
const [part, setPart] = useState<number | null>(null);
|
const [part, setPart] = useState<number | null>(null);
|
||||||
const [supplierPart, setSupplierPart] = useState<number | null>(null);
|
const [supplierPart, setSupplierPart] = useState<number | null>(null);
|
||||||
|
|
||||||
@ -86,7 +100,7 @@ export function useStockFields({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
supplier_part: {
|
supplier_part: {
|
||||||
// TODO: icon
|
hidden: part_detail?.purchaseable == false,
|
||||||
value: supplierPart,
|
value: supplierPart,
|
||||||
onValueChange: (value) => {
|
onValueChange: (value) => {
|
||||||
setSupplierPart(value);
|
setSupplierPart(value);
|
||||||
@ -109,6 +123,7 @@ export function useStockFields({
|
|||||||
description: t`Add given quantity as packs instead of individual items`
|
description: t`Add given quantity as packs instead of individual items`
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
|
// Cannot adjust location for existing stock items
|
||||||
hidden: !create,
|
hidden: !create,
|
||||||
onValueChange: (value) => {
|
onValueChange: (value) => {
|
||||||
batchGenerator.update({ location: value });
|
batchGenerator.update({ location: value });
|
||||||
@ -135,11 +150,12 @@ export function useStockFields({
|
|||||||
onValueChange: (value) => setSerialNumbers(value)
|
onValueChange: (value) => setSerialNumbers(value)
|
||||||
},
|
},
|
||||||
serial: {
|
serial: {
|
||||||
hidden: create
|
hidden:
|
||||||
// TODO: icon
|
create ||
|
||||||
|
part_detail?.trackable == false ||
|
||||||
|
(!item_detail?.quantity != undefined && item_detail?.quantity != 1)
|
||||||
},
|
},
|
||||||
batch: {
|
batch: {
|
||||||
// TODO: icon
|
|
||||||
value: batchCode,
|
value: batchCode,
|
||||||
onValueChange: (value) => setBatchCode(value)
|
onValueChange: (value) => setBatchCode(value)
|
||||||
},
|
},
|
||||||
@ -147,22 +163,23 @@ export function useStockFields({
|
|||||||
label: t`Stock Status`
|
label: t`Stock Status`
|
||||||
},
|
},
|
||||||
expiry_date: {
|
expiry_date: {
|
||||||
// TODO: icon
|
icon: <IconCalendarExclamation />,
|
||||||
|
hidden: !globalSettings.isSet('STOCK_ENABLE_EXPIRY')
|
||||||
},
|
},
|
||||||
purchase_price: {
|
purchase_price: {
|
||||||
// TODO: icon
|
icon: <IconCurrencyDollar />
|
||||||
},
|
},
|
||||||
purchase_price_currency: {
|
purchase_price_currency: {
|
||||||
// TODO: icon
|
icon: <IconCoins />
|
||||||
},
|
},
|
||||||
packaging: {
|
packaging: {
|
||||||
// TODO: icon,
|
icon: <IconPackage />
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
// TODO: icon
|
icon: <IconLink />
|
||||||
},
|
},
|
||||||
owner: {
|
owner: {
|
||||||
// TODO: icon
|
icon: <IconUsersGroup />
|
||||||
},
|
},
|
||||||
delete_on_deplete: {}
|
delete_on_deplete: {}
|
||||||
};
|
};
|
||||||
@ -171,7 +188,17 @@ export function useStockFields({
|
|||||||
// TODO: refer to stock.py in original codebase
|
// TODO: refer to stock.py in original codebase
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, [part, supplierPart, batchCode, serialNumbers, trackable, create]);
|
}, [
|
||||||
|
item_detail,
|
||||||
|
part_detail,
|
||||||
|
part,
|
||||||
|
globalSettings,
|
||||||
|
supplierPart,
|
||||||
|
batchCode,
|
||||||
|
serialNumbers,
|
||||||
|
trackable,
|
||||||
|
create
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
66
src/frontend/src/hooks/UsePlaceholder.tsx
Normal file
66
src/frontend/src/hooks/UsePlaceholder.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { api } from '../App';
|
||||||
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
|
import { apiUrl } from '../states/ApiState';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for generating a placeholder text for a serial number input
|
||||||
|
*
|
||||||
|
* This hook fetches the latest serial number information for a given part and generates a placeholder string.
|
||||||
|
*
|
||||||
|
* @param partId The ID of the part to fetch serial number information for
|
||||||
|
* @param key A unique key to identify the query
|
||||||
|
* @param enabled Whether the query should be enabled
|
||||||
|
*/
|
||||||
|
export function useSerialNumberPlaceholder({
|
||||||
|
partId,
|
||||||
|
key,
|
||||||
|
enabled = true
|
||||||
|
}: {
|
||||||
|
partId: number;
|
||||||
|
key: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}): string | undefined {
|
||||||
|
// Fetch serial number information (if available)
|
||||||
|
const snQuery = useQuery({
|
||||||
|
queryKey: ['serial-placeholder', key, partId],
|
||||||
|
enabled: enabled ?? true,
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!partId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = apiUrl(ApiEndpoints.part_serial_numbers, partId);
|
||||||
|
|
||||||
|
return api
|
||||||
|
.get(url)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholder = useMemo(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return undefined;
|
||||||
|
} else if (snQuery.data?.next) {
|
||||||
|
return t`Next serial number` + `: ${snQuery.data.next}`;
|
||||||
|
} else if (snQuery.data?.latest) {
|
||||||
|
return t`Latest serial number` + `: ${snQuery.data.latest}`;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [enabled, snQuery.data]);
|
||||||
|
|
||||||
|
return placeholder;
|
||||||
|
}
|
@ -262,7 +262,11 @@ export default function BuildDetail() {
|
|||||||
name: 'incomplete-outputs',
|
name: 'incomplete-outputs',
|
||||||
label: t`Incomplete Outputs`,
|
label: t`Incomplete Outputs`,
|
||||||
icon: <IconClipboardList />,
|
icon: <IconClipboardList />,
|
||||||
content: build.pk ? <BuildOutputTable build={build} /> : <Skeleton />
|
content: build.pk ? (
|
||||||
|
<BuildOutputTable build={build} refreshBuild={refreshInstance} />
|
||||||
|
) : (
|
||||||
|
<Skeleton />
|
||||||
|
)
|
||||||
// TODO: Hide if build is complete
|
// TODO: Hide if build is complete
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -141,7 +141,7 @@ export default function BomPricingPanel({
|
|||||||
title: t`Component`,
|
title: t`Component`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record.sub_part_detail)
|
render: (record: any) => PartColumn({ part: record.sub_part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'quantity',
|
accessor: 'quantity',
|
||||||
|
@ -30,7 +30,7 @@ export default function VariantPricingPanel({
|
|||||||
title: t`Variant Part`,
|
title: t`Variant Part`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record, true)
|
render: (record: any) => PartColumn({ part: record, full_name: true })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'pricing_min',
|
accessor: 'pricing_min',
|
||||||
|
@ -422,7 +422,10 @@ export default function StockDetail() {
|
|||||||
[stockitem]
|
[stockitem]
|
||||||
);
|
);
|
||||||
|
|
||||||
const editStockItemFields = useStockFields({ create: false });
|
const editStockItemFields = useStockFields({
|
||||||
|
create: false,
|
||||||
|
part_detail: stockitem.part_detail
|
||||||
|
});
|
||||||
|
|
||||||
const editStockItem = useEditApiFormModal({
|
const editStockItem = useEditApiFormModal({
|
||||||
url: ApiEndpoints.stock_item_list,
|
url: ApiEndpoints.stock_item_list,
|
||||||
|
@ -18,7 +18,13 @@ import { TableColumn, TableColumnProps } from './Column';
|
|||||||
import { ProjectCodeHoverCard } from './TableHoverCard';
|
import { ProjectCodeHoverCard } from './TableHoverCard';
|
||||||
|
|
||||||
// Render a Part instance within a table
|
// Render a Part instance within a table
|
||||||
export function PartColumn(part: any, full_name?: boolean) {
|
export function PartColumn({
|
||||||
|
part,
|
||||||
|
full_name
|
||||||
|
}: {
|
||||||
|
part: any;
|
||||||
|
full_name?: boolean;
|
||||||
|
}) {
|
||||||
return part ? (
|
return part ? (
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
|
@ -31,7 +31,7 @@ export function UsedInTable({
|
|||||||
switchable: false,
|
switchable: false,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
title: t`Assembly`,
|
title: t`Assembly`,
|
||||||
render: (record: any) => PartColumn(record.part_detail)
|
render: (record: any) => PartColumn({ part: record.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'part_detail.IPN',
|
accessor: 'part_detail.IPN',
|
||||||
@ -47,7 +47,7 @@ export function UsedInTable({
|
|||||||
accessor: 'sub_part',
|
accessor: 'sub_part',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
title: t`Component`,
|
title: t`Component`,
|
||||||
render: (record: any) => PartColumn(record.sub_part_detail)
|
render: (record: any) => PartColumn({ part: record.sub_part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'quantity',
|
accessor: 'quantity',
|
||||||
|
@ -82,7 +82,7 @@ export default function BuildAllocatedStockTable({
|
|||||||
title: t`Part`,
|
title: t`Part`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record.part_detail)
|
render: (record: any) => PartColumn({ part: record.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hidden: !showPartInfo,
|
hidden: !showPartInfo,
|
||||||
|
@ -175,7 +175,7 @@ export default function BuildLineTable({
|
|||||||
ordering: 'part',
|
ordering: 'part',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record.part_detail)
|
render: (record: any) => PartColumn({ part: record.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'part_detail.IPN',
|
accessor: 'part_detail.IPN',
|
||||||
|
@ -45,7 +45,7 @@ export function BuildOrderTable({
|
|||||||
accessor: 'part',
|
accessor: 'part',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record.part_detail)
|
render: (record: any) => PartColumn({ part: record.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'part_detail.IPN',
|
accessor: 'part_detail.IPN',
|
||||||
|
@ -17,16 +17,20 @@ import {
|
|||||||
useCompleteBuildOutputsForm,
|
useCompleteBuildOutputsForm,
|
||||||
useScrapBuildOutputsForm
|
useScrapBuildOutputsForm
|
||||||
} from '../../forms/BuildForms';
|
} from '../../forms/BuildForms';
|
||||||
|
import { useStockFields } from '../../forms/StockForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import { notYetImplemented } from '../../functions/notifications';
|
import { notYetImplemented } from '../../functions/notifications';
|
||||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
import {
|
||||||
|
useCreateApiFormModal,
|
||||||
|
useEditApiFormModal
|
||||||
|
} from '../../hooks/UseForm';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { LocationColumn, PartColumn, StatusColumn } from '../ColumnRenderers';
|
import { LocationColumn, PartColumn, StatusColumn } from '../ColumnRenderers';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import { RowAction } from '../RowActions';
|
import { RowAction, RowEditAction } from '../RowActions';
|
||||||
import { TableHoverCard } from '../TableHoverCard';
|
import { TableHoverCard } from '../TableHoverCard';
|
||||||
|
|
||||||
type TestResultOverview = {
|
type TestResultOverview = {
|
||||||
@ -34,7 +38,10 @@ type TestResultOverview = {
|
|||||||
result: boolean;
|
result: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BuildOutputTable({ build }: Readonly<{ build: any }>) {
|
export default function BuildOutputTable({
|
||||||
|
build,
|
||||||
|
refreshBuild
|
||||||
|
}: Readonly<{ build: any; refreshBuild: () => void }>) {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const table = useTable('build-outputs');
|
const table = useTable('build-outputs');
|
||||||
|
|
||||||
@ -186,6 +193,7 @@ export default function BuildOutputTable({ build }: Readonly<{ build: any }>) {
|
|||||||
outputs: selectedOutputs,
|
outputs: selectedOutputs,
|
||||||
onFormSuccess: () => {
|
onFormSuccess: () => {
|
||||||
table.refreshTable();
|
table.refreshTable();
|
||||||
|
refreshBuild();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -194,6 +202,7 @@ export default function BuildOutputTable({ build }: Readonly<{ build: any }>) {
|
|||||||
outputs: selectedOutputs,
|
outputs: selectedOutputs,
|
||||||
onFormSuccess: () => {
|
onFormSuccess: () => {
|
||||||
table.refreshTable();
|
table.refreshTable();
|
||||||
|
refreshBuild();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -202,9 +211,24 @@ export default function BuildOutputTable({ build }: Readonly<{ build: any }>) {
|
|||||||
outputs: selectedOutputs,
|
outputs: selectedOutputs,
|
||||||
onFormSuccess: () => {
|
onFormSuccess: () => {
|
||||||
table.refreshTable();
|
table.refreshTable();
|
||||||
|
refreshBuild();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editStockItemFields = useStockFields({
|
||||||
|
create: false,
|
||||||
|
item_detail: selectedOutputs[0],
|
||||||
|
part_detail: selectedOutputs[0]?.part_detail
|
||||||
|
});
|
||||||
|
|
||||||
|
const editBuildOutput = useEditApiFormModal({
|
||||||
|
url: ApiEndpoints.stock_item_list,
|
||||||
|
pk: selectedOutputs[0]?.pk,
|
||||||
|
title: t`Edit Build Output`,
|
||||||
|
fields: editStockItemFields,
|
||||||
|
table: table
|
||||||
|
});
|
||||||
|
|
||||||
const tableActions = useMemo(() => {
|
const tableActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
<AddItemButton
|
<AddItemButton
|
||||||
@ -276,6 +300,13 @@ export default function BuildOutputTable({ build }: Readonly<{ build: any }>) {
|
|||||||
completeBuildOutputsForm.open();
|
completeBuildOutputsForm.open();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
RowEditAction({
|
||||||
|
tooltip: t`Edit build output`,
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedOutputs([record]);
|
||||||
|
editBuildOutput.open();
|
||||||
|
}
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
title: t`Scrap`,
|
title: t`Scrap`,
|
||||||
tooltip: t`Scrap build output`,
|
tooltip: t`Scrap build output`,
|
||||||
@ -306,7 +337,7 @@ export default function BuildOutputTable({ build }: Readonly<{ build: any }>) {
|
|||||||
{
|
{
|
||||||
accessor: 'part',
|
accessor: 'part',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (record: any) => PartColumn(record?.part_detail)
|
render: (record: any) => PartColumn({ part: record?.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'quantity',
|
accessor: 'quantity',
|
||||||
@ -321,18 +352,13 @@ export default function BuildOutputTable({ build }: Readonly<{ build: any }>) {
|
|||||||
text = `# ${record.serial}`;
|
text = `# ${record.serial}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return text;
|
||||||
<Group justify="left" wrap="nowrap">
|
|
||||||
<Text>{text}</Text>
|
|
||||||
{record.batch && (
|
|
||||||
<Text style={{ fontStyle: 'italic' }} size="sm">
|
|
||||||
{t`Batch`}: {record.batch}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessor: 'batch',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
StatusColumn({
|
StatusColumn({
|
||||||
accessor: 'status',
|
accessor: 'status',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
@ -410,6 +436,7 @@ export default function BuildOutputTable({ build }: Readonly<{ build: any }>) {
|
|||||||
{addBuildOutput.modal}
|
{addBuildOutput.modal}
|
||||||
{completeBuildOutputsForm.modal}
|
{completeBuildOutputsForm.modal}
|
||||||
{scrapBuildOutputsForm.modal}
|
{scrapBuildOutputsForm.modal}
|
||||||
|
{editBuildOutput.modal}
|
||||||
{cancelBuildOutputsForm.modal}
|
{cancelBuildOutputsForm.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
tableState={table}
|
tableState={table}
|
||||||
|
@ -243,7 +243,7 @@ export default function ParametricPartTable({
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
noWrap: true,
|
noWrap: true,
|
||||||
render: (record: any) => PartColumn(record)
|
render: (record: any) => PartColumn({ part: record })
|
||||||
},
|
},
|
||||||
DescriptionColumn({}),
|
DescriptionColumn({}),
|
||||||
{
|
{
|
||||||
|
@ -43,7 +43,7 @@ export function PartParameterTable({
|
|||||||
{
|
{
|
||||||
accessor: 'part',
|
accessor: 'part',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (record: any) => PartColumn(record?.part_detail)
|
render: (record: any) => PartColumn({ part: record?.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'part_detail.IPN',
|
accessor: 'part_detail.IPN',
|
||||||
|
@ -28,7 +28,7 @@ function partTableColumns(): TableColumn[] {
|
|||||||
title: t`Part`,
|
title: t`Part`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
noWrap: true,
|
noWrap: true,
|
||||||
render: (record: any) => PartColumn(record)
|
render: (record: any) => PartColumn({ part: record })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'IPN',
|
accessor: 'IPN',
|
||||||
|
@ -336,7 +336,10 @@ export default function PluginListTable() {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
hidden: record.is_builtin != false || record.is_installed != false,
|
hidden:
|
||||||
|
record.is_builtin != false ||
|
||||||
|
record.is_installed != true ||
|
||||||
|
record.active != true,
|
||||||
title: t`Deactivate`,
|
title: t`Deactivate`,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <IconCircleX />,
|
icon: <IconCircleX />,
|
||||||
@ -347,7 +350,10 @@ export default function PluginListTable() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hidden: record.is_builtin != false || record.is_installed != true,
|
hidden:
|
||||||
|
record.is_builtin != false ||
|
||||||
|
record.is_installed != true ||
|
||||||
|
record.active != false,
|
||||||
title: t`Activate`,
|
title: t`Activate`,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
icon: <IconCircleCheck />,
|
icon: <IconCircleCheck />,
|
||||||
|
@ -37,7 +37,7 @@ export function ManufacturerPartTable({
|
|||||||
accessor: 'part',
|
accessor: 'part',
|
||||||
switchable: 'part' in params,
|
switchable: 'part' in params,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (record: any) => PartColumn(record?.part_detail)
|
render: (record: any) => PartColumn({ part: record?.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'manufacturer',
|
accessor: 'manufacturer',
|
||||||
|
@ -128,7 +128,7 @@ export function PurchaseOrderLineItemTable({
|
|||||||
title: t`Internal Part`,
|
title: t`Internal Part`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record.part_detail)
|
render: (record: any) => PartColumn({ part: record.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'description',
|
accessor: 'description',
|
||||||
|
@ -47,7 +47,7 @@ export function SupplierPartTable({
|
|||||||
accessor: 'part',
|
accessor: 'part',
|
||||||
switchable: 'part' in params,
|
switchable: 'part' in params,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (record: any) => PartColumn(record?.part_detail)
|
render: (record: any) => PartColumn({ part: record?.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'supplier',
|
accessor: 'supplier',
|
||||||
|
@ -99,7 +99,7 @@ export default function ReturnOrderLineItemTable({
|
|||||||
accessor: 'part',
|
accessor: 'part',
|
||||||
title: t`Part`,
|
title: t`Part`,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record?.part_detail)
|
render: (record: any) => PartColumn({ part: record?.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'item_detail.serial',
|
accessor: 'item_detail.serial',
|
||||||
|
@ -68,7 +68,7 @@ export default function SalesOrderAllocationTable({
|
|||||||
title: t`Part`,
|
title: t`Part`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record.part_detail)
|
render: (record: any) => PartColumn({ part: record.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'quantity',
|
accessor: 'quantity',
|
||||||
|
@ -59,7 +59,7 @@ export default function SalesOrderLineItemTable({
|
|||||||
accessor: 'part',
|
accessor: 'part',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record?.part_detail)
|
render: (record: any) => PartColumn({ part: record?.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'part_detail.IPN',
|
accessor: 'part_detail.IPN',
|
||||||
|
@ -22,7 +22,7 @@ export default function InstalledItemsTable({
|
|||||||
{
|
{
|
||||||
accessor: 'part',
|
accessor: 'part',
|
||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn(record?.part_detail)
|
render: (record: any) => PartColumn({ part: record?.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'quantity',
|
accessor: 'quantity',
|
||||||
|
@ -47,7 +47,7 @@ function stockItemTableColumns(): TableColumn[] {
|
|||||||
{
|
{
|
||||||
accessor: 'part',
|
accessor: 'part',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (record: any) => PartColumn(record?.part_detail)
|
render: (record: any) => PartColumn({ part: record?.part_detail })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'part_detail.IPN',
|
accessor: 'part_detail.IPN',
|
||||||
|
@ -81,3 +81,79 @@ test('PUI - Pages - Build Order', async ({ page }) => {
|
|||||||
.getByText('Making a high level assembly')
|
.getByText('Making a high level assembly')
|
||||||
.waitFor();
|
.waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('PUI - Pages - Build Order - Build Outputs', async ({ page }) => {
|
||||||
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
await page.goto(`${baseUrl}/part/`);
|
||||||
|
|
||||||
|
// Navigate to the correct build order
|
||||||
|
await page.getByRole('tab', { name: 'Build', exact: true }).click();
|
||||||
|
|
||||||
|
// We have now loaded the "Build Order" table. Check for some expected texts
|
||||||
|
await page.getByText('On Hold').first().waitFor();
|
||||||
|
await page.getByText('Pending').first().waitFor();
|
||||||
|
|
||||||
|
await page.getByRole('cell', { name: 'BO0011' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Incomplete Outputs' }).click();
|
||||||
|
|
||||||
|
// Create a new build output
|
||||||
|
await page.getByLabel('action-button-add-build-output').click();
|
||||||
|
await page.getByLabel('number-field-quantity').fill('5');
|
||||||
|
|
||||||
|
const placeholder = await page
|
||||||
|
.getByLabel('text-field-serial_numbers')
|
||||||
|
.getAttribute('placeholder');
|
||||||
|
|
||||||
|
let sn = 1;
|
||||||
|
|
||||||
|
if (!!placeholder && placeholder.includes('Next serial number')) {
|
||||||
|
sn = parseInt(placeholder.split(':')[1].trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate some new serial numbers
|
||||||
|
await page.getByLabel('text-field-serial_numbers').fill(`${sn}, ${sn + 1}`);
|
||||||
|
|
||||||
|
await page.getByLabel('text-field-batch_code').fill('BATCH12345');
|
||||||
|
await page.getByLabel('related-field-location').click();
|
||||||
|
await page.getByText('Reel Storage').click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
// Should be an error as the number of serial numbers doesn't match the quantity
|
||||||
|
await page.getByText('Errors exist for one or more').waitFor();
|
||||||
|
await page.getByText('Number of unique serial').waitFor();
|
||||||
|
|
||||||
|
// Fix the quantity
|
||||||
|
await page.getByLabel('number-field-quantity').fill('2');
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
// Check that new serial numbers have been created
|
||||||
|
await page
|
||||||
|
.getByRole('cell', { name: `# ${sn}` })
|
||||||
|
.first()
|
||||||
|
.waitFor();
|
||||||
|
await page
|
||||||
|
.getByRole('cell', { name: `# ${sn + 1}` })
|
||||||
|
.first()
|
||||||
|
.waitFor();
|
||||||
|
|
||||||
|
// Cancel one of the newly created outputs
|
||||||
|
const cell = await page.getByRole('cell', { name: `# ${sn}` });
|
||||||
|
const row = await cell.locator('xpath=ancestor::tr').first();
|
||||||
|
await row.getByLabel(/row-action-menu-/i).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Cancel' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('Build outputs have been cancelled').waitFor();
|
||||||
|
|
||||||
|
// Complete the other output
|
||||||
|
const cell2 = await page.getByRole('cell', { name: `# ${sn + 1}` });
|
||||||
|
const row2 = await cell2.locator('xpath=ancestor::tr').first();
|
||||||
|
await row2.getByLabel(/row-action-menu-/i).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Complete' }).click();
|
||||||
|
await page.getByLabel('related-field-location').click();
|
||||||
|
await page.getByText('Mechanical Lab').click();
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('Build outputs have been completed').waitFor();
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user