mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-14 08:19:54 +00:00
[UI] Barcode form inputs (#10973)
* Add barcode buttons to related fields - Only field types which support barcodes * Add per-user settings for barcode support * Fill form field with scanned data * Updated docs * Fix duplicate setting * Add playwright tests * Fix duplicate setting in docs * Fix broken link * Fix memo deps * Fix typo * Remove setting * Updated playwright tests * Improved typing
This commit is contained in:
BIN
docs/docs/assets/images/barcode/barcode_field.png
Normal file
BIN
docs/docs/assets/images/barcode/barcode_field.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/docs/assets/images/barcode/barcode_field_dialog.png
Normal file
BIN
docs/docs/assets/images/barcode/barcode_field_dialog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/docs/assets/images/barcode/barcode_field_filled.png
Normal file
BIN
docs/docs/assets/images/barcode/barcode_field_filled.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -69,6 +69,19 @@ To access this page, select *Scan Barcode* from the main navigation menu:
|
||||
{{ image("barcode/barcode_nav_menu.png", "Barcode menu item") }}
|
||||
{{ image("barcode/barcode_scan_page.png", "Barcode scan page") }}
|
||||
|
||||
### Barcodes in Forms
|
||||
|
||||
The InvenTree user interface supports direct scanning of barcodes within certain forms in the web UI. This means that any form field which points to a model which supports barcodes can accept barcode input. If barcode scanning is supported for a particular field, a barcode icon will be displayed next to the input field:
|
||||
|
||||
{{ image("barcode/barcode_field.png", "Barcode form field") }}
|
||||
|
||||
To scan a barcode into a form field, click this barcode icon. A barcode scanning dialog will be displayed, allowing the user to scan a barcode using their preferred input method:
|
||||
|
||||
{{ image("barcode/barcode_field_dialog.png", "Barcode field scan dialog") }}
|
||||
|
||||
Once scanned, the form field will be automatically populated with the correct item.
|
||||
|
||||
{{ image("barcode/barcode_field_filled.png", "Barcode field populated") }}
|
||||
|
||||
## App Integration
|
||||
|
||||
@@ -81,3 +94,10 @@ If enabled, InvenTree can retain logs of the most recent barcode scans. This can
|
||||
Refer to the [barcode settings](../settings/global.md#barcodes) to enable barcode history logging.
|
||||
|
||||
The barcode history can be viewed via the admin panel in the web interface.
|
||||
|
||||
## Barcode Settings
|
||||
|
||||
There are a number of settings which control the behavior of barcodes within InvenTree. For more information, refer to the links below:
|
||||
|
||||
- [Global Barcode Settings](../settings/global.md#barcodes)
|
||||
- [User Preferences for Barcode Scanning](../settings/user.md#display-settings)
|
||||
|
||||
@@ -78,7 +78,7 @@ If this setting is enabled, users can reset their password via email. This requi
|
||||
|
||||
If this setting is enabled, users must have multi-factor authentication enabled to log in.
|
||||
|
||||
#### Auto Fil SSO Users
|
||||
#### Auto Fill SSO Users
|
||||
|
||||
Automatically fill out user-details from SSO account-data. If this feature is enabled the user is only asked for their username, first- and surname if those values can not be gathered from their SSO profile. This might lead to unwanted usernames bleeding over.
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ The *Display Settings* screen shows general display configuration options:
|
||||
{{ usersetting("STICKY_HEADER") }}
|
||||
{{ usersetting("STICKY_TABLE_HEADER") }}
|
||||
{{ usersetting("SHOW_SPOTLIGHT") }}
|
||||
{{ usersetting("BARCODE_IN_FORM_FIELDS") }}
|
||||
{{ usersetting("DATE_DISPLAY_FORMAT") }}
|
||||
{{ usersetting("FORMS_CLOSE_USING_ESCAPE") }}
|
||||
{{ usersetting("DISPLAY_STOCKTAKE_TAB") }}
|
||||
|
||||
@@ -41,6 +41,12 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
'BARCODE_IN_FORM_FIELDS': {
|
||||
'name': _('Barcode Scanner in Form Fields'),
|
||||
'description': _('Allow barcode scanner input in form fields'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'SEARCH_PREVIEW_SHOW_PARTS': {
|
||||
'name': _('Search Parts'),
|
||||
'description': _('Display parts in search preview window'),
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface ModelInformationInterface {
|
||||
url_detail?: string;
|
||||
api_endpoint: ApiEndpoints;
|
||||
admin_url?: string;
|
||||
supports_barcode?: boolean;
|
||||
icon: keyof InvenTreeIconType;
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/part/:pk/',
|
||||
api_endpoint: ApiEndpoints.part_list,
|
||||
admin_url: '/part/part/',
|
||||
supports_barcode: true,
|
||||
icon: 'part'
|
||||
},
|
||||
parameter: {
|
||||
@@ -60,6 +62,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/purchasing/supplier-part/:pk/',
|
||||
api_endpoint: ApiEndpoints.supplier_part_list,
|
||||
admin_url: '/company/supplierpart/',
|
||||
supports_barcode: true,
|
||||
icon: 'supplier_part'
|
||||
},
|
||||
manufacturerpart: {
|
||||
@@ -69,6 +72,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/purchasing/manufacturer-part/:pk/',
|
||||
api_endpoint: ApiEndpoints.manufacturer_part_list,
|
||||
admin_url: '/company/manufacturerpart/',
|
||||
supports_barcode: true,
|
||||
icon: 'manufacturers'
|
||||
},
|
||||
partcategory: {
|
||||
@@ -87,6 +91,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/stock/item/:pk/',
|
||||
api_endpoint: ApiEndpoints.stock_item_list,
|
||||
admin_url: '/stock/stockitem/',
|
||||
supports_barcode: true,
|
||||
icon: 'stock'
|
||||
},
|
||||
stocklocation: {
|
||||
@@ -96,6 +101,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/stock/location/:pk/',
|
||||
api_endpoint: ApiEndpoints.stock_location_list,
|
||||
admin_url: '/stock/stocklocation/',
|
||||
supports_barcode: true,
|
||||
icon: 'location'
|
||||
},
|
||||
stocklocationtype: {
|
||||
@@ -117,6 +123,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/manufacturing/build-order/:pk/',
|
||||
api_endpoint: ApiEndpoints.build_order_list,
|
||||
admin_url: '/build/build/',
|
||||
supports_barcode: true,
|
||||
icon: 'build_order'
|
||||
},
|
||||
buildline: {
|
||||
@@ -155,6 +162,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/purchasing/purchase-order/:pk/',
|
||||
api_endpoint: ApiEndpoints.purchase_order_list,
|
||||
admin_url: '/order/purchaseorder/',
|
||||
supports_barcode: true,
|
||||
icon: 'purchase_orders'
|
||||
},
|
||||
purchaseorderlineitem: {
|
||||
@@ -170,6 +178,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/sales/sales-order/:pk/',
|
||||
api_endpoint: ApiEndpoints.sales_order_list,
|
||||
admin_url: '/order/salesorder/',
|
||||
supports_barcode: true,
|
||||
icon: 'sales_orders'
|
||||
},
|
||||
salesordershipment: {
|
||||
@@ -178,6 +187,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_overview: '/sales/index/shipments',
|
||||
url_detail: '/sales/shipment/:pk/',
|
||||
api_endpoint: ApiEndpoints.sales_order_shipment_list,
|
||||
supports_barcode: true,
|
||||
icon: 'shipment'
|
||||
},
|
||||
returnorder: {
|
||||
@@ -187,6 +197,7 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/sales/return-order/:pk/',
|
||||
api_endpoint: ApiEndpoints.return_order_list,
|
||||
admin_url: '/order/returnorder/',
|
||||
supports_barcode: true,
|
||||
icon: 'return_orders'
|
||||
},
|
||||
returnorderlineitem: {
|
||||
|
||||
@@ -19,6 +19,11 @@ export type BarcodeScanResult = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type BarcodeScanSuccessCallback = (
|
||||
barcode: string,
|
||||
response: any
|
||||
) => void;
|
||||
|
||||
// Callback function for handling a barcode scan
|
||||
// This function should return true if the barcode was handled successfully
|
||||
export type BarcodeScanCallback = (
|
||||
@@ -31,13 +36,15 @@ export default function BarcodeScanDialog({
|
||||
opened,
|
||||
callback,
|
||||
modelType,
|
||||
onClose
|
||||
onClose,
|
||||
onScanSuccess
|
||||
}: Readonly<{
|
||||
title?: string;
|
||||
opened: boolean;
|
||||
modelType?: ModelType;
|
||||
callback?: BarcodeScanCallback;
|
||||
onClose: () => void;
|
||||
onScanSuccess?: BarcodeScanSuccessCallback;
|
||||
}>) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -53,6 +60,7 @@ export default function BarcodeScanDialog({
|
||||
<ScanInputHandler
|
||||
navigate={navigate}
|
||||
onClose={onClose}
|
||||
onScanSuccess={onScanSuccess}
|
||||
modelType={modelType}
|
||||
callback={callback}
|
||||
/>
|
||||
@@ -65,10 +73,12 @@ export function ScanInputHandler({
|
||||
callback,
|
||||
modelType,
|
||||
onClose,
|
||||
onScanSuccess,
|
||||
navigate
|
||||
}: Readonly<{
|
||||
callback?: BarcodeScanCallback;
|
||||
onClose: () => void;
|
||||
onScanSuccess?: BarcodeScanSuccessCallback;
|
||||
modelType?: ModelType;
|
||||
navigate: NavigateFunction;
|
||||
}>) {
|
||||
@@ -94,7 +104,13 @@ export function ScanInputHandler({
|
||||
data[model_type]['pk']
|
||||
);
|
||||
onClose();
|
||||
|
||||
if (onScanSuccess) {
|
||||
onScanSuccess(data['barcode'], data);
|
||||
} else {
|
||||
navigate(url);
|
||||
}
|
||||
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
import type { ModelType } from '@lib/index';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { ActionIcon, Tooltip } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconQrcode } from '@tabler/icons-react';
|
||||
import BarcodeScanDialog from '../barcodes/BarcodeScanDialog';
|
||||
import BarcodeScanDialog, {
|
||||
type BarcodeScanCallback,
|
||||
type BarcodeScanSuccessCallback
|
||||
} from '../barcodes/BarcodeScanDialog';
|
||||
|
||||
/**
|
||||
* A button which opens the QR code scanner modal
|
||||
*/
|
||||
export function ScanButton() {
|
||||
export function ScanButton({
|
||||
modelType,
|
||||
callback,
|
||||
onScanSuccess
|
||||
}: {
|
||||
modelType?: ModelType;
|
||||
callback?: BarcodeScanCallback;
|
||||
onScanSuccess?: BarcodeScanSuccessCallback;
|
||||
}) {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip position='bottom-end' label={t`Scan Barcode`}>
|
||||
<ActionIcon
|
||||
aria-label={`barcode-scan-button-${modelType ?? 'any'}`}
|
||||
onClick={open}
|
||||
variant='transparent'
|
||||
title={t`Open Barcode Scanner`}
|
||||
@@ -21,7 +34,13 @@ export function ScanButton() {
|
||||
<IconQrcode />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<BarcodeScanDialog opened={opened} onClose={close} />
|
||||
<BarcodeScanDialog
|
||||
opened={opened}
|
||||
modelType={modelType}
|
||||
callback={callback}
|
||||
onClose={close}
|
||||
onScanSuccess={onScanSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
Group,
|
||||
Input,
|
||||
darken,
|
||||
useMantineColorScheme,
|
||||
@@ -15,9 +16,16 @@ import {
|
||||
} from 'react-hook-form';
|
||||
import Select from 'react-select';
|
||||
|
||||
import { ModelInformationDict } from '@lib/enums/ModelInformation';
|
||||
import type { ApiFormFieldType } from '@lib/types/Forms';
|
||||
import { useApi } from '../../../contexts/ApiContext';
|
||||
import {
|
||||
useGlobalSettingsState,
|
||||
useUserSettingsState
|
||||
} from '../../../states/SettingsStates';
|
||||
import { vars } from '../../../theme';
|
||||
import { ScanButton } from '../../buttons/ScanButton';
|
||||
import Expand from '../../items/Expand';
|
||||
import { RenderInstance } from '../../render/Instance';
|
||||
|
||||
/**
|
||||
@@ -60,6 +68,101 @@ export function RelatedModelField({
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const dataRef = useRef<any[]>([]);
|
||||
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
const userSettings = useUserSettingsState();
|
||||
|
||||
// Search input query
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [searchText] = useDebouncedValue(value, 250);
|
||||
|
||||
// Fetch a single field by primary key, using the provided API filters
|
||||
const fetchSingleField = useCallback(
|
||||
(pk: number) => {
|
||||
if (!definition?.api_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = definition?.filters ?? {};
|
||||
const url = `${definition.api_url}${pk}/`;
|
||||
|
||||
api
|
||||
.get(url, {
|
||||
params: params
|
||||
})
|
||||
.then((response) => {
|
||||
const pk_field = definition.pk_field ?? 'pk';
|
||||
if (response.data?.[pk_field]) {
|
||||
const value = {
|
||||
value: response.data[pk_field],
|
||||
data: response.data
|
||||
};
|
||||
|
||||
// Run custom callback for this field (if provided)
|
||||
if (definition.onValueChange) {
|
||||
definition.onValueChange(response.data[pk_field], response.data);
|
||||
}
|
||||
|
||||
setInitialData(value);
|
||||
dataRef.current = [value];
|
||||
setPk(response.data[pk_field]);
|
||||
}
|
||||
});
|
||||
},
|
||||
[
|
||||
definition.api_url,
|
||||
definition.filters,
|
||||
definition.onValueChange,
|
||||
definition.pk_field,
|
||||
setValue,
|
||||
setPk
|
||||
]
|
||||
);
|
||||
|
||||
// Memoize the model type information for this field
|
||||
const modelInfo = useMemo(() => {
|
||||
if (!definition.model) {
|
||||
return null;
|
||||
}
|
||||
return ModelInformationDict[definition.model];
|
||||
}, [definition.model]);
|
||||
|
||||
// Determine whether a barcode field should be added
|
||||
const addBarcodeField: boolean = useMemo(() => {
|
||||
if (!modelInfo || !modelInfo.supports_barcode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!globalSettings.isSet('BARCODE_ENABLE')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!userSettings.isSet('BARCODE_IN_FORM_FIELDS')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [globalSettings, userSettings, modelInfo]);
|
||||
|
||||
// Callback function to handle barcode scan results
|
||||
const onBarcodeScan = useCallback(
|
||||
(barcode: string, response: any) => {
|
||||
// Fetch model information from the response
|
||||
const modelData = response?.[definition.model ?? ''] ?? null;
|
||||
|
||||
if (modelData) {
|
||||
const pk_field = definition.pk_field ?? 'pk';
|
||||
const pk = modelData[pk_field];
|
||||
|
||||
if (pk) {
|
||||
// Perform a full re-fetch of the field data
|
||||
// This is necessary as the barcode scan does not provide full data necessarily
|
||||
fetchSingleField(pk);
|
||||
}
|
||||
}
|
||||
},
|
||||
[definition.model, definition.pk_field, fetchSingleField]
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const [autoFilled, setAutoFilled] = useState<boolean>(false);
|
||||
@@ -144,37 +247,7 @@ export function RelatedModelField({
|
||||
const id = pk || field.value;
|
||||
|
||||
if (id !== null && id !== undefined && id !== '') {
|
||||
const url = `${definition.api_url}${id}/`;
|
||||
|
||||
if (!url) {
|
||||
setPk(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const params = definition?.filters ?? {};
|
||||
|
||||
api
|
||||
.get(url, {
|
||||
params: params
|
||||
})
|
||||
.then((response) => {
|
||||
const pk_field = definition.pk_field ?? 'pk';
|
||||
if (response.data?.[pk_field]) {
|
||||
const value = {
|
||||
value: response.data[pk_field],
|
||||
data: response.data
|
||||
};
|
||||
|
||||
// Run custom callback for this field (if provided)
|
||||
if (definition.onValueChange) {
|
||||
definition.onValueChange(response.data[pk_field], response.data);
|
||||
}
|
||||
|
||||
setInitialData(value);
|
||||
dataRef.current = [value];
|
||||
setPk(response.data[pk_field]);
|
||||
}
|
||||
});
|
||||
fetchSingleField(id);
|
||||
} else {
|
||||
setPk(null);
|
||||
}
|
||||
@@ -185,10 +258,6 @@ export function RelatedModelField({
|
||||
field.value
|
||||
]);
|
||||
|
||||
// Search input query
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [searchText] = useDebouncedValue(value, 250);
|
||||
|
||||
const [filters, setFilters] = useState<any>({});
|
||||
|
||||
const resetSearch = useCallback(() => {
|
||||
@@ -377,6 +446,8 @@ export function RelatedModelField({
|
||||
error={definition.error ?? error?.message}
|
||||
styles={{ description: { paddingBottom: '5px' } }}
|
||||
>
|
||||
<Group justify='space-between' wrap='nowrap' gap={3}>
|
||||
<Expand>
|
||||
<Select
|
||||
id={fieldId}
|
||||
aria-label={`related-field-${field.name}`}
|
||||
@@ -429,6 +500,14 @@ export function RelatedModelField({
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Expand>
|
||||
{addBarcodeField && (
|
||||
<ScanButton
|
||||
modelType={definition.model}
|
||||
onScanSuccess={onBarcodeScan}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Input.Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ export default function UserSettings() {
|
||||
'STICKY_HEADER',
|
||||
'STICKY_TABLE_HEADER',
|
||||
'SHOW_SPOTLIGHT',
|
||||
'BARCODE_IN_FORM_FIELDS',
|
||||
'DATE_DISPLAY_FORMAT',
|
||||
'FORMS_CLOSE_USING_ESCAPE',
|
||||
'DISPLAY_STOCKTAKE_TAB',
|
||||
|
||||
@@ -54,7 +54,11 @@ export const clearTableFilters = async (page: Page) => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
};
|
||||
|
||||
export const setTableChoiceFilter = async (page: Page, filter, value) => {
|
||||
export const setTableChoiceFilter = async (
|
||||
page: Page,
|
||||
filter: string,
|
||||
value: string
|
||||
) => {
|
||||
await openFilterDrawer(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Add Filter' }).click();
|
||||
@@ -116,7 +120,7 @@ export const navigate = async (
|
||||
/**
|
||||
* CLick on the 'tab' element with the provided name
|
||||
*/
|
||||
export const loadTab = async (page: Page, tabName, exact?) => {
|
||||
export const loadTab = async (page: Page, tabName: string, exact?: boolean) => {
|
||||
await page
|
||||
.getByLabel(/panel-tabs-/)
|
||||
.getByRole('tab', { name: tabName, exact: exact ?? false })
|
||||
@@ -140,7 +144,7 @@ export const activateCalendarView = async (page: Page) => {
|
||||
/**
|
||||
* Perform a 'global search' on the provided page, for the provided query text
|
||||
*/
|
||||
export const globalSearch = async (page: Page, query) => {
|
||||
export const globalSearch = async (page: Page, query: string) => {
|
||||
await page.getByLabel('open-search').click();
|
||||
await page.getByLabel('global-search-input').clear();
|
||||
await page.getByPlaceholder('Enter search text').fill(query);
|
||||
|
||||
@@ -14,7 +14,7 @@ interface LoginOptions {
|
||||
/*
|
||||
* Perform form based login operation from the "login" URL
|
||||
*/
|
||||
export const doLogin = async (page, options?: LoginOptions) => {
|
||||
export const doLogin = async (page: Page, options?: LoginOptions) => {
|
||||
const username: string = options?.username ?? user.username;
|
||||
const password: string = options?.password ?? user.password;
|
||||
|
||||
|
||||
@@ -225,7 +225,9 @@ test('Purchase Orders - Barcodes', async ({ browser }) => {
|
||||
// Ensure we can scan back to this page, with the associated barcode
|
||||
await page.getByRole('tab', { name: 'Sales' }).click();
|
||||
await page.waitForTimeout(250);
|
||||
await page.getByRole('button', { name: 'Open Barcode Scanner' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'barcode-scan-button-any' }).click();
|
||||
|
||||
await page.getByPlaceholder('Enter barcode data').fill('1234567890');
|
||||
await page.getByRole('button', { name: 'Scan', exact: true }).click();
|
||||
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { test } from '../baseFixtures';
|
||||
import { doCachedLogin } from '../login';
|
||||
|
||||
const scan = async (page, barcode) => {
|
||||
const scan = async (page: Page, barcode: string) => {
|
||||
await page.getByLabel('barcode-input-scanner').click();
|
||||
await page.getByLabel('barcode-scan-keyboard-input').fill(barcode);
|
||||
await page.getByRole('button', { name: 'Scan', exact: true }).click();
|
||||
};
|
||||
|
||||
test('Scanning - Dialog', async ({ browser }) => {
|
||||
test('Barcode Scanning - Dialog', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
await page.getByRole('button', { name: 'Open Barcode Scanner' }).click();
|
||||
// Attempt scan with invalid data
|
||||
await page.getByRole('button', { name: 'barcode-scan-button-any' }).click();
|
||||
await scan(page, 'invalid-barcode-123');
|
||||
await page.getByText('No match found for barcode').waitFor();
|
||||
|
||||
// Attempt scan with "legacy" barcode format
|
||||
await scan(page, '{"part": 15}');
|
||||
|
||||
await page.getByText('Part: R_550R_0805_1%', { exact: true }).waitFor();
|
||||
await page.getByText('Available:').waitFor();
|
||||
await page.getByText('Required:').waitFor();
|
||||
|
||||
// Attempt scan with "modern" barcode format
|
||||
await page.getByRole('button', { name: 'barcode-scan-button-any' }).click();
|
||||
await scan(page, 'INV-BO0010');
|
||||
|
||||
await page.getByText('Build Order: BO0010').waitFor();
|
||||
await page.getByText('Making a high level assembly part').waitFor();
|
||||
});
|
||||
|
||||
test('Scanning - Basic', async ({ browser }) => {
|
||||
test('Barcode Scanning - Basic', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
// Navigate to the 'scan' page
|
||||
@@ -39,7 +52,7 @@ test('Scanning - Basic', async ({ browser }) => {
|
||||
await page.getByText('No match found for barcode').waitFor();
|
||||
});
|
||||
|
||||
test('Scanning - Part', async ({ browser }) => {
|
||||
test('Barcode Scanning - Part', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'scan/' });
|
||||
|
||||
await scan(page, '{"part": 1}');
|
||||
@@ -49,7 +62,7 @@ test('Scanning - Part', async ({ browser }) => {
|
||||
await page.getByRole('cell', { name: 'part', exact: true }).waitFor();
|
||||
});
|
||||
|
||||
test('Scanning - Stockitem', async ({ browser }) => {
|
||||
test('Barcode Scanning - Stockitem', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'scan/' });
|
||||
await scan(page, '{"stockitem": 408}');
|
||||
|
||||
@@ -58,7 +71,7 @@ test('Scanning - Stockitem', async ({ browser }) => {
|
||||
await page.getByRole('cell', { name: 'Quantity: 100' }).waitFor();
|
||||
});
|
||||
|
||||
test('Scanning - StockLocation', async ({ browser }) => {
|
||||
test('Barcode Scanning - StockLocation', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'scan/' });
|
||||
|
||||
await scan(page, '{"stocklocation": 3}');
|
||||
@@ -71,7 +84,7 @@ test('Scanning - StockLocation', async ({ browser }) => {
|
||||
.waitFor();
|
||||
});
|
||||
|
||||
test('Scanning - SupplierPart', async ({ browser }) => {
|
||||
test('Barcode Scanning - SupplierPart', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'scan/' });
|
||||
await scan(page, '{"supplierpart": 204}');
|
||||
|
||||
@@ -80,7 +93,7 @@ test('Scanning - SupplierPart', async ({ browser }) => {
|
||||
await page.getByRole('cell', { name: 'supplierpart', exact: true }).waitFor();
|
||||
});
|
||||
|
||||
test('Scanning - PurchaseOrder', async ({ browser }) => {
|
||||
test('Barcode Scanning - PurchaseOrder', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'scan/' });
|
||||
await scan(page, '{"purchaseorder": 12}');
|
||||
|
||||
@@ -92,7 +105,7 @@ test('Scanning - PurchaseOrder', async ({ browser }) => {
|
||||
.waitFor();
|
||||
});
|
||||
|
||||
test('Scanning - SalesOrder', async ({ browser }) => {
|
||||
test('Barcode Scanning - SalesOrder', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'scan/' });
|
||||
|
||||
await scan(page, '{"salesorder": 6}');
|
||||
@@ -103,7 +116,7 @@ test('Scanning - SalesOrder', async ({ browser }) => {
|
||||
await page.getByRole('cell', { name: 'salesorder', exact: true }).waitFor();
|
||||
});
|
||||
|
||||
test('Scanning - Build', async ({ browser }) => {
|
||||
test('Barcode Scanning - Build', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'scan/' });
|
||||
await scan(page, '{"build": 8}');
|
||||
|
||||
@@ -112,3 +125,32 @@ test('Scanning - Build', async ({ browser }) => {
|
||||
await page.getByText('PCBA build').waitFor();
|
||||
await page.getByRole('cell', { name: 'build', exact: true }).waitFor();
|
||||
});
|
||||
|
||||
test('Barcode Scanning - Forms', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
url: '/stock/location/index/stock-items'
|
||||
});
|
||||
|
||||
// Open the "Add Stock Item" form
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-add-stock-item' })
|
||||
.click();
|
||||
|
||||
// Fill out the "part" data
|
||||
await page.getByRole('button', { name: 'barcode-scan-button-part' }).click();
|
||||
await page
|
||||
.getByRole('textbox', { name: 'barcode-scan-keyboard-input' })
|
||||
.fill('INV-PA99');
|
||||
await page.getByRole('button', { name: 'Scan', exact: true }).click();
|
||||
await page.getByText('Red Round Table').waitFor();
|
||||
|
||||
// Fill out the "location" data
|
||||
await page
|
||||
.getByRole('button', { name: 'barcode-scan-button-stocklocation' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('textbox', { name: 'barcode-scan-keyboard-input' })
|
||||
.fill('INV-SL37');
|
||||
await page.getByRole('button', { name: 'Scan', exact: true }).click();
|
||||
await page.getByText('Offsite Storage').waitFor();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user