Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Barcode scanning #8732

Merged
merged 46 commits into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
68e9cd0
Implement new "general purpose" barcode scan dialog
SchrodingersGat Dec 20, 2024
936d614
Handle scan results
SchrodingersGat Dec 20, 2024
999d215
Fix missing imports
SchrodingersGat Dec 20, 2024
2700126
Handle successful global scan
SchrodingersGat Dec 20, 2024
8720022
Handle error when linking barcode
SchrodingersGat Dec 20, 2024
973dff5
Backend fix for InvenTreeInternalBarcodePlugin
SchrodingersGat Dec 20, 2024
6523463
Error handling
SchrodingersGat Dec 20, 2024
0430dce
Working on scanner input
SchrodingersGat Dec 21, 2024
8e99860
Refactor scan page
SchrodingersGat Dec 21, 2024
d6c28a4
Callback from scanner input
SchrodingersGat Dec 21, 2024
9871ed6
Refactoring <Scan> page
SchrodingersGat Dec 21, 2024
66088f1
Allow InvenTreeTable to be used with supplied data
SchrodingersGat Dec 22, 2024
310073f
Refactor optionalparams
SchrodingersGat Dec 22, 2024
c62af0f
Refactoring table of scan results
SchrodingersGat Dec 22, 2024
6dcecb7
Implement callbacks
SchrodingersGat Dec 22, 2024
5bb88c0
Navigate from barcode table
SchrodingersGat Dec 22, 2024
ac8a9e7
Fix delete callback
SchrodingersGat Dec 22, 2024
a000084
Refactor callbacks
SchrodingersGat Dec 22, 2024
2054c63
Refactor idAccessor
SchrodingersGat Dec 22, 2024
ab034df
prevent duplicate scans
SchrodingersGat Dec 22, 2024
1aa6ac0
Fix for deleting items from table
SchrodingersGat Dec 22, 2024
90c9ded
Cleanup
SchrodingersGat Dec 22, 2024
20567d5
Merge remote-tracking branch 'origin/master' into barcode-scanning
SchrodingersGat Dec 22, 2024
c3be887
Bump API version
SchrodingersGat Dec 22, 2024
6f6d9ca
Merge remote-tracking branch 'origin/master' into barcode-scanning
SchrodingersGat Dec 23, 2024
ad3812d
Merge branch 'master' into barcode-scanning
SchrodingersGat Dec 23, 2024
5e3a3a0
Merge branch 'master' into barcode-scanning
SchrodingersGat Dec 24, 2024
29f412a
Merge branch 'master' of github.com:inventree/InvenTree into barcode-…
SchrodingersGat Dec 24, 2024
d3d9636
Merge branch 'master' into barcode-scanning
SchrodingersGat Dec 26, 2024
0287b13
Adjust playwright tests
SchrodingersGat Dec 26, 2024
a379474
Update playwright tests
SchrodingersGat Dec 26, 2024
cf97bf8
Update barcode screenshots
SchrodingersGat Dec 26, 2024
4000133
Fix links
SchrodingersGat Dec 26, 2024
028c4ee
Add quick links to barcode formats
SchrodingersGat Dec 26, 2024
f2d3dc4
Updated screenshots
SchrodingersGat Dec 26, 2024
7fcbacc
Merge branch 'master' into barcode-scanning
SchrodingersGat Dec 27, 2024
8faca0e
Fix for BuildLineSubTable
SchrodingersGat Dec 27, 2024
eab2b6f
Specify idAccessor values
SchrodingersGat Dec 27, 2024
cf853b7
Clear barcode input after timeout period
SchrodingersGat Dec 27, 2024
88cd351
Move items
SchrodingersGat Dec 27, 2024
0c57615
Fix for playwright test
SchrodingersGat Dec 27, 2024
b3f4809
Remove debug print
SchrodingersGat Dec 27, 2024
eaf3c3f
Additional error ignores
SchrodingersGat Dec 28, 2024
d2d39b2
Cleanup scanner input
SchrodingersGat Dec 28, 2024
44b0e8b
Playwright test adjustments
SchrodingersGat Dec 28, 2024
b441e3f
Merge branch 'master' into barcode-scanning
SchrodingersGat Dec 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/docs/assets/images/barcode/barcode_link_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/docs/assets/images/barcode/barcode_link_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/docs/assets/images/barcode/barcode_no_match.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/docs/assets/images/barcode/barcode_scan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 40 additions & 15 deletions docs/docs/barcodes/barcodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,74 @@ 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:

- [Part](../part/part.md#part)
- [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)

## Web Integration
### Configuration Options

Barcode scanning can be enabled within the web interface. Barcode scanning in the web interface supports scanning via:
The barcode system can be configured via the [global settings](../settings/global.md#barcodes).

- Keyboard style scanners (e.g. USB connected)
- Webcam (image processing)
## Web Integration

### Configuration
Barcode scanning can be enabled within the web interface. This allows users to scan barcodes directly from the web browser.

Barcode scanning may need to be enabled for the web interface:
### Input Modes

{% with id="barcode_config", url="barcode/barcode_settings.png", description="Barcode settings" %}
{% include 'img.html' %}
{% endwith %}
The following barcode input modes are supported by the web interface:

### Scanning
- **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

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:
### Quick Scan

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

Expand Down
6 changes: 5 additions & 1 deletion docs/docs/barcodes/external.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/settings/global.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,15 @@ def scan(self, barcode_data):

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})
3 changes: 2 additions & 1 deletion src/backend/InvenTree/stock/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,8 +902,9 @@ def get_serializer(self, *args, **kwargs):
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',
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
207 changes: 207 additions & 0 deletions src/frontend/src/components/barcodes/BarcodeCameraInput.tsx
Original file line number Diff line number Diff line change
@@ -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<BarcodeInputProps>) {
const [qrCodeScanner, setQrCodeScanner] = useState<Html5Qrcode | null>(null);
const [camId, setCamId] = useLocalStorage<CameraDevice | null>({
key: 'camId',
defaultValue: null
});
const [cameras, setCameras] = useState<any[]>([]);
const [cameraValue, setCameraValue] = useState<string | null>(null);
const [scanningEnabled, setScanningEnabled] = useState<boolean>(false);
const [wasAutoPaused, setWasAutoPaused] = useState<boolean>(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: <IconX />
});
});
setScanningEnabled(true);
}
}

function btnStopScanning() {
if (qrCodeScanner && scanningEnabled) {
qrCodeScanner.stop().catch((err: string) => {
showNotification({
title: t`Error while stopping`,
message: err,
color: 'red',
icon: <IconX />
});
});
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 (
<Stack gap='xs'>
<Group gap='xs' preventGrowOverflow>
<Expand>
<Select
leftSection={<IconCamera />}
value={cameraValue}
onChange={setCameraValue}
data={cameras.map((device) => {
return { value: device.id, label: device.label };
})}
/>
</Expand>

{scanningEnabled ? (
<ActionIcon
size='lg'
color='red'
onClick={btnStopScanning}
title={t`Stop scanning`}
variant='transparent'
>
<IconPlayerStopFilled />
</ActionIcon>
) : (
<ActionIcon
size='lg'
color='green'
onClick={btnStartScanning}
title={t`Start scanning`}
disabled={!camId}
variant='transparent'
>
<IconPlayerPlayFilled />
</ActionIcon>
)}
</Group>
{scanningEnabled ? (
<Container px={0} id='reader' w={'100%'} mih='300px' />
) : (
<Container px={0} id='reader' w={'100%'}>
{placeholder}
</Container>
)}
</Stack>
);
}
Loading
Loading