diff --git a/docs/docs/assets/images/barcode/barcode_link_1.png b/docs/docs/assets/images/barcode/barcode_link_1.png index cb3fabd922..448da23569 100644 Binary files a/docs/docs/assets/images/barcode/barcode_link_1.png and b/docs/docs/assets/images/barcode/barcode_link_1.png differ diff --git a/docs/docs/assets/images/barcode/barcode_link_2.png b/docs/docs/assets/images/barcode/barcode_link_2.png index 0272ea911f..10c373f956 100644 Binary files a/docs/docs/assets/images/barcode/barcode_link_2.png and b/docs/docs/assets/images/barcode/barcode_link_2.png differ diff --git a/docs/docs/assets/images/barcode/barcode_nav_menu.png b/docs/docs/assets/images/barcode/barcode_nav_menu.png new file mode 100644 index 0000000000..d2f503e50b Binary files /dev/null and b/docs/docs/assets/images/barcode/barcode_nav_menu.png differ diff --git a/docs/docs/assets/images/barcode/barcode_no_match.png b/docs/docs/assets/images/barcode/barcode_no_match.png index 1e1c233055..e0807ed8ca 100644 Binary files a/docs/docs/assets/images/barcode/barcode_no_match.png and b/docs/docs/assets/images/barcode/barcode_no_match.png differ diff --git a/docs/docs/assets/images/barcode/barcode_scan.png b/docs/docs/assets/images/barcode/barcode_scan.png index be28755da8..145956ab46 100644 Binary files a/docs/docs/assets/images/barcode/barcode_scan.png and b/docs/docs/assets/images/barcode/barcode_scan.png differ diff --git a/docs/docs/assets/images/barcode/barcode_scan_page.png b/docs/docs/assets/images/barcode/barcode_scan_page.png new file mode 100644 index 0000000000..e4c58e7111 Binary files /dev/null and b/docs/docs/assets/images/barcode/barcode_scan_page.png differ diff --git a/docs/docs/assets/images/barcode/barcode_unlink.png b/docs/docs/assets/images/barcode/barcode_unlink.png deleted file mode 100644 index e70e44348e..0000000000 Binary files a/docs/docs/assets/images/barcode/barcode_unlink.png and /dev/null differ diff --git a/docs/docs/assets/images/barcode/barcode_unlink_1.png b/docs/docs/assets/images/barcode/barcode_unlink_1.png new file mode 100644 index 0000000000..bf60069b4c Binary files /dev/null and b/docs/docs/assets/images/barcode/barcode_unlink_1.png differ diff --git a/docs/docs/assets/images/barcode/barcode_unlink_2.png b/docs/docs/assets/images/barcode/barcode_unlink_2.png new file mode 100644 index 0000000000..01a41980e5 Binary files /dev/null and b/docs/docs/assets/images/barcode/barcode_unlink_2.png differ diff --git a/docs/docs/barcodes/barcodes.md b/docs/docs/barcodes/barcodes.md index a51c3dcc96..e1d7cb8ae1 100644 --- a/docs/docs/barcodes/barcodes.md +++ b/docs/docs/barcodes/barcodes.md @@ -13,7 +13,15 @@ InvenTree has native support for barcodes, which provides powerful functionality - Barcodes can be embedded in [labels or reports](../report/barcodes.md) - Barcode functionality can be [extended via plugins](../extend/plugins/barcode.md) -### Barcode Data Types +### Barcode Formats + +InvenTree supports the following barcode formats: + +- [Internal Barcodes](./internal.md): Native InvenTree barcodes, which are automatically generated for each item +- [External Barcodes](./external.md): External (third party) barcodes which can be assigned to items +- [Custom Barcodes](./custom.md): Fully customizable barcodes can be generated using the plugin system. + +### Barcode Model Linking Barcodes can be linked with the following data model types: @@ -21,41 +29,58 @@ Barcodes can be linked with the following data model types: - [Stock Item](../stock/stock.md#stock-item) - [Stock Location](../stock/stock.md#stock-location) - [Supplier Part](../order/company.md#supplier-parts) +- [Purchase Order](../order/purchase_order.md#purchase-orders) +- [Sales Order](../order/sales_order.md#sales-orders) +- [Return Order](../order/return_order.md#return-orders) +- [Build Order](../build/build.md#build-orders) + +### Configuration Options + +The barcode system can be configured via the [global settings](../settings/global.md#barcodes). ## Web Integration -Barcode scanning can be enabled within the web interface. Barcode scanning in the web interface supports scanning via: +Barcode scanning can be enabled within the web interface. This allows users to scan barcodes directly from the web browser. -- Keyboard style scanners (e.g. USB connected) -- Webcam (image processing) +### Input Modes -### Configuration +The following barcode input modes are supported by the web interface: -Barcode scanning may need to be enabled for the web interface: +- **Webcam**: Use a connected webcam to scan barcodes +- **Scanner**: Use a connected barcode scanner to scan barcodes +- **Keyboard**: Manually enter a barcode via the keyboard -{% with id="barcode_config", url="barcode/barcode_settings.png", description="Barcode settings" %} -{% include 'img.html' %} -{% endwith %} +### Quick Scan -### Scanning - -When enabled, select the barcode icon in the top-right of the menu bar to scan a barcode. If the barcode is recognized by the system, the web browser will automatically navigate to the correct item: +If barcode scanning is enabled in the web interface, select the barcode icon in the top-right of the menu bar to perform a quick-scan of a barcode. If the barcode is recognized by the system, the web browser will automatically navigate to the correct item: {% with id="barcode_scan", url="barcode/barcode_scan.png", description="Barcode scan" %} {% include 'img.html' %} {% endwith %} -#### No Match Found - If no match is found for the scanned barcode, the following error message is displayed: {% with id="barcode_no_match", url="barcode/barcode_no_match.png", description="No match for barcode" %} {% include 'img.html' %} {% endwith %} +### Scanning Action Page + +A more comprehensive barcode scanning interface is available via the "Scan" page in the web interface. This page allows the user to scan multiple barcodes, and perform certain actions on the scanned items. + +To access this page, select *Scan Barcode* from the main navigation menu: + +{% with id="barcode_nav_menu", url="barcode/barcode_nav_menu.png", description="Barcode menu item" %} +{% include 'img.html' %} +{% endwith %} + +{% with id="barcode_scan_page", url="barcode/barcode_scan_page.png", description="Barcode scan page" %} +{% include 'img.html' %} +{% endwith %} + ## App Integration -Barcode scanning is a key feature of the [companion mobile app](../app/barcode.md). +Barcode scanning is a key feature of the [companion mobile app](../app/barcode.md). When running on a device with an integrated camera, the app can scan barcodes directly from the camera feed. ## Barcode History diff --git a/docs/docs/barcodes/external.md b/docs/docs/barcodes/external.md index fb837d3fff..37825e04ba 100644 --- a/docs/docs/barcodes/external.md +++ b/docs/docs/barcodes/external.md @@ -36,7 +36,11 @@ To link an arbitrary barcode, select the *Link Barcode* action as shown below: If an item already has a linked barcode, it can be un-linked by selecting the *Unlink Barcode* action: -{% with id="barcode_unlink", url="barcode/barcode_unlink.png", description="Unlink barcode" %} +{% with id="barcode_unlink_1", url="barcode/barcode_unlink_1.png", description="Unlink barcode" %} +{% include 'img.html' %} +{% endwith %} + +{% with id="barcode_unlink_2", url="barcode/barcode_unlink_2.png", description="Unlink barcode" %} {% include 'img.html' %} {% endwith %} diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md index 6b7ff3b213..c09a3b3d57 100644 --- a/docs/docs/settings/global.md +++ b/docs/docs/settings/global.md @@ -91,6 +91,8 @@ Configuration of barcode functionality: {{ globalsetting("BARCODE_STORE_RESULTS") }} {{ globalsetting("BARCODE_RESULTS_MAX_NUM") }} +Read more about [barcode scanning](../barcodes/barcodes.md). + ### Pricing and Currency Configuration of pricing data and currency support: diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 91d803b28d..bd03cc9a31 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 295 +INVENTREE_API_VERSION = 296 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v296 - 2024-12-25 : https://github.com/inventree/InvenTree/pull/8732 + - Adjust default "part_detail" behaviour for StockItem API endpoints + v295 - 2024-12-23 : https://github.com/inventree/InvenTree/pull/8746 - Improve API documentation for build APIs diff --git a/src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py b/src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py index 2b7e5b0be5..224885813d 100644 --- a/src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py +++ b/src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py @@ -135,15 +135,15 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi def generate(self, model_instance: InvenTreeBarcodeMixin): """Generate a barcode for a given model instance.""" - barcode_format = self.get_setting('INTERNAL_BARCODE_FORMAT') - - if barcode_format == 'json': - return json.dumps({model_instance.barcode_model_type(): model_instance.pk}) + barcode_format = self.get_setting( + 'INTERNAL_BARCODE_FORMAT', backup_value='json' + ) if barcode_format == 'short': prefix = self.get_setting('SHORT_BARCODE_PREFIX') model_type_code = model_instance.barcode_model_type_code() return f'{prefix}{model_type_code}{model_instance.pk}' - - return None + else: + # Default = JSON format + return json.dumps({model_instance.barcode_model_type(): model_instance.pk}) diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index e0db540b15..0fe2326c70 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -902,8 +902,9 @@ class StockApiMixin: try: params = self.request.query_params + kwargs['part_detail'] = str2bool(params.get('part_detail', True)) + for key in [ - 'part_detail', 'path_detail', 'location_detail', 'supplier_part_detail', diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index f093dba680..eff5aa491a 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -11,8 +11,8 @@ export const api = axios.create({}); * Setup default settings for the Axios API instance. */ export function setApiDefaults() { - const host = useLocalState.getState().host; - const token = useUserState.getState().token; + const { host } = useLocalState.getState(); + const { token } = useUserState.getState(); api.defaults.baseURL = host; api.defaults.timeout = 2500; diff --git a/src/frontend/src/components/barcodes/BarcodeCameraInput.tsx b/src/frontend/src/components/barcodes/BarcodeCameraInput.tsx new file mode 100644 index 0000000000..e219b1de45 --- /dev/null +++ b/src/frontend/src/components/barcodes/BarcodeCameraInput.tsx @@ -0,0 +1,207 @@ +import { t } from '@lingui/macro'; +import { ActionIcon, Container, Group, Select, Stack } from '@mantine/core'; +import { useDocumentVisibility, useLocalStorage } from '@mantine/hooks'; +import { showNotification } from '@mantine/notifications'; +import { + IconCamera, + IconPlayerPlayFilled, + IconPlayerStopFilled, + IconX +} from '@tabler/icons-react'; +import { type CameraDevice, Html5Qrcode } from 'html5-qrcode'; +import { useEffect, useState } from 'react'; +import Expand from '../items/Expand'; +import type { BarcodeInputProps } from './BarcodeInput'; + +export default function BarcodeCameraInput({ + onScan +}: Readonly) { + const [qrCodeScanner, setQrCodeScanner] = useState(null); + const [camId, setCamId] = useLocalStorage({ + key: 'camId', + defaultValue: null + }); + const [cameras, setCameras] = useState([]); + const [cameraValue, setCameraValue] = useState(null); + const [scanningEnabled, setScanningEnabled] = useState(false); + const [wasAutoPaused, setWasAutoPaused] = useState(false); + const documentState = useDocumentVisibility(); + + let lastValue = ''; + + // Mount QR code once we are loaded + useEffect(() => { + setQrCodeScanner(new Html5Qrcode('reader')); + + // load cameras + Html5Qrcode.getCameras().then((devices) => { + if (devices?.length) { + setCameras(devices); + } + }); + }, []); + + // set camera value from id + useEffect(() => { + if (camId) { + setCameraValue(camId.id); + } + }, [camId]); + + // Stop/start when leaving or reentering page + useEffect(() => { + if (scanningEnabled && documentState === 'hidden') { + btnStopScanning(); + setWasAutoPaused(true); + } else if (wasAutoPaused && documentState === 'visible') { + btnStartScanning(); + setWasAutoPaused(false); + } + }, [documentState]); + + // Scanner functions + function onScanSuccess(decodedText: string) { + qrCodeScanner?.pause(); + + // dedouplication + if (decodedText === lastValue) { + qrCodeScanner?.resume(); + return; + } + lastValue = decodedText; + + // submit value upstream + onScan?.(decodedText); + + qrCodeScanner?.resume(); + } + + function onScanFailure(error: string) { + if ( + error != + 'QR code parse error, error = NotFoundException: No MultiFormat Readers were able to detect the code.' + ) { + console.warn(`Code scan error = ${error}`); + } + } + + function btnStartScanning() { + if (camId && qrCodeScanner && !scanningEnabled) { + qrCodeScanner + .start( + camId.id, + { fps: 10, qrbox: { width: 250, height: 250 } }, + (decodedText) => { + onScanSuccess(decodedText); + }, + (errorMessage) => { + onScanFailure(errorMessage); + } + ) + .catch((err: string) => { + showNotification({ + title: t`Error while scanning`, + message: err, + color: 'red', + icon: + }); + }); + setScanningEnabled(true); + } + } + + function btnStopScanning() { + if (qrCodeScanner && scanningEnabled) { + qrCodeScanner.stop().catch((err: string) => { + showNotification({ + title: t`Error while stopping`, + message: err, + color: 'red', + icon: + }); + }); + setScanningEnabled(false); + } + } + + // on value change + useEffect(() => { + if (cameraValue === null) return; + if (cameraValue === camId?.id) { + return; + } + + const cam = cameras.find((cam) => cam.id === cameraValue); + + // stop scanning if cam changed while scanning + if (qrCodeScanner && scanningEnabled) { + // stop scanning + qrCodeScanner.stop().then(() => { + // change ID + setCamId(cam); + // start scanning + qrCodeScanner.start( + cam.id, + { fps: 10, qrbox: { width: 250, height: 250 } }, + (decodedText) => { + onScanSuccess(decodedText); + }, + (errorMessage) => { + onScanFailure(errorMessage); + } + ); + }); + } else { + setCamId(cam); + } + }, [cameraValue]); + + const placeholder = t`Start scanning by selecting a camera and pressing the play button.`; + + return ( + + + + - - {inp} + {t`Barcode Input`} + + - - - Action - + + + + {t`Action`} + {selection.length === 0 ? ( - - No selection - + + Scan and select items to perform actions + ) : ( <> {selection.length} items selected - - General Actions - - - - - - 1} - title={t`Lookup part`} - variant='default' - > - - - - - - - + {SelectedActions} )} - + - - - History - - - - - - + + + + {t`Scanned Items`} + + + { + setSelection(ids); + }} + onItemsDeleted={(ids: string[]) => { + const newHistory = history.filter( + (item) => !ids.includes(item.id) + ); + + historyHandlers.setState(newHistory); + setHistoryStorage(newHistory); + }} + /> + + ); } - -function HistoryTable({ - data, - selection, - setSelection -}: Readonly<{ - data: ScanItem[]; - selection: string[]; - setSelection: React.Dispatch>; -}>) { - const toggleRow = (id: string) => - setSelection((current) => - current.includes(id) - ? current.filter((item) => item !== id) - : [...current, id] - ); - const toggleAll = () => - setSelection((current) => - current.length === data.length ? [] : data.map((item) => item.id) - ); - - const rows = useMemo(() => { - return data.map((item) => { - return ( - - - toggleRow(item.id)} - /> - - - {item.pk && item.model && item.instance ? ( - - ) : ( - item.ref - )} - - {item.model} - {item.source} - {item.timestamp?.toString()} - - ); - }); - }, [data, selection]); - - // rendering - if (data.length === 0) - return ( - - No history - - ); - return ( - - - - - - - - - - - - {rows} -
- 0 && selection.length !== data.length - } - /> - - Item - - Type - - Source - - Scanned at -
-
- ); -} - -// region input stuff -enum InputMethod { - Manual = 'manually', - ImageBarcode = 'imageBarcode' -} - -export interface ScanInputInterface { - action: (items: ScanItem[]) => void; -} - -interface BarcodeInputProps { - action: (decodedText: string) => void; - notScanningPlaceholder?: string; -} - -function InputManual({ action }: Readonly) { - const [value, setValue] = useState(''); - - function btnAddItem() { - if (value === '') return; - - const new_item: ScanItem = { - id: randomId(), - ref: value, - data: { item: value }, - timestamp: new Date(), - source: InputMethod.Manual - }; - action([new_item]); - setValue(''); - } - - function btnAddDummyItem() { - const new_item: ScanItem = { - id: randomId(), - ref: 'Test item', - data: {}, - timestamp: new Date(), - source: InputMethod.Manual - }; - action([new_item]); - } - - return ( - <> - - setValue(event.currentTarget.value)} - onKeyDown={getHotkeyHandler([['Enter', btnAddItem]])} - /> - - - - - - {IS_DEV_OR_DEMO && ( - - )} - - ); -} - -/* Input that uses QR code detection from images */ -export function InputImageBarcode({ - action, - notScanningPlaceholder = t`Start scanning by selecting a camera and pressing the play button.` -}: Readonly) { - const [qrCodeScanner, setQrCodeScanner] = useState(null); - const [camId, setCamId] = useLocalStorage({ - key: 'camId', - defaultValue: null - }); - const [cameras, setCameras] = useState([]); - const [cameraValue, setCameraValue] = useState(null); - const [scanningEnabled, setScanningEnabled] = useState(false); - const [wasAutoPaused, setWasAutoPaused] = useState(false); - const documentState = useDocumentVisibility(); - - let lastValue = ''; - - // Mount QR code once we are loaded - useEffect(() => { - setQrCodeScanner(new Html5Qrcode('reader')); - - // load cameras - Html5Qrcode.getCameras().then((devices) => { - if (devices?.length) { - setCameras(devices); - } - }); - }, []); - - // set camera value from id - useEffect(() => { - if (camId) { - setCameraValue(camId.id); - } - }, [camId]); - - // Stop/start when leaving or reentering page - useEffect(() => { - if (scanningEnabled && documentState === 'hidden') { - btnStopScanning(); - setWasAutoPaused(true); - } else if (wasAutoPaused && documentState === 'visible') { - btnStartScanning(); - setWasAutoPaused(false); - } - }, [documentState]); - - // Scanner functions - function onScanSuccess(decodedText: string) { - qrCodeScanner?.pause(); - - // dedouplication - if (decodedText === lastValue) { - qrCodeScanner?.resume(); - return; - } - lastValue = decodedText; - - // submit value upstream - action(decodedText); - - qrCodeScanner?.resume(); - } - - function onScanFailure(error: string) { - if ( - error != - 'QR code parse error, error = NotFoundException: No MultiFormat Readers were able to detect the code.' - ) { - console.warn(`Code scan error = ${error}`); - } - } - - // button handlers - function btnSelectCamera() { - Html5Qrcode.getCameras() - .then((devices) => { - if (devices?.length) { - setCamId(devices[0]); - } - }) - .catch((err) => { - showNotification({ - title: t`Error while getting camera`, - message: err, - color: 'red', - icon: - }); - }); - } - - function btnStartScanning() { - if (camId && qrCodeScanner && !scanningEnabled) { - qrCodeScanner - .start( - camId.id, - { fps: 10, qrbox: { width: 250, height: 250 } }, - (decodedText) => { - onScanSuccess(decodedText); - }, - (errorMessage) => { - onScanFailure(errorMessage); - } - ) - .catch((err: string) => { - showNotification({ - title: t`Error while scanning`, - message: err, - color: 'red', - icon: - }); - }); - setScanningEnabled(true); - } - } - - function btnStopScanning() { - if (qrCodeScanner && scanningEnabled) { - qrCodeScanner.stop().catch((err: string) => { - showNotification({ - title: t`Error while stopping`, - message: err, - color: 'red', - icon: - }); - }); - setScanningEnabled(false); - } - } - - // on value change - useEffect(() => { - if (cameraValue === null) return; - if (cameraValue === camId?.id) { - return; - } - - const cam = cameras.find((cam) => cam.id === cameraValue); - - // stop scanning if cam changed while scanning - if (qrCodeScanner && scanningEnabled) { - // stop scanning - qrCodeScanner.stop().then(() => { - // change ID - setCamId(cam); - // start scanning - qrCodeScanner.start( - cam.id, - { fps: 10, qrbox: { width: 250, height: 250 } }, - (decodedText) => { - onScanSuccess(decodedText); - }, - (errorMessage) => { - onScanFailure(errorMessage); - } - ); - }); - } else { - setCamId(cam); - } - }, [cameraValue]); - - return ( - - -