diff --git a/docs/docs/assets/images/barcode/barcode_field.png b/docs/docs/assets/images/barcode/barcode_field.png
new file mode 100644
index 0000000000..37683eb33e
Binary files /dev/null and b/docs/docs/assets/images/barcode/barcode_field.png differ
diff --git a/docs/docs/assets/images/barcode/barcode_field_dialog.png b/docs/docs/assets/images/barcode/barcode_field_dialog.png
new file mode 100644
index 0000000000..202bf2fcd6
Binary files /dev/null and b/docs/docs/assets/images/barcode/barcode_field_dialog.png differ
diff --git a/docs/docs/assets/images/barcode/barcode_field_filled.png b/docs/docs/assets/images/barcode/barcode_field_filled.png
new file mode 100644
index 0000000000..208cfa4a3a
Binary files /dev/null and b/docs/docs/assets/images/barcode/barcode_field_filled.png differ
diff --git a/docs/docs/barcodes/index.md b/docs/docs/barcodes/index.md
index 8a8eb2ab20..b9e53b38b6 100644
--- a/docs/docs/barcodes/index.md
+++ b/docs/docs/barcodes/index.md
@@ -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)
diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md
index 4b32f972c0..d443084077 100644
--- a/docs/docs/settings/global.md
+++ b/docs/docs/settings/global.md
@@ -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.
diff --git a/docs/docs/settings/user.md b/docs/docs/settings/user.md
index 39871b25d3..faaa7aae8a 100644
--- a/docs/docs/settings/user.md
+++ b/docs/docs/settings/user.md
@@ -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") }}
diff --git a/src/backend/InvenTree/common/setting/user.py b/src/backend/InvenTree/common/setting/user.py
index 793f9bae1b..b318a00213 100644
--- a/src/backend/InvenTree/common/setting/user.py
+++ b/src/backend/InvenTree/common/setting/user.py
@@ -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'),
diff --git a/src/frontend/lib/enums/ModelInformation.tsx b/src/frontend/lib/enums/ModelInformation.tsx
index b529e2c49a..6f93a1ebeb 100644
--- a/src/frontend/lib/enums/ModelInformation.tsx
+++ b/src/frontend/lib/enums/ModelInformation.tsx
@@ -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: {
diff --git a/src/frontend/src/components/barcodes/BarcodeScanDialog.tsx b/src/frontend/src/components/barcodes/BarcodeScanDialog.tsx
index 3457d55534..6e2eb50640 100644
--- a/src/frontend/src/components/barcodes/BarcodeScanDialog.tsx
+++ b/src/frontend/src/components/barcodes/BarcodeScanDialog.tsx
@@ -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({
@@ -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();
- navigate(url);
+
+ if (onScanSuccess) {
+ onScanSuccess(data['barcode'], data);
+ } else {
+ navigate(url);
+ }
+
match = true;
break;
}
diff --git a/src/frontend/src/components/buttons/ScanButton.tsx b/src/frontend/src/components/buttons/ScanButton.tsx
index 46676b6033..542656aed1 100644
--- a/src/frontend/src/components/buttons/ScanButton.tsx
+++ b/src/frontend/src/components/buttons/ScanButton.tsx
@@ -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 (
<>
-
+
>
);
}
diff --git a/src/frontend/src/components/forms/fields/RelatedModelField.tsx b/src/frontend/src/components/forms/fields/RelatedModelField.tsx
index d66fe76aa9..85398bac35 100644
--- a/src/frontend/src/components/forms/fields/RelatedModelField.tsx
+++ b/src/frontend/src/components/forms/fields/RelatedModelField.tsx
@@ -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([]);
const dataRef = useRef([]);
+ const globalSettings = useGlobalSettingsState();
+ const userSettings = useUserSettingsState();
+
+ // Search input query
+ const [value, setValue] = useState('');
+ 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(false);
const [autoFilled, setAutoFilled] = useState(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('');
- const [searchText] = useDebouncedValue(value, 250);
-
const [filters, setFilters] = useState({});
const resetSearch = useCallback(() => {
@@ -377,58 +446,68 @@ export function RelatedModelField({
error={definition.error ?? error?.message}
styles={{ description: { paddingBottom: '5px' } }}
>
-