[REF] point_of_sale,pos_*: convert barcode_reader to service

**Notable feature changes**

- PoS app continues to load even if no nomenclature_id is configured.
  - When scanning, an error popup is shown to the user mentioning about
    the misconfiguration.
- We introduce a default error handler that shows the ErrorBarcodePopup
  when there are no registered barcode handlers via the
  `useBarcodeReader` hook.

closes odoo/odoo#115906

Related: odoo/enterprise#38444
Signed-off-by: Samuel Degueldre <sad@odoo.com>
This commit is contained in:
Joseph Caburnay
2023-03-24 03:28:42 +01:00
parent 3081278abc
commit 73b9d48703
15 changed files with 185 additions and 249 deletions
+1
View File
@@ -55,6 +55,7 @@ class PosController(PortalAccount):
session_info = request.env['ir.http'].session_info()
session_info['user_context']['allowed_company_ids'] = company.ids
session_info['user_companies'] = {'current_company': company.id, 'allowed_companies': {company.id: session_info['user_companies']['allowed_companies'][company.id]}}
session_info['nomenclature_id'] = pos_session.company_id.nomenclature_id.id
context = {
'session_info': session_info,
'login_number': pos_session.login(),
@@ -0,0 +1,18 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
import { useComponent, useEffect } from "@odoo/owl";
export function useBarcodeReader(callbackMap, exclusive = false) {
const current = useComponent();
const barcodeReader = useService("barcode_reader");
if (barcodeReader) {
for (const [key, callback] of Object.entries(callbackMap)) {
callbackMap[key] = callback.bind(current);
}
useEffect(
() => barcodeReader.register(callbackMap, exclusive),
() => []
);
}
}
@@ -0,0 +1,142 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { Mutex } from "@web/core/utils/concurrency";
import { session } from "@web/session";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { ErrorBarcodePopup } from "@point_of_sale/js/Popups/ErrorBarcodePopup";
import BarcodeParser from "barcodes.BarcodeParser";
export class BarcodeReader {
constructor({ parser, popup }) {
this.parser = parser;
this.popup = popup;
this.setup();
}
setup() {
this.mutex = new Mutex();
this.cbMaps = new Set();
// FIXME POSREF: When LoginScreen becomes a normal screen, we can remove this exclusive callback handling.
this.exclusiveCbMap = null;
this.remoteScanning = false;
this.remoteActive = 0;
}
register(cbMap, exclusive) {
if (exclusive) {
this.exclusiveCbMap = cbMap;
} else {
this.cbMaps.add(cbMap);
}
return () => {
if (exclusive) {
this.exclusiveCbMap = null;
} else {
this.cbMaps.delete(cbMap);
}
};
}
scan(code) {
return this.mutex.exec(() => this._scan(code));
}
async _scan(code) {
if (!code) {
return;
}
const cbMaps = this.exclusiveCbMap ? [this.exclusiveCbMap] : [...this.cbMaps];
let parsedResult = this.parser.parse_barcode(code);
if (!Array.isArray(parsedResult)) {
parsedResult = [parsedResult];
}
for (const parseBarcode of parsedResult) {
const cbs = cbMaps.map((cbMap) => cbMap[parseBarcode.type]).filter(Boolean);
if (cbs.length === 0) {
this.popup.add(ErrorBarcodePopup, { code: this.codeRepr(parseBarcode) });
}
for (const cb of cbs) {
await cb(parseBarcode);
}
}
}
codeRepr(parsedBarcode) {
if (parsedBarcode.code.length > 32) {
return parsedBarcode.code.substring(0, 29) + "...";
} else {
return parsedBarcode.code;
}
}
// the barcode scanner will listen on the hw_proxy/scanner interface for
// scan events until disconnectFromProxy is called
connectToProxy(hwProxy) {
this.remoteScanning = true;
if (this.remoteActive >= 1) {
return;
}
this.remoteActive = 1;
this.waitForBarcode(hwProxy);
}
async waitForBarcode(hwProxy) {
try {
const barcode = await hwProxy.connection.rpc(
"/hw_proxy/scanner",
{},
{ shadow: true, timeout: 7500 }
);
if (!this.remoteScanning) {
this.remoteActive = 0;
return;
}
this.scan(barcode);
} catch {
if (!this.remoteScanning) {
this.remoteActive = 0;
return;
}
}
this.waitForBarcode();
}
// the barcode scanner will stop listening on the hw_proxy/scanner remote interface
disconnectFromProxy() {
this.remoteScanning = false;
}
}
export const barcodeReader = {
dependencies: ["barcode", "popup"],
async start(env, { barcode, popup }) {
let barcodeReader = null;
if (session.nomenclature_id) {
const parser = new BarcodeParser({ nomenclature_id: [session.nomenclature_id] });
await parser.is_loaded();
barcodeReader = new BarcodeReader({ parser, popup });
}
barcode.bus.addEventListener("barcode_scanned", (ev) => {
if (barcodeReader) {
barcodeReader.scan(ev.detail.barcode);
} else {
popup.add(ErrorPopup, {
title: env._t("Unable to parse barcode"),
body: env._t(
"No barcode nomenclature has been configured. This can be changed in the configuration settings."
),
});
}
});
return barcodeReader;
},
};
registry.category("services").add("barcode_reader", barcodeReader);
@@ -17,6 +17,7 @@ export class DebugWidget extends Component {
super.setup();
this.debug = useService("debug");
this.popup = useService("popup");
this.barcodeReader = useService("barcode_reader");
const numberBuffer = useService("number_buffer");
useBus(numberBuffer, "buffer-update", this._onBufferUpdate);
this.state = useState({
@@ -87,14 +88,18 @@ export class DebugWidget extends Component {
this.env.proxy.debug_reset_weight();
}
async barcodeScan() {
await this.env.barcode_reader.scan(this.state.barcodeInput);
if (!this.barcodeReader) {
return;
}
await this.barcodeReader.scan(this.state.barcodeInput);
}
async barcodeScanEAN() {
const ean = this.env.barcode_reader.barcode_parser.sanitize_ean(
this.state.barcodeInput || "0"
);
if (!this.barcodeReader) {
return;
}
const ean = this.barcodeReader.parser.sanitize_ean(this.state.barcodeInput || "0");
this.state.barcodeInput = ean;
await this.env.barcode_reader.scan(ean);
await this.barcodeReader.scan(ean);
}
async deleteOrders() {
const { confirmed } = await this.popup.add(ConfirmPopup, {
@@ -22,7 +22,7 @@
<p class="category">Barcode Scanner</p>
<ul>
<li>
<input t-model="state.barcodeInput" type="text" class="ean"></input>
<input t-model="state.barcodeInput" type="text" class="ean" t-att-disabled="!barcodeReader" t-attf-placeholder="{{ !barcodeReader ? 'No nomenclature' : '' }}"/>
</li>
<li class="button barcode" t-on-click="barcodeScan">Scan</li>
<li class="button custom_ean" t-on-click="barcodeScanEAN">Scan EAN-13</li>
@@ -21,12 +21,13 @@ export class PosStore extends Reactive {
legacyEnv = legacyEnv;
globalState = new PosGlobalState({ env: markRaw(legacyEnv) });
static serviceDependencies = ["popup", "orm", "number_buffer"];
constructor({ popup, orm, number_buffer }) {
static serviceDependencies = ["popup", "orm", "number_buffer", "barcode_reader"];
constructor({ popup, orm, number_buffer, barcode_reader }) {
super();
this.orm = orm;
this.popup = popup;
this.numberBuffer = number_buffer;
this.barcodeReader = barcode_reader;
this.setup();
}
// use setup instead of constructor because setup can be patched.
@@ -46,9 +47,9 @@ export class PosStore extends Reactive {
this.showScreen(screenName);
}
connect_to_proxy() {
connectToProxy() {
return new Promise((resolve, reject) => {
this.globalState.env.barcode_reader.disconnect_from_proxy();
this.barcodeReader?.disconnectFromProxy();
this.globalState.loadingSkipButtonIsShown = true;
this.globalState.env.proxy
.autoconnect({
@@ -58,7 +59,7 @@ export class PosStore extends Reactive {
.then(
() => {
if (this.globalState.config.iface_scan_via_proxy) {
this.globalState.env.barcode_reader.connect_to_proxy();
this.barcodeReader?.connectToProxy(this.globalState.env.proxy);
}
resolve();
},
+1 -20
View File
@@ -2,7 +2,6 @@
import { loadCSS } from "@web/core/assets";
import { useService } from "@web/core/utils/hooks";
import BarcodeParser from "barcodes.BarcodeParser";
import { batched } from "@point_of_sale/js/utils";
import { debounce } from "@web/core/utils/timing";
import { Transition } from "@web/core/transition";
@@ -139,9 +138,8 @@ export class Chrome extends Component {
try {
await this.env.pos.load_server_data();
await this.setupBarcodeParser();
if (this.env.pos.config.use_proxy) {
await this.pos.connect_to_proxy();
await this.pos.connectToProxy();
}
// Load the saved `env.pos.toRefundLines` from localStorage when
// the PosGlobalState is ready.
@@ -219,23 +217,6 @@ export class Chrome extends Component {
}
}
setupBarcodeParser() {
if (!this.env.pos.company.nomenclature_id) {
const errorMessage = this.env._t(
"The barcode nomenclature setting is not configured. " +
"Make sure to configure it on your Point of Sale configuration settings"
);
throw new Error(this.env._t("Missing barcode nomenclature"), {
cause: { message: errorMessage },
});
}
const barcode_parser = new BarcodeParser({
nomenclature_id: this.env.pos.company.nomenclature_id,
});
this.env.barcode_reader.set_barcode_parser(barcode_parser);
return barcode_parser.is_loaded();
}
/**
* Save `env.pos.toRefundLines` in localStorage on beforeunload - closing the
* browser, reloading or going to other page.
@@ -3,12 +3,11 @@
import { ControlButtonsMixin } from "@point_of_sale/js/ControlButtonsMixin";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { useBarcodeReader } from "@point_of_sale/js/custom_hooks";
import { useBarcodeReader } from "@point_of_sale/app/barcode_reader_hook";
import { parse } from "web.field_utils";
import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { ErrorBarcodePopup } from "@point_of_sale/js/Popups/ErrorBarcodePopup";
import { ControlButtonPopup } from "@point_of_sale/js/Popups/ControlButtonPopup";
import { ConnectionLostError } from "@web/core/network/rpc_service";
@@ -49,7 +48,6 @@ export class ProductScreen extends ControlButtonsMixin(Component) {
price: this._barcodeProductAction,
client: this._barcodePartnerAction,
discount: this._barcodeDiscountAction,
error: this._barcodeErrorAction,
});
this.state = useState({
mobile_pane: this.props.mobile_pane || "right",
@@ -186,19 +184,6 @@ export class ProductScreen extends ControlButtonsMixin(Component) {
last_orderline.set_discount(code.value);
}
}
// IMPROVEMENT: The following two methods should be in PosScreenComponent?
// Why? Because once we start declaring barcode actions in different
// screens, these methods will also be declared over and over.
_barcodeErrorAction(code) {
this.popup.add(ErrorBarcodePopup, { code: this._codeRepr(code) });
}
_codeRepr(code) {
if (code.code.length > 32) {
return code.code.substring(0, 29) + "...";
} else {
return code.code;
}
}
async _displayAllControlPopup() {
await this.popup.add(ControlButtonPopup, {
controlButtons: this.controlButtons,
@@ -1,165 +0,0 @@
/** @odoo-module */
import concurrency from "web.concurrency";
import core from "web.core";
var Mutex = concurrency.Mutex;
// this module interfaces with the barcode reader. It assumes the barcode reader
// is set-up to act like a keyboard. Use connect() and disconnect() to activate
// and deactivate the barcode reader. Use set_action_callbacks to tell it
// what to do when it reads a barcode.
export const BarcodeReader = core.Class.extend({
actions: ["product", "cashier", "client"],
init: function (attributes) {
this.mutex = new Mutex();
this.action_callbacks = {};
this.exclusive_callbacks = {};
this.proxy = attributes.proxy;
this.env = attributes.env;
this.remote_scanning = false;
this.remote_active = 0;
this.barcode_parser = attributes.barcode_parser;
this.action_callback_stack = [];
core.bus.on("barcode_scanned", this, function (barcode) {
// use mutex to make sure scans are done one after the other
this.mutex.exec(async () => {
await this.scan(barcode);
});
});
},
set_barcode_parser: function (barcode_parser) {
this.barcode_parser = barcode_parser;
},
// when a barcode is scanned and parsed, the callback corresponding
// to its type is called with the parsed_barcode as a parameter.
// (parsed_barcode is the result of parse_barcode(barcode))
//
// callbacks is a Map of 'actions' : callback(parsed_barcode)
// that sets the callback for each action. if a callback for the
// specified action already exists, it is replaced.
//
// possible actions include :
// 'product' | 'cashier' | 'client' | 'discount'
set_action_callback: function (name, callback) {
if (this.action_callbacks[name]) {
this.action_callbacks[name].add(callback);
} else {
this.action_callbacks[name] = new Set([callback]);
}
},
remove_action_callback: function (name, callback) {
if (!callback) {
delete this.action_callbacks[name];
return;
}
const callbacks = this.action_callbacks[name];
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
delete this.action_callbacks[name];
}
}
},
/**
* Allow setting of exclusive callbacks. If there are exclusive callbacks,
* these callbacks are called neglecting the regular callbacks. This is
* useful for rendered Components that wants to take exclusive access
* to the barcode reader.
*
* @param {String} name
* @param {Function} callback function that takes parsed barcode
*/
set_exclusive_callback: function (name, callback) {
if (this.exclusive_callbacks[name]) {
this.exclusive_callbacks[name].add(callback);
} else {
this.exclusive_callbacks[name] = new Set([callback]);
}
},
remove_exclusive_callback: function (name, callback) {
if (!callback) {
delete this.exclusive_callbacks[name];
return;
}
const callbacks = this.exclusive_callbacks[name];
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
delete this.exclusive_callbacks[name];
}
}
},
scan: async function (code) {
if (!code) {
return;
}
const callbacks = Object.keys(this.exclusive_callbacks).length
? this.exclusive_callbacks
: this.action_callbacks;
let parsed_results = this.barcode_parser.parse_barcode(code);
if (!Array.isArray(parsed_results)) {
parsed_results = [parsed_results];
}
for (const parsed_result of parsed_results) {
if (callbacks[parsed_result.type]) {
for (const cb of callbacks[parsed_result.type]) {
await cb(parsed_result);
}
} else if (callbacks.error) {
[...callbacks.error].map((cb) => cb(parsed_result));
} else {
console.warn("Ignored Barcode Scan:", parsed_result);
}
}
},
// the barcode scanner will listen on the hw_proxy/scanner interface for
// scan events until disconnect_from_proxy is called
connect_to_proxy: function () {
var self = this;
this.remote_scanning = true;
if (this.remote_active >= 1) {
return;
}
this.remote_active = 1;
function waitforbarcode() {
return self.proxy.connection
.rpc("/hw_proxy/scanner", {}, { shadow: true, timeout: 7500 })
.then(
function (barcode) {
if (!self.remote_scanning) {
self.remote_active = 0;
return;
}
self.scan(barcode);
waitforbarcode();
},
function () {
if (!self.remote_scanning) {
self.remote_active = 0;
return;
}
waitforbarcode();
}
);
}
waitforbarcode();
},
// the barcode scanner will stop listening on the hw_proxy/scanner remote interface
disconnect_from_proxy: function () {
this.remote_scanning = false;
},
});
@@ -3,7 +3,7 @@ import { OfflineErrorPopup } from "./Popups/OfflineErrorPopup";
import { ConfirmPopup } from "./Popups/ConfirmPopup";
import { ErrorTracebackPopup } from "./Popups/ErrorTracebackPopup";
import { ErrorPopup } from "./Popups/ErrorPopup";
import { useEnv, onMounted, onPatched, onWillUnmount, useComponent } from "@odoo/owl";
import { useEnv, onMounted, onPatched, useComponent } from "@odoo/owl";
/**
* Introduce error handlers in the component.
@@ -90,33 +90,3 @@ export function useAutoFocusToLast() {
onMounted(autofocus);
onPatched(autofocus);
}
export function useBarcodeReader(callbackMap, exclusive = false) {
const current = useComponent();
const barcodeReader = current.env.barcode_reader;
for (const [key, callback] of Object.entries(callbackMap)) {
callbackMap[key] = callback.bind(current);
}
onMounted(() => {
if (barcodeReader) {
for (const key in callbackMap) {
if (exclusive) {
barcodeReader.set_exclusive_callback(key, callbackMap[key]);
} else {
barcodeReader.set_action_callback(key, callbackMap[key]);
}
}
}
});
onWillUnmount(() => {
if (barcodeReader) {
for (const key in callbackMap) {
if (exclusive) {
barcodeReader.remove_exclusive_callback(key, callbackMap[key]);
} else {
barcodeReader.remove_action_callback(key, callbackMap[key]);
}
}
}
});
}
@@ -6,7 +6,6 @@
import env from "web.env";
import concurrency from "web.concurrency";
import { JobQueue, ProxyDevice } from "@point_of_sale/js/devices";
import { BarcodeReader } from "@point_of_sale/js/barcode_reader";
// Create new env object base on web.env.
// Mutating this new object won't affect the original object.
@@ -14,5 +13,4 @@ export const pos_env = Object.create(env);
pos_env.proxy_queue = new JobQueue(); // used to prevent parallels communications to the proxy
pos_env.proxy = new ProxyDevice({ env: pos_env }); // used to communicate to the hardware devices via a local proxy
pos_env.barcode_reader = new BarcodeReader({ env: pos_env, proxy: pos_env.proxy });
pos_env.posMutex = new concurrency.Mutex();
@@ -8,7 +8,7 @@ import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { useService } from "@web/core/utils/hooks";
import { useEnv } from "@odoo/owl";
import { useBarcodeReader } from "@point_of_sale/js/custom_hooks";
import { useBarcodeReader } from "@point_of_sale/app/barcode_reader_hook";
export function useCashierSelector(
{ onCashierChanged, exclusive } = { onCashierChanged: () => {}, exclusive: false }
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import { useBarcodeReader } from "@point_of_sale/js/custom_hooks";
import { useBarcodeReader } from "@point_of_sale/app/barcode_reader_hook";
import { patch } from "@web/core/utils/patch";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
import { useService } from "@web/core/utils/hooks";
@@ -3,7 +3,7 @@
import { _t } from "web.core";
import { patch } from "@web/core/utils/patch";
import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen";
import { useBarcodeReader } from "@point_of_sale/js/custom_hooks";
import { useBarcodeReader } from "@point_of_sale/app/barcode_reader_hook";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { PaymentTransactionPopup } from "./PaymentTransactionPopup";
import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup";
@@ -1,7 +1,7 @@
/** @odoo-module */
import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import { useBarcodeReader } from "@point_of_sale/js/custom_hooks";
import { useBarcodeReader } from "@point_of_sale/app/barcode_reader_hook";
import { patch } from "@web/core/utils/patch";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";