diff --git a/addons/web/static/img/barcode.svg b/addons/web/static/img/barcode.svg
new file mode 100644
index 00000000000..76dbd06cf2a
--- /dev/null
+++ b/addons/web/static/img/barcode.svg
@@ -0,0 +1,20 @@
+
+
diff --git a/addons/web/static/src/webclient/barcode/barcode_scanner.js b/addons/web/static/src/webclient/barcode/barcode_scanner.js
new file mode 100644
index 00000000000..12909d71fab
--- /dev/null
+++ b/addons/web/static/src/webclient/barcode/barcode_scanner.js
@@ -0,0 +1,278 @@
+/** @odoo-module **/
+/* global BarcodeDetector */
+
+import Dialog from "web.OwlDialog";
+import { delay } from "web.concurrency";
+import { loadJS, templates } from "@web/core/assets";
+
+const { App, Component, EventBus, onMounted, onWillStart, onWillUnmount, useRef } = owl;
+import { _t } from "web.core";
+const bus = new EventBus();
+const busOk = "BarcodeDialog-Ok";
+const busError = "BarcodeDialog-Error";
+
+/**
+ * Check for HTMLVideoElement readiness.
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
+ */
+const HAVE_NOTHING = 0;
+const HAVE_METADATA = 1;
+function isVideoElementReady(video) {
+ return ![HAVE_NOTHING, HAVE_METADATA].includes(video.readyState);
+}
+
+/**
+ * Builder for BarcodeDetector-like polyfill class using ZXing library.
+ *
+ * @param {ZXing} ZXing Zxing library
+ * @returns {class} ZxingBarcodeDetector class
+ */
+function buildZXingBarcodeDetector(ZXing) {
+ const ZXingFormats = new Map([
+ ["aztec", ZXing.BarcodeFormat.AZTEC],
+ ["code_39", ZXing.BarcodeFormat.CODE_39],
+ ["code_128", ZXing.BarcodeFormat.CODE_128],
+ ["data_matrix", ZXing.BarcodeFormat.DATA_MATRIX],
+ ["ean_8", ZXing.BarcodeFormat.EAN_8],
+ ["ean_13", ZXing.BarcodeFormat.EAN_13],
+ ["itf", ZXing.BarcodeFormat.ITF],
+ ["pdf417", ZXing.BarcodeFormat.PDF_417],
+ ["qr_code", ZXing.BarcodeFormat.QR_CODE],
+ ["upc_a", ZXing.BarcodeFormat.UPC_A],
+ ["upc_e", ZXing.BarcodeFormat.UPC_E],
+ ]);
+
+ const allSupportedFormats = Array.from(ZXingFormats.keys());
+
+ /**
+ * ZXingBarcodeDetector class
+ *
+ * BarcodeDetector-like polyfill class using ZXing library.
+ * API follows the Shape Detection Web API (specifically Barcode Detection).
+ */
+ class ZXingBarcodeDetector {
+ /**
+ * @param {object} opts
+ * @param {Array} opts.formats list of codes' formats to detect
+ */
+ constructor(opts = {}) {
+ const formats = opts.formats || allSupportedFormats;
+ const hints = new Map([
+ [
+ ZXing.DecodeHintType.POSSIBLE_FORMATS,
+ formats.map((format) => ZXingFormats.get(format)),
+ ],
+ // Enable Scanning at 90 degrees rotation
+ // https://github.com/zxing-js/library/issues/291
+ [ZXing.DecodeHintType.TRY_HARDER, true],
+ ]);
+ this.reader = new ZXing.MultiFormatReader();
+ this.reader.setHints(hints);
+ }
+
+ /**
+ * Detect codes in image.
+ *
+ * @param {HTMLVideoElement} video source video element
+ * @returns {Promise} array of detected codes
+ */
+ async detect(video) {
+ if (!(video instanceof HTMLVideoElement)) {
+ throw new DOMException(
+ "imageDataFrom() requires an HTMLVideoElement",
+ "InvalidArgumentError"
+ );
+ }
+ if (!isVideoElementReady(video)) {
+ throw new DOMException("HTMLVideoElement is not ready", "InvalidStateError");
+ }
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+
+ ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
+
+ const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(canvas);
+ const binaryBitmap = new ZXing.BinaryBitmap(new ZXing.HybridBinarizer(luminanceSource));
+ try {
+ const result = this.reader.decode(binaryBitmap);
+ const format = Array.from(ZXingFormats).find(
+ ([k, val]) => val === result.getBarcodeFormat()
+ );
+ const rawValue = result.getText();
+ return [
+ {
+ format,
+ rawValue,
+ },
+ ];
+ } catch (err) {
+ if (err.name === "NotFoundException") {
+ return [];
+ }
+ throw err;
+ }
+ }
+ }
+
+ /**
+ * Supported codes formats
+ *
+ * @static
+ * @returns {Array}
+ */
+ ZXingBarcodeDetector.getSupportedFormats = async () => allSupportedFormats;
+
+ return ZXingBarcodeDetector;
+}
+class BarcodeDialog extends Component {
+ /**
+ * @override
+ */
+ setup() {
+ this.videoPreviewRef = useRef("videoPreview");
+ this.interval = null;
+ this.stream = null;
+ this.detector = null;
+
+ onWillStart(async () => {
+ let DetectorClass;
+ // Use Barcode Detection API if available.
+ // As support is still bleeding edge (mainly Chrome on Android),
+ // also provides a fallback using ZXing library.
+ if ("BarcodeDetector" in window) {
+ DetectorClass = BarcodeDetector;
+ } else {
+ await loadJS("/web/static/lib/zxing-library/zxing-library.js");
+ DetectorClass = buildZXingBarcodeDetector(window.ZXing);
+ }
+ const formats = await DetectorClass.getSupportedFormats();
+ this.detector = new DetectorClass({ formats });
+ });
+
+ onMounted(async () => {
+ const constraints = {
+ video: { facingMode: this.props.facingMode },
+ audio: false,
+ };
+
+ try {
+ this.stream = await navigator.mediaDevices.getUserMedia(constraints);
+ } catch (err) {
+ const errors = {
+ NotFoundError: _t("No device can be found."),
+ NotAllowedError: _t("Odoo needs your authorization first."),
+ };
+ const errorMessage =
+ _t("Could not start scanning. ") + (errors[err.name] || err.message);
+ this.onError(new Error(errorMessage));
+ return;
+ }
+ this.videoPreviewRef.el.srcObject = this.stream;
+ await this.isVideoReady();
+ this.interval = setInterval(this.detectCode.bind(this), 100);
+ });
+
+ onWillUnmount(() => {
+ clearInterval(this.interval);
+ this.interval = null;
+ if (this.stream) {
+ this.stream.getTracks().forEach((track) => track.stop());
+ this.stream = null;
+ }
+ });
+ }
+
+ /**
+ * Check for camera preview element readiness
+ *
+ * @returns {Promise} resolves when the video element is ready
+ */
+ async isVideoReady() {
+ // FIXME: even if it shouldn't happened, a timeout could be useful here.
+ return new Promise(async (resolve) => {
+ while (!isVideoElementReady(this.videoPreviewRef.el)) {
+ await delay(10);
+ }
+ resolve();
+ });
+ }
+
+ /**
+ * Detection success handler
+ *
+ * @param {string} result found code
+ */
+ onResult(result) {
+ this.props.onClose();
+ bus.trigger(busOk, result);
+ }
+
+ /**
+ * Detection error handler
+ *
+ * @param {Error} error
+ */
+ onError(error) {
+ this.props.onClose();
+ bus.trigger(busError, { error });
+ }
+
+ /**
+ * Attempt to detect codes in the current camera preview's frame
+ */
+ async detectCode() {
+ try {
+ const codes = await this.detector.detect(this.videoPreviewRef.el);
+ if (codes.length === 0) {
+ return;
+ }
+ this.onResult(codes[0].rawValue);
+ } catch (err) {
+ this.onError(err);
+ }
+ }
+}
+
+Object.assign(BarcodeDialog, {
+ components: {
+ Dialog,
+ },
+ template: "web.BarcodeDialog",
+});
+
+/**
+ * Check for BarcodeScanner support
+ * @returns {boolean}
+ */
+export function isBarcodeScannerSupported() {
+ return navigator.mediaDevices && navigator.mediaDevices.getUserMedia;
+}
+
+/**
+ * Opens the BarcodeScanning dialog and begins code detection using the device's camera.
+ *
+ * @returns {Promise} resolves when a {qr,bar}code has been detected
+ */
+export async function scanBarcode(facingMode = "environment") {
+ const promise = new Promise((resolve, reject) => {
+ bus.on(busOk, null, resolve);
+ bus.on(busError, null, reject);
+ });
+ const appForBarcodeDialog = new App(BarcodeDialog, {
+ env: owl.Component.env,
+ dev: owl.Component.env.isDebug(),
+ templates,
+ translatableAttributes: ["data-tooltip"],
+ translateFn: _t,
+ props: {
+ onClose: () => appForBarcodeDialog.destroy(),
+ facingMode: facingMode,
+ },
+ });
+ await appForBarcodeDialog.mount(document.body);
+ return promise;
+}
diff --git a/addons/web/static/src/webclient/barcode/barcode_scanner.xml b/addons/web/static/src/webclient/barcode/barcode_scanner.xml
new file mode 100644
index 00000000000..fb004627c02
--- /dev/null
+++ b/addons/web/static/src/webclient/barcode/barcode_scanner.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/addons/web/static/src/webclient/icons.scss b/addons/web/static/src/webclient/icons.scss
index ecf1dd41680..9636a8f6dd9 100644
--- a/addons/web/static/src/webclient/icons.scss
+++ b/addons/web/static/src/webclient/icons.scss
@@ -69,3 +69,12 @@ $oi-sizes: (
.oi-pulse {
animation: fa-spin 1s infinite steps(8);
}
+
+.o_barcode {
+ -webkit-mask: url('/web/static/img/barcode.svg') center/contain no-repeat;
+ mask: url('/web/static/img/barcode.svg') center/contain no-repeat;
+ background-color: $o-brand-primary;
+ width: 26px;
+ margin-left: 10px;
+ border: 0;
+}