mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
[UI] Barcode actions (#9538)
* Provide callback function for barcode scan dialog * Adds hook for barcode scan dialog * Fix callback processing * Add function to extract API URL for a model instance * Tweak page titles * Extract instance data when scanning a barcode * Scan item into location * Scan in stock location * Remove notYetImplemented func * Revert "Remove notYetImplemented func" This reverts commit a35408380e749c1d9054fd4fe7bcabec71863982. * Add stock item action * Add playwright tests
This commit is contained in:
parent
7d87b8b896
commit
79dad13328
@ -10,7 +10,7 @@ from django.db import models
|
|||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import resolve, reverse
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@ -958,7 +958,17 @@ class InvenTreeBarcodeMixin(models.Model):
|
|||||||
|
|
||||||
if hasattr(self, 'get_api_url'):
|
if hasattr(self, 'get_api_url'):
|
||||||
api_url = self.get_api_url()
|
api_url = self.get_api_url()
|
||||||
data['api_url'] = f'{api_url}{self.pk}/'
|
data['api_url'] = api_url = f'{api_url}{self.pk}/'
|
||||||
|
|
||||||
|
# Attempt to serialize the object too
|
||||||
|
try:
|
||||||
|
match = resolve(api_url)
|
||||||
|
view_class = match.func.view_class
|
||||||
|
serializer_class = view_class.serializer_class
|
||||||
|
serializer = serializer_class(self)
|
||||||
|
data['instance'] = serializer.data
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if hasattr(self, 'get_absolute_url'):
|
if hasattr(self, 'get_absolute_url'):
|
||||||
data['web_url'] = self.get_absolute_url()
|
data['web_url'] = self.get_absolute_url()
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import type { NavigateFunction } from 'react-router-dom';
|
import type { NavigateFunction } from 'react-router-dom';
|
||||||
import { ModelInformationDict } from '../enums/ModelInformation';
|
import { ModelInformationDict } from '../enums/ModelInformation';
|
||||||
import type { ModelType } from '../enums/ModelType';
|
import type { ModelType } from '../enums/ModelType';
|
||||||
|
import { apiUrl } from './Api';
|
||||||
import { cancelEvent } from './Events';
|
import { cancelEvent } from './Events';
|
||||||
|
|
||||||
export const getBaseUrl = (): string =>
|
export const getBaseUrl = (): string =>
|
||||||
(window as any).INVENTREE_SETTINGS?.base_url || 'web';
|
(window as any).INVENTREE_SETTINGS?.base_url || 'web';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the detail view URL for a given model type
|
* Returns the detail view URL for a given model type.
|
||||||
|
* This is the UI URL, not the API URL.
|
||||||
*/
|
*/
|
||||||
export function getDetailUrl(
|
export function getDetailUrl(
|
||||||
model: ModelType,
|
model: ModelType,
|
||||||
@ -35,6 +37,27 @@ export function getDetailUrl(
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the API detail URL for a given model type.
|
||||||
|
*/
|
||||||
|
export function getApiUrl(
|
||||||
|
model: ModelType,
|
||||||
|
pk: number | string
|
||||||
|
): string | undefined {
|
||||||
|
const modelInfo = ModelInformationDict[model];
|
||||||
|
|
||||||
|
if (pk === undefined || pk === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!pk && modelInfo && modelInfo.api_endpoint) {
|
||||||
|
return apiUrl(modelInfo.api_endpoint, pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`No API detail URL found for model ${model} <${pk}>`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Navigate to a provided link.
|
* Navigate to a provided link.
|
||||||
* - If the link is to be opened externally, open it in a new tab.
|
* - If the link is to be opened externally, open it in a new tab.
|
||||||
|
@ -5,6 +5,7 @@ import { apiUrl } from '@lib/functions/Api';
|
|||||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Box, Divider, Modal } from '@mantine/core';
|
import { Box, Divider, Modal } from '@mantine/core';
|
||||||
|
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { type NavigateFunction, useNavigate } from 'react-router-dom';
|
import { type NavigateFunction, useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
@ -13,13 +14,29 @@ import { useUserState } from '../../states/UserState';
|
|||||||
import { StylishText } from '../items/StylishText';
|
import { StylishText } from '../items/StylishText';
|
||||||
import { BarcodeInput } from './BarcodeInput';
|
import { BarcodeInput } from './BarcodeInput';
|
||||||
|
|
||||||
|
export type BarcodeScanResult = {
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Callback function for handling a barcode scan
|
||||||
|
// This function should return true if the barcode was handled successfully
|
||||||
|
export type BarcodeScanCallback = (
|
||||||
|
barcode: string,
|
||||||
|
response: any
|
||||||
|
) => Promise<BarcodeScanResult>;
|
||||||
|
|
||||||
export default function BarcodeScanDialog({
|
export default function BarcodeScanDialog({
|
||||||
title,
|
title,
|
||||||
opened,
|
opened,
|
||||||
|
callback,
|
||||||
|
modelType,
|
||||||
onClose
|
onClose
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
title?: string;
|
title?: string;
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
|
modelType?: ModelType;
|
||||||
|
callback?: BarcodeScanCallback;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}>) {
|
}>) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -33,69 +50,162 @@ export default function BarcodeScanDialog({
|
|||||||
>
|
>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Box>
|
<Box>
|
||||||
<ScanInputHandler navigate={navigate} onClose={onClose} />
|
<ScanInputHandler
|
||||||
|
navigate={navigate}
|
||||||
|
onClose={onClose}
|
||||||
|
modelType={modelType}
|
||||||
|
callback={callback}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScanInputHandler({
|
export function ScanInputHandler({
|
||||||
|
callback,
|
||||||
|
modelType,
|
||||||
onClose,
|
onClose,
|
||||||
navigate
|
navigate
|
||||||
}: Readonly<{ onClose: () => void; navigate: NavigateFunction }>) {
|
}: Readonly<{
|
||||||
|
callback?: BarcodeScanCallback;
|
||||||
|
onClose: () => void;
|
||||||
|
modelType?: ModelType;
|
||||||
|
navigate: NavigateFunction;
|
||||||
|
}>) {
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
|
||||||
const onScan = useCallback((barcode: string) => {
|
const defaultScan = useCallback(
|
||||||
if (!barcode || barcode.length === 0) {
|
(data: any) => {
|
||||||
return;
|
let match = false;
|
||||||
}
|
|
||||||
|
|
||||||
setProcessing(true);
|
// Find the matching model type
|
||||||
|
for (const model_type of Object.keys(ModelInformationDict)) {
|
||||||
|
// If a specific model type is provided, check if it matches
|
||||||
|
if (modelType && model_type !== modelType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
api
|
if (data[model_type]?.['pk']) {
|
||||||
.post(apiUrl(ApiEndpoints.barcode), {
|
if (user.hasViewPermission(model_type as ModelType)) {
|
||||||
barcode: barcode
|
const url = getDetailUrl(
|
||||||
})
|
model_type as ModelType,
|
||||||
.then((response) => {
|
data[model_type]['pk']
|
||||||
setError('');
|
);
|
||||||
|
onClose();
|
||||||
const data = response.data ?? {};
|
navigate(url);
|
||||||
let match = false;
|
match = true;
|
||||||
|
break;
|
||||||
// Find the matching model type
|
|
||||||
for (const model_type of Object.keys(ModelInformationDict)) {
|
|
||||||
if (data[model_type]?.['pk']) {
|
|
||||||
if (user.hasViewPermission(model_type as ModelType)) {
|
|
||||||
const url = getDetailUrl(
|
|
||||||
model_type as ModelType,
|
|
||||||
data[model_type]['pk']
|
|
||||||
);
|
|
||||||
onClose();
|
|
||||||
navigate(url);
|
|
||||||
match = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
setError(t`No matching item found`);
|
setError(t`No matching item found`);
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
.catch((error) => {
|
[navigate, onClose, user, modelType]
|
||||||
const _error = extractErrorMessage({
|
);
|
||||||
error: error,
|
|
||||||
field: 'error',
|
const onScan = useCallback(
|
||||||
defaultMessage: t`Failed to scan barcode`
|
(barcode: string) => {
|
||||||
|
if (!barcode || barcode.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProcessing(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
api
|
||||||
|
.post(apiUrl(ApiEndpoints.barcode), {
|
||||||
|
barcode: barcode
|
||||||
|
})
|
||||||
|
.then((response: any) => {
|
||||||
|
const data = response.data ?? {};
|
||||||
|
|
||||||
|
if (callback && data.success && response.status === 200) {
|
||||||
|
const instance = null;
|
||||||
|
|
||||||
|
// If the caller is expecting a specific model type, check if it matches
|
||||||
|
if (modelType) {
|
||||||
|
const pk: number = data[modelType]?.['pk'];
|
||||||
|
if (!pk) {
|
||||||
|
setError(t`Barcode does not match the expected model type`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(barcode, data)
|
||||||
|
.then((result: BarcodeScanResult) => {
|
||||||
|
if (result.success) {
|
||||||
|
hideNotification('barcode-scan');
|
||||||
|
showNotification({
|
||||||
|
id: 'barcode-scan',
|
||||||
|
title: t`Success`,
|
||||||
|
message: result.success,
|
||||||
|
color: 'green'
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
setError(result.error ?? t`Failed to handle barcode`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setProcessing(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If no callback is provided, use the default scan function
|
||||||
|
defaultScan(data);
|
||||||
|
setProcessing(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const _error = extractErrorMessage({
|
||||||
|
error: error,
|
||||||
|
field: 'error',
|
||||||
|
defaultMessage: t`Failed to scan barcode`
|
||||||
|
});
|
||||||
|
|
||||||
|
setError(_error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setProcessing(false);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
setError(_error);
|
[callback, defaultScan, modelType, onClose]
|
||||||
})
|
);
|
||||||
.finally(() => {
|
|
||||||
setProcessing(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <BarcodeInput onScan={onScan} error={error} processing={processing} />;
|
return <BarcodeInput onScan={onScan} error={error} processing={processing} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useBarcodeScanDialog({
|
||||||
|
title,
|
||||||
|
callback,
|
||||||
|
modelType
|
||||||
|
}: Readonly<{
|
||||||
|
title: string;
|
||||||
|
modelType?: ModelType;
|
||||||
|
callback: BarcodeScanCallback;
|
||||||
|
}>) {
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
|
const open = useCallback(() => {
|
||||||
|
setOpened(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dialog = (
|
||||||
|
<BarcodeScanDialog
|
||||||
|
title={title}
|
||||||
|
opened={opened}
|
||||||
|
callback={callback}
|
||||||
|
modelType={modelType}
|
||||||
|
onClose={() => setOpened(false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
open,
|
||||||
|
dialog
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -342,8 +342,8 @@ export default function CategoryDetail() {
|
|||||||
selectedId={category?.pk}
|
selectedId={category?.pk}
|
||||||
/>
|
/>
|
||||||
<PageDetail
|
<PageDetail
|
||||||
title={t`Part Category`}
|
title={category?.name ?? t`Part Category`}
|
||||||
subtitle={category?.name}
|
subtitle={category?.description}
|
||||||
icon={category?.icon && <ApiIcon name={category?.icon} />}
|
icon={category?.icon && <ApiIcon name={category?.icon} />}
|
||||||
breadcrumbs={breadcrumbs}
|
breadcrumbs={breadcrumbs}
|
||||||
breadcrumbAction={() => {
|
breadcrumbAction={() => {
|
||||||
|
@ -2,11 +2,14 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
|
import { apiUrl } from '@lib/index';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Group, Skeleton, Stack, Text } from '@mantine/core';
|
import { Group, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { IconInfoCircle, IconPackages, IconSitemap } from '@tabler/icons-react';
|
import { IconInfoCircle, IconPackages, IconSitemap } from '@tabler/icons-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { api } from '../../App';
|
||||||
|
import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialog';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
import {
|
import {
|
||||||
@ -35,7 +38,6 @@ import {
|
|||||||
useTransferStockItem
|
useTransferStockItem
|
||||||
} from '../../forms/StockForms';
|
} from '../../forms/StockForms';
|
||||||
import { InvenTreeIcon } from '../../functions/icons';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import { notYetImplemented } from '../../functions/notifications';
|
|
||||||
import {
|
import {
|
||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
@ -272,6 +274,62 @@ export default function Stock() {
|
|||||||
const transferStockItems = useTransferStockItem(stockItemActionProps);
|
const transferStockItems = useTransferStockItem(stockItemActionProps);
|
||||||
const countStockItems = useCountStockItem(stockItemActionProps);
|
const countStockItems = useCountStockItem(stockItemActionProps);
|
||||||
|
|
||||||
|
const scanInStockItem = useBarcodeScanDialog({
|
||||||
|
title: t`Scan Stock Item`,
|
||||||
|
modelType: ModelType.stockitem,
|
||||||
|
callback: async (barcode, response) => {
|
||||||
|
const item = response.stockitem.instance;
|
||||||
|
|
||||||
|
// Scan the stock item into the current location
|
||||||
|
return api
|
||||||
|
.post(apiUrl(ApiEndpoints.stock_transfer), {
|
||||||
|
location: location.pk,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
pk: item.pk,
|
||||||
|
quantity: item.quantity
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return {
|
||||||
|
success: t`Scanned stock item into location`
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error scanning stock item:', error);
|
||||||
|
return {
|
||||||
|
error: t`Error scanning stock item`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const scanInStockLocation = useBarcodeScanDialog({
|
||||||
|
title: t`Scan Stock Location`,
|
||||||
|
modelType: ModelType.stocklocation,
|
||||||
|
callback: async (barcode, response) => {
|
||||||
|
const pk = response.stocklocation.pk;
|
||||||
|
|
||||||
|
// Set the parent location
|
||||||
|
return api
|
||||||
|
.patch(apiUrl(ApiEndpoints.stock_location_list, pk), {
|
||||||
|
parent: location.pk
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return {
|
||||||
|
success: t`Scanned stock location into location`
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error scanning stock location:', error);
|
||||||
|
return {
|
||||||
|
error: t`Error scanning stock location`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const locationActions = useMemo(
|
const locationActions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
<AdminButton model={ModelType.stocklocation} id={location.pk} />,
|
<AdminButton model={ModelType.stocklocation} id={location.pk} />,
|
||||||
@ -286,14 +344,14 @@ export default function Stock() {
|
|||||||
{
|
{
|
||||||
name: 'Scan in stock items',
|
name: 'Scan in stock items',
|
||||||
icon: <InvenTreeIcon icon='stock' />,
|
icon: <InvenTreeIcon icon='stock' />,
|
||||||
tooltip: 'Scan items',
|
tooltip: 'Scan item into this location',
|
||||||
onClick: notYetImplemented
|
onClick: scanInStockItem.open
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Scan in container',
|
name: 'Scan in container',
|
||||||
icon: <InvenTreeIcon icon='unallocated_stock' />,
|
icon: <InvenTreeIcon icon='unallocated_stock' />,
|
||||||
tooltip: 'Scan container',
|
tooltip: 'Scan container into this location',
|
||||||
onClick: notYetImplemented
|
onClick: scanInStockLocation.open
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -362,6 +420,8 @@ export default function Stock() {
|
|||||||
<>
|
<>
|
||||||
{editLocation.modal}
|
{editLocation.modal}
|
||||||
{deleteLocation.modal}
|
{deleteLocation.modal}
|
||||||
|
{scanInStockItem.dialog}
|
||||||
|
{scanInStockLocation.dialog}
|
||||||
<InstanceDetail
|
<InstanceDetail
|
||||||
status={requestStatus}
|
status={requestStatus}
|
||||||
loading={id ? instanceQuery.isFetching : false}
|
loading={id ? instanceQuery.isFetching : false}
|
||||||
@ -377,8 +437,8 @@ export default function Stock() {
|
|||||||
selectedId={location?.pk}
|
selectedId={location?.pk}
|
||||||
/>
|
/>
|
||||||
<PageDetail
|
<PageDetail
|
||||||
title={t`Stock Items`}
|
title={location?.name ?? t`Stock Location`}
|
||||||
subtitle={location?.name}
|
subtitle={location?.description}
|
||||||
icon={location?.icon && <ApiIcon name={location?.icon} />}
|
icon={location?.icon && <ApiIcon name={location?.icon} />}
|
||||||
actions={locationActions}
|
actions={locationActions}
|
||||||
editAction={editLocation.open}
|
editAction={editLocation.open}
|
||||||
|
@ -33,6 +33,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 } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
|
import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialog';
|
||||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
@ -749,6 +750,37 @@ export default function StockDetail() {
|
|||||||
parts: stockitem.part_detail ? [stockitem.part_detail] : []
|
parts: stockitem.part_detail ? [stockitem.part_detail] : []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const scanIntoLocation = useBarcodeScanDialog({
|
||||||
|
title: t`Scan Into Location`,
|
||||||
|
modelType: ModelType.stocklocation,
|
||||||
|
callback: async (barcode, response) => {
|
||||||
|
const pk = response.stocklocation.pk;
|
||||||
|
|
||||||
|
return api
|
||||||
|
.post(apiUrl(ApiEndpoints.stock_transfer), {
|
||||||
|
location: pk,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
pk: stockitem.pk,
|
||||||
|
quantity: stockitem.quantity
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refreshInstance();
|
||||||
|
return {
|
||||||
|
success: t`Scanned stock item into location`
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log('Error scanning stock item:', error);
|
||||||
|
return {
|
||||||
|
error: t`Error scanning stock item`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const stockActions = useMemo(() => {
|
const stockActions = useMemo(() => {
|
||||||
// Can this stock item be transferred to a different location?
|
// Can this stock item be transferred to a different location?
|
||||||
const canTransfer =
|
const canTransfer =
|
||||||
@ -775,6 +807,14 @@ export default function StockDetail() {
|
|||||||
pk={stockitem.pk}
|
pk={stockitem.pk}
|
||||||
hash={stockitem?.barcode_hash}
|
hash={stockitem?.barcode_hash}
|
||||||
perm={user.hasChangeRole(UserRoles.stock)}
|
perm={user.hasChangeRole(UserRoles.stock)}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
name: t`Scan into location`,
|
||||||
|
icon: <InvenTreeIcon icon='location' />,
|
||||||
|
tooltip: t`Scan this item into a location`,
|
||||||
|
onClick: scanIntoLocation.open
|
||||||
|
}
|
||||||
|
]}
|
||||||
/>,
|
/>,
|
||||||
<PrintingActions
|
<PrintingActions
|
||||||
modelType={ModelType.stockitem}
|
modelType={ModelType.stockitem}
|
||||||
@ -974,6 +1014,7 @@ export default function StockDetail() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{findBySerialNumber.modal}
|
{findBySerialNumber.modal}
|
||||||
|
{scanIntoLocation.dialog}
|
||||||
<InstanceDetail
|
<InstanceDetail
|
||||||
requiredRole={UserRoles.stock}
|
requiredRole={UserRoles.stock}
|
||||||
status={requestStatus}
|
status={requestStatus}
|
||||||
|
@ -236,6 +236,17 @@ test('Stock - Stock Actions', async ({ browser }) => {
|
|||||||
.waitFor();
|
.waitFor();
|
||||||
await page.getByText('123').first().waitFor();
|
await page.getByText('123').first().waitFor();
|
||||||
|
|
||||||
|
// Check barcode actions
|
||||||
|
await page.getByLabel('action-menu-barcode-actions').click();
|
||||||
|
await page
|
||||||
|
.getByLabel('action-menu-barcode-actions-scan-into-location')
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.getByPlaceholder('Enter barcode data')
|
||||||
|
.fill('{"stocklocation": 12}');
|
||||||
|
await page.getByRole('button', { name: 'Scan', exact: true }).click();
|
||||||
|
await page.getByText('Scanned stock item into location').waitFor();
|
||||||
|
|
||||||
// Add stock, and change status
|
// Add stock, and change status
|
||||||
await launchStockAction('add');
|
await launchStockAction('add');
|
||||||
await page.getByLabel('number-field-quantity').fill('12');
|
await page.getByLabel('number-field-quantity').fill('12');
|
||||||
@ -284,3 +295,34 @@ test('Stock - Tracking', async ({ browser }) => {
|
|||||||
await page.getByRole('link', { name: 'Widget Assembly' }).waitFor();
|
await page.getByRole('link', { name: 'Widget Assembly' }).waitFor();
|
||||||
await page.getByRole('cell', { name: 'Installed into assembly' }).waitFor();
|
await page.getByRole('cell', { name: 'Installed into assembly' }).waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Stock - Location', async ({ browser }) => {
|
||||||
|
const page = await doCachedLogin(browser, { url: 'stock/location/12/' });
|
||||||
|
|
||||||
|
await loadTab(page, 'Default Parts');
|
||||||
|
await loadTab(page, 'Stock Items');
|
||||||
|
await loadTab(page, 'Stock Locations');
|
||||||
|
await loadTab(page, 'Location Details');
|
||||||
|
|
||||||
|
await page.getByLabel('action-menu-barcode-actions').click();
|
||||||
|
await page
|
||||||
|
.getByLabel('action-menu-barcode-actions-scan-in-stock-items')
|
||||||
|
.waitFor();
|
||||||
|
await page
|
||||||
|
.getByLabel('action-menu-barcode-actions-scan-in-container')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Attempt to scan in the same location (should fail)
|
||||||
|
await page
|
||||||
|
.getByPlaceholder('Enter barcode data')
|
||||||
|
.fill('{"stocklocation": 12}');
|
||||||
|
await page.getByRole('button', { name: 'Scan', exact: true }).click();
|
||||||
|
await page.getByText('Error scanning stock location').waitFor();
|
||||||
|
|
||||||
|
// Attempt to scan bad data (no match)
|
||||||
|
await page
|
||||||
|
.getByPlaceholder('Enter barcode data')
|
||||||
|
.fill('{"stocklocation": 1234}');
|
||||||
|
await page.getByRole('button', { name: 'Scan', exact: true }).click();
|
||||||
|
await page.getByText('No match found for barcode data').waitFor();
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user