mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 20:15:44 +00:00
Stock Transfer Improvements (#8570)
* Allow transfer of items independent of status marker * Update test * Display errors in stock transsfer form * Add option to set status when transferring stock * Fix inStock check for stock actions * Allow adjustment of status when counting stock item * Allow status adjustment for other actions: - Remove stock - Add stock * Revert error behavior * Enhanced unit test * Unit test fix * Bump API version * Fix for playwright test - Added helper func * Extend playwright tests for stock actions
This commit is contained in:
@ -23,6 +23,11 @@ export type ApiFormAdjustFilterType = {
|
||||
data: FieldValues;
|
||||
};
|
||||
|
||||
export type ApiFormFieldChoice = {
|
||||
value: any;
|
||||
display_name: string;
|
||||
};
|
||||
|
||||
/** Definition of the ApiForm field component.
|
||||
* - The 'name' attribute *must* be provided
|
||||
* - All other attributes are optional, and may be provided by the API
|
||||
@ -83,7 +88,7 @@ export type ApiFormFieldType = {
|
||||
child?: ApiFormFieldType;
|
||||
children?: { [key: string]: ApiFormFieldType };
|
||||
required?: boolean;
|
||||
choices?: any[];
|
||||
choices?: ApiFormFieldChoice[];
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
exclude?: boolean;
|
||||
|
@ -213,6 +213,7 @@ export function TableField({
|
||||
*/
|
||||
export function TableFieldExtraRow({
|
||||
visible,
|
||||
fieldName,
|
||||
fieldDefinition,
|
||||
defaultValue,
|
||||
emptyValue,
|
||||
@ -220,6 +221,7 @@ export function TableFieldExtraRow({
|
||||
onValueChange
|
||||
}: {
|
||||
visible: boolean;
|
||||
fieldName?: string;
|
||||
fieldDefinition: ApiFormFieldType;
|
||||
defaultValue?: any;
|
||||
error?: string;
|
||||
@ -253,6 +255,7 @@ export function TableFieldExtraRow({
|
||||
<InvenTreeIcon icon='downright' />
|
||||
</Container>
|
||||
<StandaloneField
|
||||
fieldName={fieldName ?? 'field'}
|
||||
fieldDefinition={field}
|
||||
defaultValue={defaultValue}
|
||||
error={error}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Flex, Group, Skeleton, Table, Text } from '@mantine/core';
|
||||
import { Flex, Group, Skeleton, Stack, Table, Text } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { modals } from '@mantine/modals';
|
||||
import {
|
||||
@ -19,6 +19,7 @@ import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||
import { StandaloneField } from '../components/forms/StandaloneField';
|
||||
import type {
|
||||
ApiFormAdjustFilterType,
|
||||
ApiFormFieldChoice,
|
||||
ApiFormFieldSet
|
||||
} from '../components/forms/fields/ApiFormField';
|
||||
import {
|
||||
@ -43,6 +44,7 @@ import {
|
||||
import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder';
|
||||
import { apiUrl } from '../states/ApiState';
|
||||
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||
import { StatusFilterOptions } from '../tables/Filter';
|
||||
|
||||
/**
|
||||
* Construct a set of fields for creating / editing a StockItem instance
|
||||
@ -430,6 +432,7 @@ type StockRow = {
|
||||
function StockOperationsRow({
|
||||
props,
|
||||
transfer = false,
|
||||
changeStatus = false,
|
||||
add = false,
|
||||
setMax = false,
|
||||
merge = false,
|
||||
@ -437,15 +440,29 @@ function StockOperationsRow({
|
||||
}: {
|
||||
props: TableFieldRowProps;
|
||||
transfer?: boolean;
|
||||
changeStatus?: boolean;
|
||||
add?: boolean;
|
||||
setMax?: boolean;
|
||||
merge?: boolean;
|
||||
record?: any;
|
||||
}) {
|
||||
const statusOptions: ApiFormFieldChoice[] = useMemo(() => {
|
||||
return (
|
||||
StatusFilterOptions(ModelType.stockitem)()?.map((choice) => {
|
||||
return {
|
||||
value: choice.value,
|
||||
display_name: choice.label
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}, []);
|
||||
|
||||
const [quantity, setQuantity] = useState<StockItemQuantity>(
|
||||
add ? 0 : (props.item?.quantity ?? 0)
|
||||
);
|
||||
|
||||
const [status, setStatus] = useState<number | undefined>(undefined);
|
||||
|
||||
const removeAndRefresh = () => {
|
||||
props.removeFn(props.idx);
|
||||
};
|
||||
@ -463,6 +480,17 @@ function StockOperationsRow({
|
||||
}
|
||||
});
|
||||
|
||||
const [statusOpen, statusHandlers] = useDisclosure(false, {
|
||||
onOpen: () => {
|
||||
setStatus(record?.status || undefined);
|
||||
props.changeFn(props.idx, 'status', record?.status || undefined);
|
||||
},
|
||||
onClose: () => {
|
||||
setStatus(undefined);
|
||||
props.changeFn(props.idx, 'status', undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const stockString: string = useMemo(() => {
|
||||
if (!record) {
|
||||
return '-';
|
||||
@ -481,14 +509,21 @@ function StockOperationsRow({
|
||||
<>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<Flex gap='sm' align='center'>
|
||||
<Thumbnail
|
||||
size={40}
|
||||
src={record.part_detail?.thumbnail}
|
||||
align='center'
|
||||
/>
|
||||
<div>{record.part_detail?.name}</div>
|
||||
</Flex>
|
||||
<Stack gap='xs'>
|
||||
<Flex gap='sm' align='center'>
|
||||
<Thumbnail
|
||||
size={40}
|
||||
src={record.part_detail?.thumbnail}
|
||||
align='center'
|
||||
/>
|
||||
<div>{record.part_detail?.name}</div>
|
||||
</Flex>
|
||||
{props.rowErrors?.pk?.message && (
|
||||
<Text c='red' size='xs'>
|
||||
{props.rowErrors.pk.message}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{record.location ? record.location_detail?.pathstring : '-'}
|
||||
@ -531,6 +566,15 @@ function StockOperationsRow({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{changeStatus && (
|
||||
<ActionButton
|
||||
size='sm'
|
||||
icon={<InvenTreeIcon icon='status' />}
|
||||
tooltip={t`Change Status`}
|
||||
onClick={() => statusHandlers.toggle()}
|
||||
variant={statusOpen ? 'filled' : 'transparent'}
|
||||
/>
|
||||
)}
|
||||
{transfer && (
|
||||
<ActionButton
|
||||
size='sm'
|
||||
@ -544,12 +588,30 @@ function StockOperationsRow({
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{changeStatus && (
|
||||
<TableFieldExtraRow
|
||||
visible={statusOpen}
|
||||
onValueChange={(value: any) => {
|
||||
setStatus(value);
|
||||
props.changeFn(props.idx, 'status', value || undefined);
|
||||
}}
|
||||
fieldName='status'
|
||||
fieldDefinition={{
|
||||
field_type: 'choice',
|
||||
label: t`Status`,
|
||||
choices: statusOptions,
|
||||
value: status
|
||||
}}
|
||||
defaultValue={status}
|
||||
/>
|
||||
)}
|
||||
{transfer && (
|
||||
<TableFieldExtraRow
|
||||
visible={transfer && packagingOpen}
|
||||
onValueChange={(value: any) => {
|
||||
props.changeFn(props.idx, 'packaging', value || undefined);
|
||||
}}
|
||||
fieldName='packaging'
|
||||
fieldDefinition={{
|
||||
field_type: 'string',
|
||||
label: t`Packaging`
|
||||
@ -604,19 +666,19 @@ function stockTransferFields(items: any[]): ApiFormFieldSet {
|
||||
<StockOperationsRow
|
||||
props={row}
|
||||
transfer
|
||||
changeStatus
|
||||
setMax
|
||||
key={record.pk}
|
||||
record={record}
|
||||
/>
|
||||
);
|
||||
},
|
||||
headers: [t`Part`, t`Location`, t`In Stock`, t`Move`, t`Actions`]
|
||||
headers: [t`Part`, t`Location`, t`Stock`, t`Move`, t`Actions`]
|
||||
},
|
||||
location: {
|
||||
filters: {
|
||||
structural: false
|
||||
}
|
||||
// TODO: icon
|
||||
},
|
||||
notes: {}
|
||||
};
|
||||
@ -641,6 +703,7 @@ function stockRemoveFields(items: any[]): ApiFormFieldSet {
|
||||
<StockOperationsRow
|
||||
props={row}
|
||||
setMax
|
||||
changeStatus
|
||||
add
|
||||
key={record.pk}
|
||||
record={record}
|
||||
@ -670,7 +733,13 @@ function stockAddFields(items: any[]): ApiFormFieldSet {
|
||||
const record = records[row.item.pk];
|
||||
|
||||
return (
|
||||
<StockOperationsRow props={row} add key={record.pk} record={record} />
|
||||
<StockOperationsRow
|
||||
changeStatus
|
||||
props={row}
|
||||
add
|
||||
key={record.pk}
|
||||
record={record}
|
||||
/>
|
||||
);
|
||||
},
|
||||
headers: [t`Part`, t`Location`, t`In Stock`, t`Add`, t`Actions`]
|
||||
@ -696,6 +765,7 @@ function stockCountFields(items: any[]): ApiFormFieldSet {
|
||||
return (
|
||||
<StockOperationsRow
|
||||
props={row}
|
||||
changeStatus
|
||||
key={row.item.pk}
|
||||
record={records[row.item.pk]}
|
||||
/>
|
||||
@ -763,6 +833,7 @@ function stockMergeFields(items: any[]): ApiFormFieldSet {
|
||||
props={row}
|
||||
key={row.item.item}
|
||||
merge
|
||||
changeStatus
|
||||
record={records[row.item.item]}
|
||||
/>
|
||||
);
|
||||
|
@ -653,7 +653,15 @@ export default function StockDetail() {
|
||||
});
|
||||
|
||||
const stockActions = useMemo(() => {
|
||||
const inStock = stockitem.in_stock;
|
||||
const inStock =
|
||||
user.hasChangeRole(UserRoles.stock) &&
|
||||
stockitem.quantity > 0 &&
|
||||
!stockitem.sales_order &&
|
||||
!stockitem.belongs_to &&
|
||||
!stockitem.customer &&
|
||||
!stockitem.consumed_by &&
|
||||
!stockitem.is_building;
|
||||
|
||||
const serial = stockitem.serial;
|
||||
const serialized =
|
||||
serial != null &&
|
||||
|
@ -37,6 +37,18 @@ export const clearTableFilters = async (page) => {
|
||||
await page.getByLabel('filter-drawer-close').click();
|
||||
};
|
||||
|
||||
export const setTableChoiceFilter = async (page, filter, value) => {
|
||||
await openFilterDrawer(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Add Filter' }).click();
|
||||
await page.getByPlaceholder('Select filter').fill(filter);
|
||||
await page.getByRole('option', { name: 'Status' }).click();
|
||||
await page.getByPlaceholder('Select filter value').click();
|
||||
await page.getByRole('option', { name: value }).click();
|
||||
|
||||
await closeFilterDrawer(page);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the parent 'row' element for a given 'cell' element
|
||||
* @param cell - The cell element
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { test } from '../baseFixtures.ts';
|
||||
import { baseUrl } from '../defaults.ts';
|
||||
import { clearTableFilters, setTableChoiceFilter } from '../helpers.ts';
|
||||
import { doQuickLogin } from '../login.ts';
|
||||
|
||||
test('Sales Orders', async ({ page }) => {
|
||||
test('Sales Orders - Basic Tests', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.goto(`${baseUrl}/home`);
|
||||
@ -11,7 +12,11 @@ test('Sales Orders', async ({ page }) => {
|
||||
|
||||
// Check for expected text in the table
|
||||
await page.getByRole('tab', { name: 'Sales Orders' }).waitFor();
|
||||
await page.getByText('In Progress').first().waitFor();
|
||||
|
||||
await clearTableFilters(page);
|
||||
|
||||
await setTableChoiceFilter(page, 'status', 'On Hold');
|
||||
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
|
||||
// Navigate to a particular sales order
|
||||
|
@ -182,10 +182,22 @@ test('Stock - Stock Actions', async ({ page }) => {
|
||||
await page.getByLabel('action-menu-stock-operations-count').waitFor();
|
||||
await page.getByLabel('action-menu-stock-operations-add').waitFor();
|
||||
await page.getByLabel('action-menu-stock-operations-remove').waitFor();
|
||||
|
||||
await page.getByLabel('action-menu-stock-operations-transfer').click();
|
||||
await page.getByLabel('text-field-notes').fill('test notes');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByText('This field is required.').first().waitFor();
|
||||
|
||||
// Set the status field
|
||||
await page.getByLabel('action-button-change-status').click();
|
||||
await page.getByLabel('choice-field-status').click();
|
||||
await page.getByText('Attention needed').click();
|
||||
|
||||
// Set the packaging field
|
||||
await page.getByLabel('action-button-adjust-packaging').click();
|
||||
await page.getByLabel('text-field-packaging').fill('test packaging');
|
||||
|
||||
// Close the dialog
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
// Find an item which has been sent to a customer
|
||||
|
Reference in New Issue
Block a user