mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-28 13:54:25 +00:00
Stock convert (#11818)
* [UI] Reimplement "convert stock" action * Add playwright tests
This commit is contained in:
@@ -2636,9 +2636,7 @@ class Part(
|
|||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
# Child parts
|
# Child parts
|
||||||
children = self.get_descendants(include_self=False)
|
for child in self.get_descendants(include_self=False):
|
||||||
|
|
||||||
for child in children:
|
|
||||||
parts.append(child)
|
parts.append(child)
|
||||||
|
|
||||||
# Immediate parent, and siblings
|
# Immediate parent, and siblings
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export enum ApiEndpoints {
|
|||||||
stock_merge = 'stock/merge/',
|
stock_merge = 'stock/merge/',
|
||||||
stock_assign = 'stock/assign/',
|
stock_assign = 'stock/assign/',
|
||||||
stock_status = 'stock/status/',
|
stock_status = 'stock/status/',
|
||||||
|
stock_convert = 'stock/:id/convert/',
|
||||||
stock_install = 'stock/:id/install/',
|
stock_install = 'stock/:id/install/',
|
||||||
stock_uninstall = 'stock/:id/uninstall/',
|
stock_uninstall = 'stock/:id/uninstall/',
|
||||||
stock_serialize = 'stock/:id/serialize/',
|
stock_serialize = 'stock/:id/serialize/',
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
IconPackages,
|
IconPackages,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconSitemap
|
IconSitemap,
|
||||||
|
IconTransform
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { type ReactNode, useMemo, useState } from 'react';
|
import { type ReactNode, useMemo, useState } from 'react';
|
||||||
@@ -33,7 +34,7 @@ import { ModelType } from '@lib/enums/ModelType';
|
|||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { getDetailUrl, getOverviewUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl, getOverviewUrl } from '@lib/functions/Navigation';
|
||||||
import type { StockOperationProps } from '@lib/types/Forms';
|
import type { ApiFormFieldSet, StockOperationProps } from '@lib/types/Forms';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialog';
|
import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialog';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
@@ -665,6 +666,26 @@ export default function StockDetail() {
|
|||||||
onFormSuccess: refreshInstance
|
onFormSuccess: refreshInstance
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const convertStockItemFields: ApiFormFieldSet = useMemo(() => {
|
||||||
|
return {
|
||||||
|
part: {
|
||||||
|
filters: {
|
||||||
|
active: true,
|
||||||
|
convert_from: stockitem.part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [stockitem]);
|
||||||
|
|
||||||
|
const convertStockItem = useCreateApiFormModal({
|
||||||
|
url: ApiEndpoints.stock_convert,
|
||||||
|
pk: stockitem.pk,
|
||||||
|
title: t`Convert Stock Item`,
|
||||||
|
modalId: 'convert-stock-item',
|
||||||
|
fields: convertStockItemFields,
|
||||||
|
onFormSuccess: refreshInstance
|
||||||
|
});
|
||||||
|
|
||||||
const duplicateStockItemFields = useStockFields({
|
const duplicateStockItemFields = useStockFields({
|
||||||
create: true,
|
create: true,
|
||||||
modalId: 'duplicate-stock-item'
|
modalId: 'duplicate-stock-item'
|
||||||
@@ -824,16 +845,6 @@ export default function StockDetail() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const stockActions = useMemo(() => {
|
const stockActions = useMemo(() => {
|
||||||
// Can this stock item be transferred to a different location?
|
|
||||||
const canTransfer =
|
|
||||||
user.hasChangeRole(UserRoles.stock) &&
|
|
||||||
!stockitem.sales_order &&
|
|
||||||
!stockitem.belongs_to &&
|
|
||||||
!stockitem.customer &&
|
|
||||||
!stockitem.consumed_by;
|
|
||||||
|
|
||||||
const isBuilding = stockitem.is_building;
|
|
||||||
|
|
||||||
const serial = stockitem.serial;
|
const serial = stockitem.serial;
|
||||||
const serialized =
|
const serialized =
|
||||||
serial != null &&
|
serial != null &&
|
||||||
@@ -841,6 +852,10 @@ export default function StockDetail() {
|
|||||||
serial != '' &&
|
serial != '' &&
|
||||||
stockitem.quantity == 1;
|
stockitem.quantity == 1;
|
||||||
|
|
||||||
|
const canConvert =
|
||||||
|
!!stockitem.part_detail?.variant_of ||
|
||||||
|
!!stockitem.part_detail?.is_template;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<AdminButton model={ModelType.stockitem} id={stockitem.pk} />,
|
<AdminButton model={ModelType.stockitem} id={stockitem.pk} />,
|
||||||
<LocateItemButton stockId={stockitem.pk} />,
|
<LocateItemButton stockId={stockitem.pk} />,
|
||||||
@@ -906,6 +921,13 @@ export default function StockDetail() {
|
|||||||
hidden: !user.hasChangeRole(UserRoles.stock),
|
hidden: !user.hasChangeRole(UserRoles.stock),
|
||||||
onClick: () => editStockItem.open()
|
onClick: () => editStockItem.open()
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
name: t`Convert`,
|
||||||
|
tooltip: t`Convert this stock item to a different part`,
|
||||||
|
hidden: !user.hasChangeRole(UserRoles.stock) || !canConvert,
|
||||||
|
icon: <IconTransform color='blue' />,
|
||||||
|
onClick: () => convertStockItem.open()
|
||||||
|
},
|
||||||
DeleteItemAction({
|
DeleteItemAction({
|
||||||
hidden: !user.hasDeleteRole(UserRoles.stock),
|
hidden: !user.hasDeleteRole(UserRoles.stock),
|
||||||
onClick: () => deleteStockItem.open()
|
onClick: () => deleteStockItem.open()
|
||||||
@@ -1037,8 +1059,9 @@ export default function StockDetail() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</InstanceDetail>
|
</InstanceDetail>
|
||||||
{editStockItem.modal}
|
{editStockItem.modal}
|
||||||
{duplicateStockItem.modal}
|
|
||||||
{deleteStockItem.modal}
|
{deleteStockItem.modal}
|
||||||
|
{convertStockItem.modal}
|
||||||
|
{duplicateStockItem.modal}
|
||||||
{serializeStockItem.modal}
|
{serializeStockItem.modal}
|
||||||
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
{orderPartsWizard.wizard}
|
{orderPartsWizard.wizard}
|
||||||
|
|||||||
@@ -393,6 +393,39 @@ test('Stock - Stock Actions', async ({ browser }) => {
|
|||||||
await page.getByLabel('action-menu-stock-operations-return').click();
|
await page.getByLabel('action-menu-stock-operations-return').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test conversion between part variants
|
||||||
|
test('Stock - Convert', async ({ browser }) => {
|
||||||
|
const page = await doCachedLogin(browser, { url: 'stock/item/242/details' });
|
||||||
|
|
||||||
|
await page.getByText('widget.red.00 | Red Widget |').waitFor();
|
||||||
|
|
||||||
|
// Convert to widget.red.02
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'action-menu-stock-item-actions' })
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: 'action-menu-stock-item-actions-convert' })
|
||||||
|
.click();
|
||||||
|
await page.getByRole('combobox', { name: 'related-field-part' }).fill('red');
|
||||||
|
await page.getByText('widget.red.02 | Red Widget |').click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
await page.getByText('widget.red.02 | Red Widget |').waitFor();
|
||||||
|
|
||||||
|
// Convert to widget.red.00
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'action-menu-stock-item-actions' })
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: 'action-menu-stock-item-actions-convert' })
|
||||||
|
.click();
|
||||||
|
await page.getByRole('combobox', { name: 'related-field-part' }).fill('red');
|
||||||
|
await page.getByText('widget.red.00 | Red Widget |').click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
await page.getByText('widget.red.00 | Red Widget |').waitFor();
|
||||||
|
});
|
||||||
|
|
||||||
test('Stock - Return Items', async ({ browser }) => {
|
test('Stock - Return Items', async ({ browser }) => {
|
||||||
const page = await doCachedLogin(browser, {
|
const page = await doCachedLogin(browser, {
|
||||||
url: 'sales/customer/32/assigned-stock'
|
url: 'sales/customer/32/assigned-stock'
|
||||||
|
|||||||
Reference in New Issue
Block a user