From 25cbd9c4e7d945cce8b055047694df06fb4f884a Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Thu, 9 Mar 2023 08:03:21 +0000 Subject: [PATCH] [REF] point_of_sale, pos*: convert ProxyDevice to ES6 class and service *: pos_epson_printer, pos_restaurant, pos_six This commit continues the work of reducing the reliance of the pos modules on legacy features from the JS framework, such as core.Class and mixins, by converting the ProxyDevice class to ES6 (and renaming it HardwareProxy, to avoid confusion with DeviceProxy from iot). It also makes it available as a service in the new environment so that we can hopefully get rid of the legacy environment in the near future. closes odoo/odoo#114821 Related: odoo/enterprise#37991 Signed-off-by: Joseph Caburnay (jcb) --- .../static/src/app/barcode_reader_service.js | 38 +- .../static/src/app/debug/debug_widget.js | 37 +- .../src/app/hardware_proxy/hardware_proxy.js | 228 ++++++++ .../navbar/cash_move_popup/cash_move_popup.js | 5 +- .../static/src/app/navbar/navbar.js | 1 + .../static/src/app/navbar/navbar.xml | 2 +- .../point_of_sale/static/src/app/pos_store.js | 83 +-- addons/point_of_sale/static/src/js/Chrome.js | 5 +- .../CustomerFacingDisplayButton.js | 30 +- .../src/js/ChromeWidgets/ProxyStatus.js | 97 ++-- .../src/js/ChromeWidgets/SaleDetailsButton.js | 9 +- .../src/js/ChromeWidgets/SyncNotification.js | 2 +- .../src/js/Misc/AbstractReceiptScreen.js | 5 +- .../static/src/js/Popups/ClosePosPopup.js | 1 + .../js/Screens/PaymentScreen/PaymentScreen.js | 5 +- .../js/Screens/ProductScreen/ProductScreen.js | 8 +- .../js/Screens/ReceiptScreen/ReceiptScreen.js | 2 +- .../src/js/Screens/ScaleScreen/ScaleScreen.js | 17 +- .../TicketScreen/ReprintReceiptScreen.js | 2 +- addons/point_of_sale/static/src/js/devices.js | 488 ------------------ addons/point_of_sale/static/src/js/models.js | 109 ++-- addons/point_of_sale/static/src/js/pos_env.js | 16 - .../point_of_sale/static/src/xml/Chrome.xml | 3 - .../src/xml/ChromeWidgets/ProxyStatus.xml | 20 +- .../static/src/xml/Popups/ClosePosPopup.xml | 2 +- .../static/src/js/pos_epson_printer.js | 2 +- .../static/src/js/Screens/TipScreen.js | 5 +- .../static/src/xml/Screens/TipScreen.xml | 2 +- addons/pos_six/static/src/js/payment_six.js | 4 +- 29 files changed, 456 insertions(+), 772 deletions(-) create mode 100644 addons/point_of_sale/static/src/app/hardware_proxy/hardware_proxy.js delete mode 100644 addons/point_of_sale/static/src/js/devices.js delete mode 100644 addons/point_of_sale/static/src/js/pos_env.js diff --git a/addons/point_of_sale/static/src/app/barcode_reader_service.js b/addons/point_of_sale/static/src/app/barcode_reader_service.js index 599922f80b9..c8bdc56bcc5 100644 --- a/addons/point_of_sale/static/src/app/barcode_reader_service.js +++ b/addons/point_of_sale/static/src/app/barcode_reader_service.js @@ -8,9 +8,11 @@ import { ErrorBarcodePopup } from "@point_of_sale/js/Popups/ErrorBarcodePopup"; import BarcodeParser from "barcodes.BarcodeParser"; export class BarcodeReader { - constructor({ parser, popup }) { + static serviceDependencies = ["popup", "hardware_proxy"]; + constructor(parser, { popup, hardware_proxy }) { this.parser = parser; this.popup = popup; + this.hardwareProxy = hardware_proxy; this.setup(); } @@ -75,33 +77,22 @@ export class BarcodeReader { // the barcode scanner will listen on the hw_proxy/scanner interface for // scan events until disconnectFromProxy is called - connectToProxy(hwProxy) { + connectToProxy() { this.remoteScanning = true; if (this.remoteActive >= 1) { return; } this.remoteActive = 1; - this.waitForBarcode(hwProxy); + this.waitForBarcode(); } - 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; - } + async waitForBarcode() { + const barcode = await this.hardwareProxy.message("scanner").catch(() => {}); + if (!this.remoteScanning) { + this.remoteActive = 0; + return; } + this.scan(barcode); this.waitForBarcode(); } @@ -112,14 +103,15 @@ export class BarcodeReader { } export const barcodeReader = { - dependencies: ["barcode", "popup"], - async start(env, { barcode, popup }) { + dependencies: [...BarcodeReader.serviceDependencies, "popup", "barcode"], + async start(env, deps) { + const { popup, barcode } = deps; 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 }); + barcodeReader = new BarcodeReader(parser, deps); } barcode.bus.addEventListener("barcode_scanned", (ev) => { diff --git a/addons/point_of_sale/static/src/app/debug/debug_widget.js b/addons/point_of_sale/static/src/app/debug/debug_widget.js index ce6a092243b..6c977b60bee 100644 --- a/addons/point_of_sale/static/src/app/debug/debug_widget.js +++ b/addons/point_of_sale/static/src/app/debug/debug_widget.js @@ -18,6 +18,7 @@ export class DebugWidget extends Component { this.debug = useService("debug"); this.popup = useService("popup"); this.barcodeReader = useService("barcode_reader"); + this.hardwareProxy = useService("hardware_proxy"); const numberBuffer = useService("number_buffer"); useBus(numberBuffer, "buffer-update", this._onBufferUpdate); this.state = useState({ @@ -49,29 +50,15 @@ export class DebugWidget extends Component { }, }); - // NOTE: Perhaps this can still be improved. - // What we do here is loop thru the `event` elements - // then we assign animation that happens when the event is triggered - // in the proxy. E.g. if open_cashbox is sent, the open_cashbox element - // changes color from '#6CD11D' to '#1E1E1E' for a duration of 2sec. - this.eventElementsRef = {}; - this.animations = {}; + // Make the background of the "hardware events" section flash when a corresponding message + // is sent to the proxy. for (const eventName of ["open_cashbox", "print_receipt", "scale_read"]) { - this.eventElementsRef[eventName] = useRef(eventName); - this.env.proxy.add_notification( - eventName, - (() => { - if (this.animations[eventName]) { - this.animations[eventName].cancel(); - } - const eventElement = this.eventElementsRef[eventName].el; - eventElement.style.backgroundColor = "#6CD11D"; - this.animations[eventName] = eventElement.animate( - { backgroundColor: ["#6CD11D", "#1E1E1E"] }, - 2000 - ); - }).bind(this) - ); + const ref = useRef(eventName); + let animation; + useBus(this.hardwareProxy, `send_message:${eventName}`, () => { + animation?.cancel(); + animation = ref.el?.animate({ backgroundColor: ["#6CD11D", "#1E1E1E"] }, 2000); + }); } } toggleWidget() { @@ -80,12 +67,12 @@ export class DebugWidget extends Component { setWeight() { var weightInKg = parse.float(this.state.weightInput); if (!isNaN(weightInKg)) { - this.env.proxy.debug_set_weight(weightInKg); + this.hardwareProxy.setDebugWeight(weightInKg); } } resetWeight() { this.state.weightInput = ""; - this.env.proxy.debug_reset_weight(); + this.hardwareProxy.resetDebugWeight(); } async barcodeScan() { if (!this.barcodeReader) { @@ -172,7 +159,7 @@ export class DebugWidget extends Component { } } refreshDisplay() { - this.env.proxy.message("display_refresh", {}); + this.hardwareProxy.message("display_refresh", {}); } _onBufferUpdate({ detail: value }) { this.state.buffer = value; diff --git a/addons/point_of_sale/static/src/app/hardware_proxy/hardware_proxy.js b/addons/point_of_sale/static/src/app/hardware_proxy/hardware_proxy.js new file mode 100644 index 00000000000..5fc28e93d52 --- /dev/null +++ b/addons/point_of_sale/static/src/app/hardware_proxy/hardware_proxy.js @@ -0,0 +1,228 @@ +/** @odoo-module */ +import { Printer } from "@point_of_sale/js/printers"; +import { EventBus, reactive } from "@odoo/owl"; +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { effect } from "@point_of_sale/utils"; + +/** + * This object interfaces with the local proxy to communicate to the various hardware devices + * connected to the Point of Sale. As the communication only goes from the POS to the proxy, + * methods are used both to signal an event, and to fetch information. Maybe could be improved + * by using the bus for two-way communication? + */ +export class HardwareProxy extends EventBus { + static serviceDependencies = ["rpc"]; + constructor() { + super(); + this.setup(...arguments); + } + setup({ rpc }) { + this.rpc = rpc; + this.debugWeight = 0; + this.useDebugWeight = false; + this.host = ""; + this.keptalive = false; + this.customerDisplayAvailable = true; + this.connectionInfo = reactive({ status: "disconnected", drivers: {} }); + effect( + (info) => { + if (info.status === "connected" && this.printer) { + this.printer.print_receipt(); + } + }, + [this.connectionInfo] + ); + } + + setConnectionInfo(info) { + Object.assign(this.connectionInfo, info); + if (!info.drivers && this.connectionInfo.status === "disconnected") { + this.connectionInfo.drivers = {}; + } + } + + disconnect() { + if (this.connectionInfo.status !== "disconnected") { + this.host = null; + this.setConnectionInfo({ status: "disconnected" }); + } + } + + async connect() { + if (this.pos.config.iface_print_via_proxy) { + this.connectToPrinter(); + } + try { + if (await this.message("handshake")) { + this.setConnectionInfo({ status: "connected" }); + localStorage.hw_proxy_url = this.host; + this.keepalive(); + } else { + this.setConnectionInfo({ status: "disconnected" }); + console.error("Connection refused by the Proxy"); + } + } catch { + this.setConnectionInfo({ status: "disconnected" }); + console.error("Could not connect to the Proxy"); + } + } + + connectToPrinter() { + this.printer = new Printer(this.host, this.pos); + } + + /** + * Find a proxy and connects to it. + * + * @param {Object} [options] + * @param {string} [options.force_ip] only try to connect to the specified ip. + * @param {string} [options.port] + * @returns {Promise} + */ + async autoconnect(options) { + this.setConnectionInfo({ status: "connecting", drivers: {} }); + let url = options.force_ip || localStorage.hw_proxy_url; + // Return a pending promise if there is no url to connect to + // FIXME POSREF do something useful instead if this condition can happen, remove if not + if (!url) { + return new Promise(() => {}); + } + + const { protocol } = window.location; + if (!url.includes("//")) { + url = `${protocol}//${url}`; + } + if (url.indexOf(":", 5) < 0) { + url += ":" + (options.port || (protocol === "https:" ? 443 : 8069)); + } + + if (await this.checkProxyAvailability(url)) { + this.host = url; + return this.connect(url); + } + } + + // starts a loop that updates the connection status + keepalive() { + const status = () => { + const always = () => setTimeout(status, 5000); + const xhr = new browser.XMLHttpRequest(); + xhr.timeout = 2500; + this.rpc(`${this.host}/hw_proxy/status_json`, {}, { silent: true, xhr }) + .then( + (drivers) => this.setConnectionInfo({ status: "connected", drivers }), + () => { + if (this.connectionInfo.status !== "connecting") { + this.setConnectionInfo({ status: "disconnected" }); + } + } + ) + .then(always, always); + }; + + if (!this.keptalive) { + this.keptalive = true; + status(); + } + } + + /** + * @param {string} name + * @param {Object} [params] + * @returns {Promise} + */ + message(name, params) { + this.dispatchEvent(new CustomEvent(`send_message:${name}`)); + if (this.connectionInfo.status === "disconnected") { + return Promise.reject(); + } + return this.rpc(`${this.host}/hw_proxy/${name}`, params, { silent: true }); + } + + /** + * Makes sure that the proxy is available by attempting to call the hello + * route on the proxy. + * + * @param {string} url + * @returns {Promise} + */ + async checkProxyAvailability(url) { + this.setConnectionInfo({ status: "connecting" }); + const maxRetries = 3; + for (let i = 0; i <= maxRetries; i++) { + const timeoutController = new AbortController(); + setTimeout(() => timeoutController.abort(), 1000); + const response = await browser + .fetch(`${url}/hw_proxy/hello`, { + signal: timeoutController.signal, + }) + .catch(() => ({})); + if (response.ok) { + return true; + } + } + this.setConnectionInfo({ status: "disconnected" }); + return false; + } + /** + * Returns the weight on the scale. + * + * @returns {Promise} + */ + readScale() { + if (this.useDebugWeight) { + return this.debugWeight; + } + return this.message("scale_read") + .then(({ weight }) => weight) + .catch(() => 0); + } + + // sets a custom weight, ignoring the proxy returned value. + setDebugWeight(weight) { + this.useDebugWeight = true; + this.debugWeight = weight; + } + + // resets the custom weight and re-enable listening to the proxy for weight values + resetDebugWeight() { + this.useDebugWeight = false; + this.debugWeight = 0; + } + + updateCustomerDisplay(html) { + if (this.customerDisplayAvailable && this.connectionInfo.status == "connected") { + return this.message("customer_facing_display", { html }, { timeout: 5000 }); + } + } + + /** + * @param {string} html + * @returns {Promise} + */ + takeControlOfCustomerDisplay(html) { + return this.message("take_control", { html }); + } + + /** + * @returns {Promise} + */ + testOwnershipOfCustomerDisplay() { + return this.host ? this.message("test_ownership") : Promise.reject({ abort: true }); + } + + // asks the proxy to log some information, as with the debug.log you can provide several arguments. + log() { + return this.message("log", { arguments: [...arguments] }); + } +} + +export const hardwareProxyService = { + dependencies: HardwareProxy.serviceDependencies, + start(env, deps) { + return new HardwareProxy(deps); + }, +}; + +registry.category("services").add("hardware_proxy", hardwareProxyService); diff --git a/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.js b/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.js index 3feeb239057..302a2f2fdf1 100644 --- a/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.js +++ b/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.js @@ -18,6 +18,7 @@ export class CashMovePopup extends AbstractAwaitablePopup { this.notification = useService("pos_notification"); this.popup = useService("popup"); this.orm = useService("orm"); + this.hardwareProxy = useService("hardware_proxy"); this.state = useState({ /** @type {'in'|'out'} */ type: "out", @@ -62,7 +63,7 @@ export class CashMovePopup extends AbstractAwaitablePopup { reason, extras, ]); - if (this.env.proxy.printer) { + if (this.hardwareProxy.printer) { const renderedReceipt = renderToString("point_of_sale.CashMoveReceipt", { _receipt: { type, @@ -74,7 +75,7 @@ export class CashMovePopup extends AbstractAwaitablePopup { company: this.env.pos.company, }, }); - const printResult = await this.env.proxy.printer.print_receipt(renderedReceipt); + const printResult = await this.hardwareProxy.printer.print_receipt(renderedReceipt); if (!printResult.successful) { this.popup.add(ErrorPopup, { title: printResult.message.title, diff --git a/addons/point_of_sale/static/src/app/navbar/navbar.js b/addons/point_of_sale/static/src/app/navbar/navbar.js index b2be2916d3d..f56139be1e0 100644 --- a/addons/point_of_sale/static/src/app/navbar/navbar.js +++ b/addons/point_of_sale/static/src/app/navbar/navbar.js @@ -32,6 +32,7 @@ export class Navbar extends Component { this.debug = useService("debug"); this.popup = useService("popup"); this.notification = useService("pos_notification"); + this.hardwareProxy = useService("hardware_proxy"); } get customerFacingDisplayButtonIsShown() { return this.env.pos.config.iface_customer_facing_display; diff --git a/addons/point_of_sale/static/src/app/navbar/navbar.xml b/addons/point_of_sale/static/src/app/navbar/navbar.xml index 7d5ab44516e..4a96a0235a7 100644 --- a/addons/point_of_sale/static/src/app/navbar/navbar.xml +++ b/addons/point_of_sale/static/src/app/navbar/navbar.xml @@ -20,7 +20,7 @@
- + diff --git a/addons/point_of_sale/static/src/app/pos_store.js b/addons/point_of_sale/static/src/app/pos_store.js index d92d9fbb7bb..d20667ec66a 100644 --- a/addons/point_of_sale/static/src/app/pos_store.js +++ b/addons/point_of_sale/static/src/app/pos_store.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { PosGlobalState } from "@point_of_sale/js/models"; -import { pos_env as legacyEnv } from "@point_of_sale/js/pos_env"; +import legacyEnv from "web.env"; import { registry } from "@web/core/registry"; import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; @@ -19,19 +19,30 @@ export class PosStore extends Reactive { mainScreen = { name: null, component: null }; tempScreen = null; legacyEnv = legacyEnv; - globalState = new PosGlobalState({ env: markRaw(legacyEnv) }); - static serviceDependencies = ["popup", "orm", "number_buffer", "barcode_reader"]; - constructor({ popup, orm, number_buffer, barcode_reader }) { + static serviceDependencies = [ + "popup", + "orm", + "number_buffer", + "barcode_reader", + "hardware_proxy", + ]; + constructor() { super(); + this.setup(...arguments); + } + // use setup instead of constructor because setup can be patched. + setup({ popup, orm, number_buffer, hardware_proxy, barcode_reader }) { this.orm = orm; this.popup = popup; this.numberBuffer = number_buffer; this.barcodeReader = barcode_reader; - this.setup(); + this.hardwareProxy = hardware_proxy; + this.globalState = new PosGlobalState({ + env: markRaw(legacyEnv), + hardwareProxy: hardware_proxy, + }); } - // use setup instead of constructor because setup can be patched. - setup() {} showScreen(name, props) { const component = registry.category("pos_screens").get(name); @@ -51,38 +62,34 @@ export class PosStore extends Reactive { return new Promise((resolve, reject) => { this.barcodeReader?.disconnectFromProxy(); this.globalState.loadingSkipButtonIsShown = true; - this.globalState.env.proxy - .autoconnect({ - force_ip: this.globalState.config.proxy_ip || undefined, - progress: function (prog) {}, - }) - .then( - () => { - if (this.globalState.config.iface_scan_via_proxy) { - this.barcodeReader?.connectToProxy(this.globalState.env.proxy); - } - resolve(); - }, - (statusText, url) => { - // this should reject so that it can be captured when we wait for pos.ready - // in the chrome component. - // then, if it got really rejected, we can show the error. - if (statusText == "error" && window.location.protocol == "https:") { - reject({ - title: _t("HTTPS connection to IoT Box failed"), - body: _.str.sprintf( - _t( - "Make sure you are using IoT Box v18.12 or higher. Navigate to %s to accept the certificate of your IoT Box." - ), - url - ), - popup: "alert", - }); - } else { - resolve(); - } + this.hardwareProxy.autoconnect({ force_ip: this.globalState.config.proxy_ip }).then( + () => { + if (this.globalState.config.iface_scan_via_proxy) { + this.barcodeReader?.connectToProxy(); } - ); + resolve(); + }, + (statusText, url) => { + // this should reject so that it can be captured when we wait for pos.ready + // in the chrome component. + // then, if it got really rejected, we can show the error. + if (statusText == "error" && window.location.protocol == "https:") { + // FIXME POSREF this looks like it's dead code. + reject({ + title: _t("HTTPS connection to IoT Box failed"), + body: _.str.sprintf( + _t( + "Make sure you are using IoT Box v18.12 or higher. Navigate to %s to accept the certificate of your IoT Box." + ), + url + ), + popup: "alert", + }); + } else { + resolve(); + } + } + ); }); } diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js index 5efed15d78d..e04ee9b229a 100644 --- a/addons/point_of_sale/static/src/js/Chrome.js +++ b/addons/point_of_sale/static/src/js/Chrome.js @@ -12,7 +12,7 @@ import { Navbar } from "@point_of_sale/app/navbar/navbar"; // ChromeAdapter imports import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { registry } from "@web/core/registry"; -import { pos_env as env } from "@point_of_sale/js/pos_env"; +import env from "web.env"; import { ErrorTracebackPopup } from "./Popups/ErrorTracebackPopup"; @@ -48,7 +48,8 @@ export class Chrome extends Component { env.legacyActionManager = legacyActionManager; // The proxy requires the instance of PosGlobalState to function properly. - env.proxy.set_pos(reactivePos); + this.hardwareProxy = useService("hardware_proxy"); + this.hardwareProxy.pos = reactivePos; // TODO: Should we continue on exposing posmodel as global variable? // Expose only the reactive version of `pos` when in debug mode. diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js index 0f99f5d01d0..32426144fb3 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js @@ -1,12 +1,14 @@ /** @odoo-module */ import { Component, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; export class CustomerFacingDisplayButton extends Component { static template = "CustomerFacingDisplayButton"; setup() { super.setup(); + this.hardwareProxy = useService("hardware_proxy"); this.local = this.env.pos.config.iface_customer_facing_display_local && !this.env.pos.config.iface_customer_facing_display_via_proxy; @@ -42,7 +44,7 @@ export class CustomerFacingDisplayButton extends Component { async onClickProxy() { try { const renderedHtml = await this.env.pos.render_html_for_customer_facing_display(); - let ownership = await this.env.proxy.take_ownership_over_customer_screen(renderedHtml); + let ownership = await this.hardwareProxy.takeControlOfCustomerDisplay(renderedHtml); if (typeof ownership === "string") { ownership = JSON.parse(ownership); } @@ -51,8 +53,8 @@ export class CustomerFacingDisplayButton extends Component { } else { this.state.status = "warning"; } - if (!this.env.proxy.posbox_supports_display) { - this.env.proxy.posbox_supports_display = true; + if (!this.hardwareProxy.customerDisplayAvailable) { + this.hardwareProxy.customerDisplayAvailable = true; this._start(); } } catch (error) { @@ -68,35 +70,33 @@ export class CustomerFacingDisplayButton extends Component { return; } - const self = this; - async function loop() { - if (self.env.proxy.posbox_supports_display) { + const loop = async () => { + if (this.hardwareProxy.customerDisplayAvailable) { try { - let ownership = await self.env.proxy.test_ownership_of_customer_screen(); + let ownership = await this.hardwareProxy.testOwnershipOfCustomerDisplay(); if (typeof ownership === "string") { ownership = JSON.parse(ownership); } if (ownership.status === "OWNER") { - self.state.status = "success"; + this.state.status = "success"; } else { - self.state.status = "warning"; + this.state.status = "warning"; } - setTimeout(loop, 3000); } catch (error) { if (error.abort) { // Stop the loop return; } if (typeof error == "undefined") { - self.state.status = "failure"; + this.state.status = "failure"; } else { - self.state.status = "not_found"; - self.env.proxy.posbox_supports_display = false; + this.state.status = "not_found"; + this.hardwareProxy.customerDisplayAvailable = false; } - setTimeout(loop, 3000); } + setTimeout(loop, 3000); } - } + }; loop(); } } diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js b/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js index 265bd7b2f04..bd84357755e 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js @@ -1,6 +1,9 @@ /** @odoo-module */ -import { Component, onMounted, onWillUnmount, useState } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; +import { usePos } from "@point_of_sale/app/pos_hook"; +import { useService } from "@web/core/utils/hooks"; +import { _t } from "@web/core/l10n/translation"; // Previously ProxyStatusWidget export class ProxyStatus extends Component { @@ -8,63 +11,45 @@ export class ProxyStatus extends Component { setup() { super.setup(); - const initialProxyStatus = this.env.proxy.get("status"); - this.state = useState({ - status: initialProxyStatus.status, - msg: initialProxyStatus.msg, - }); - this.statuses = ["connected", "connecting", "disconnected", "warning"]; - this.index = 0; - - onMounted(() => { - this.env.proxy.on("change:status", this, this._onChangeStatus); - }); - - onWillUnmount(() => { - this.env.proxy.off("change:status", this, this._onChangeStatus); - }); + this.pos = usePos(); + const hardwareProxy = useService("hardware_proxy"); + this.connectionInfo = useState(hardwareProxy.connectionInfo); } - _onChangeStatus(posProxy, statusChange) { - this._setStatus(statusChange.newValue); - } - _setStatus(newStatus) { - if (newStatus.status === "connected") { - var warning = false; - var msg = ""; - if (this.env.pos.config.iface_scan_via_proxy) { - var scannerStatus = newStatus.drivers.scanner - ? newStatus.drivers.scanner.status - : false; - if (scannerStatus != "connected" && scannerStatus != "connecting") { - warning = true; - msg += this.env._t("Scanner"); - } - } - if (this.env.pos.config.iface_print_via_proxy || this.env.pos.config.iface_cashdrawer) { - var printerStatus = newStatus.drivers.printer - ? newStatus.drivers.printer.status - : false; - if (printerStatus != "connected" && printerStatus != "connecting") { - warning = true; - msg = msg ? msg + " & " : msg; - msg += this.env._t("Printer"); - } - } - if (this.env.pos.config.iface_electronic_scale) { - var scaleStatus = newStatus.drivers.scale ? newStatus.drivers.scale.status : false; - if (scaleStatus != "connected" && scaleStatus != "connecting") { - warning = true; - msg = msg ? msg + " & " : msg; - msg += this.env._t("Scale"); - } - } - msg = msg ? msg + " " + this.env._t("Offline") : msg; - this.state.status = warning ? "warning" : "connected"; - this.state.msg = msg; - } else { - this.state.status = newStatus.status; - this.state.msg = newStatus.msg || ""; + get message() { + if (this.connectionInfo.status === "connected") { + const { drivers } = this.connectionInfo; + const { + iface_scan_via_proxy, + iface_print_via_proxy, + iface_cashdrawer, + iface_electronic_scale, + } = this.env.pos.config; + const devices = [ + { + name: _t("Scanner"), + driver: drivers.scanner, + enabled: iface_scan_via_proxy, + }, + { + name: _t("Printer"), + driver: drivers.printer, + enabled: iface_print_via_proxy || iface_cashdrawer, + }, + { + name: _t("Scale"), + driver: drivers.scale, + enabled: iface_electronic_scale, + }, + ]; + const disconnectedDevices = devices.filter(({ enabled, driver }) => { + return enabled && !["connected", "connecting"].includes(driver?.status); + }); + if (disconnectedDevices.length) { + return `${disconnectedDevices.map((d) => d.name).join(" & ")} ${_t("Offline")}`; + } + return ""; } + return this.connectionInfo.message || ""; } } diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js index 965e4e87709..e058c7ae9b9 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js @@ -12,6 +12,7 @@ export class SaleDetailsButton extends Component { super.setup(...arguments); this.popup = useService("popup"); this.orm = useService("orm"); + this.hardwareProxy = useService("hardware_proxy"); } async onClick() { @@ -30,11 +31,11 @@ export class SaleDetailsButton extends Component { pos: this.env.pos, }) ); - const printResult = await this.env.proxy.printer.print_receipt(report); - if (!printResult.successful) { + const { successful, message } = await this.hardwareProxy.printer.print_receipt(report); + if (!successful) { await this.popup.add(ErrorPopup, { - title: printResult.message.title, - body: printResult.message.body, + title: message.title, + body: message.body, }); } } diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js b/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js index ce4c91c092d..3ad8a8a46c4 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js @@ -6,6 +6,6 @@ export class SyncNotification extends Component { static template = "SyncNotification"; onClick() { - this.env.pos.push_orders(null, { show_error: true }); + this.env.pos.push_orders({ show_error: true }); } } diff --git a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js index b01e79024b2..15e3bd8e42a 100644 --- a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js @@ -16,10 +16,11 @@ export class AbstractReceiptScreen extends Component { super.setup(); this.orderReceipt = useRef("order-receipt"); this.popup = useService("popup"); + this.hardwareProxy = useService("hardware_proxy"); } async _printReceipt() { - if (this.env.proxy.printer) { - const printResult = await this.env.proxy.printer.print_receipt( + if (this.hardwareProxy.printer) { + const printResult = await this.hardwareProxy.printer.print_receipt( this.orderReceipt.el.innerHTML ); if (printResult.successful) { diff --git a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js index 4b0b600412d..b05028d79ab 100644 --- a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js @@ -21,6 +21,7 @@ export class ClosePosPopup extends AbstractAwaitablePopup { this.popup = useService("popup"); this.pos = useService("pos"); this.orm = useService("orm"); + this.hardwareProxy = useService("hardware_proxy"); this.manualInputCashCount = false; this.cashControl = this.env.pos.config.cash_control; this.closeSessionClicked = false; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index ed1e1d4a309..88fc7c62dae 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -31,6 +31,7 @@ export class PaymentScreen extends Component { this.orm = useService("orm"); this.popup = useService("popup"); this.notification = useService("pos_notification"); + this.hardwareProxy = useService("hardware_proxy"); this.payment_methods_from_config = this.env.pos.payment_methods.filter((method) => this.env.pos.config.payment_method_ids.includes(method.id) ); @@ -132,7 +133,7 @@ export class PaymentScreen extends Component { this.render(true); } openCashbox() { - this.env.proxy.printer.open_cashbox(); + this.hardwareProxy.printer.open_cashbox(); } async addTip() { // click_tip @@ -217,7 +218,7 @@ export class PaymentScreen extends Component { (this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) && this.env.pos.config.iface_cashdrawer ) { - this.env.proxy.printer.open_cashbox(); + this.hardwareProxy.printer.open_cashbox(); } this.currentOrder.initialize_validation_date(); diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js index ee40baa22ec..57f0d852e23 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js @@ -14,6 +14,7 @@ import { ConnectionLostError } from "@web/core/network/rpc_service"; import { usePos } from "@point_of_sale/app/pos_hook"; import { Component, onMounted, useState } from "@odoo/owl"; import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; +import { ErrorBarcodePopup } from "@point_of_sale/js/Popups/ErrorBarcodePopup"; import { MobileOrderWidget } from "../../Misc/MobileOrderWidget"; import { NumpadWidget } from "./NumpadWidget"; @@ -142,7 +143,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { // assume that the result is unique. product = this.env.pos.db.get_product_by_id(foundProductIds[0]); } else { - return this._barcodeErrorAction(code); + return this.popup.add(ErrorBarcodePopup, { code: code.base_code }); } } const options = await product.getAddProductOptions(code); @@ -181,10 +182,9 @@ export class ProductScreen extends ControlButtonsMixin(Component) { this.currentOrder.set_partner(partner); this.currentOrder.updatePricelist(partner); } - return true; + return; } - this._barcodeErrorAction(code); - return false; + return this.popup.add(ErrorBarcodePopup, { code: code.base_code }); } _barcodeDiscountAction(code) { var last_orderline = this.currentOrder.get_last_orderline(); diff --git a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js index 7a8b2e5727b..e1aa94b4a85 100644 --- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -162,7 +162,7 @@ export class ReceiptScreen extends AbstractReceiptScreen { ? this.currentOrder.finalized : true; return ( - this.env.proxy.printer && + this.hardwareProxy.printer && this.env.pos.config.iface_print_skip_screen && invoiced_finalized ); diff --git a/addons/point_of_sale/static/src/js/Screens/ScaleScreen/ScaleScreen.js b/addons/point_of_sale/static/src/js/Screens/ScaleScreen/ScaleScreen.js index 1bc040c2d03..5c9bf046846 100644 --- a/addons/point_of_sale/static/src/js/Screens/ScaleScreen/ScaleScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ScaleScreen/ScaleScreen.js @@ -4,6 +4,7 @@ import { round_precision as round_pr } from "web.utils"; import { registry } from "@web/core/registry"; import { usePos } from "@point_of_sale/app/pos_hook"; import { Component, onMounted, onWillUnmount, useExternalListener, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; export class ScaleScreen extends Component { static template = "ScaleScreen"; @@ -14,6 +15,7 @@ export class ScaleScreen extends Component { */ setup() { super.setup(); + this.hardwareProxy = useService("hardware_proxy"); useExternalListener(document, "keyup", this._onHotkeys); this.state = useState({ weight: 0 }); onMounted(this.onMounted); @@ -26,7 +28,7 @@ export class ScaleScreen extends Component { } onWillUnmount() { // stop the scale reading - this.env.proxy_queue.clear(); + this.shouldRead = false; } back() { this.props.resolve({ confirmed: false, payload: null }); @@ -47,14 +49,15 @@ export class ScaleScreen extends Component { } } _readScale() { - this.env.proxy_queue.schedule(this._setWeight.bind(this), { - duration: 500, - repeat: true, - }); + this.shouldRead = true; + this._setWeight(); } async _setWeight() { - const reading = await this.env.proxy.scale_read(); - this.state.weight = reading.weight; + if (!this.shouldRead) { + return; + } + this.state.weight = await this.hardwareProxy.readScale(); + setTimeout(() => this._setWeight(), 500); } get _activePricelist() { const current_order = this.env.pos.get_order(); diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js index 9b3b75af340..0407b211f3b 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js @@ -21,7 +21,7 @@ export class ReprintReceiptScreen extends AbstractReceiptScreen { this.pos.showScreen("TicketScreen", { reuseSavedUIState: true }); } async printReceipt() { - if (this.env.proxy.printer && this.env.pos.config.iface_print_skip_screen) { + if (this.hardwareProxy.printer && this.env.pos.config.iface_print_skip_screen) { const result = await this._printReceipt(); if (result) { this.pos.showScreen("TicketScreen", { reuseSavedUIState: true }); diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js deleted file mode 100644 index 8dd7ad29550..00000000000 --- a/addons/point_of_sale/static/src/js/devices.js +++ /dev/null @@ -1,488 +0,0 @@ -/** @odoo-module */ -import core from "web.core"; -import mixins from "web.mixins"; -import Session from "web.Session"; -import { Printer } from "@point_of_sale/js/printers"; - -// the JobQueue schedules a sequence of 'jobs'. each job is -// a function returning a promise. The queue waits for each job to finish -// before launching the next. Each job can also be scheduled with a delay. -// the is used to prevent parallel requests to the proxy. - -export const JobQueue = function () { - var queue = []; - var running = false; - var scheduled_end_time = 0; - var end_of_queue = Promise.resolve(); - var stoprepeat = false; - - var run = function () { - var runNextJob = function () { - if (queue.length === 0) { - running = false; - scheduled_end_time = 0; - return Promise.resolve(); - } - running = true; - var job = queue[0]; - if (!job.opts.repeat || stoprepeat) { - queue.shift(); - stoprepeat = false; - } - - // the time scheduled for this job - scheduled_end_time = new Date().getTime() + (job.opts.duration || 0); - - // we run the job and put in prom when it finishes - var prom = job.fun() || Promise.resolve(); - - var always = function () { - // we run the next job after the scheduled_end_time, even if it finishes before - return new Promise(function (resolve, reject) { - setTimeout(resolve, Math.max(0, scheduled_end_time - new Date().getTime())); - }); - }; - // we don't care if a job fails ... - return prom.then(always, always).then(runNextJob); - }; - - if (!running) { - end_of_queue = runNextJob(); - } - }; - - /** - * Adds a job to the schedule. - * - * @param {function} fun must return a promise - * @param {object} [opts] - * @param {number} [opts.duration] the job is guaranteed to finish no quicker than this (milisec) - * @param {boolean} [opts.repeat] if true, the job will be endlessly repeated - * @param {boolean} [opts.important] if true, the scheduled job cannot be canceled by a queue.clear() - */ - this.schedule = function (fun, opts) { - queue.push({ fun: fun, opts: opts || {} }); - if (!running) { - run(); - } - }; - - // remove all jobs from the schedule (except the ones marked as important) - this.clear = function () { - queue = _.filter(queue, function (job) { - return job.opts.important === true; - }); - }; - - // end the repetition of the current job - this.stoprepeat = function () { - stoprepeat = true; - }; - - /** - * Returns a promise that resolves when all scheduled jobs have been run. - * (jobs added after the call to this method are considered as well) - * - * @returns {Promise} - */ - this.finished = function () { - return end_of_queue; - }; -}; - -// this object interfaces with the local proxy to communicate to the various hardware devices -// connected to the Point of Sale. As the communication only goes from the POS to the proxy, -// methods are used both to signal an event, and to fetch information. - -export const ProxyDevice = core.Class.extend(mixins.PropertiesMixin, { - init: function (options) { - mixins.PropertiesMixin.init.call(this); - var self = this; - options = options || {}; - - this.env = options.env; - - this.weighing = false; - this.debug_weight = 0; - this.use_debug_weight = false; - - this.paying = false; - - this.notifications = {}; - this.bypass_proxy = false; - - this.connection = null; - this.host = ""; - this.keptalive = false; - - this.set("status", {}); - - this.set_connection_status("disconnected"); - - this.on("change:status", this, function (eh, status) { - status = status.newValue; - if (status.status === "connected" && self.printer) { - self.printer.print_receipt(); - } - }); - - this.posbox_supports_display = true; - - window.hw_proxy = this; - }, - set_pos: function (pos) { - this.setParent(pos); - this.pos = pos; - }, - set_connection_status: function (status, drivers, msg = "") { - var oldstatus = this.get("status"); - var newstatus = {}; - newstatus.status = status; - newstatus.drivers = status === "disconnected" ? {} : oldstatus.drivers; - newstatus.drivers = drivers ? drivers : newstatus.drivers; - newstatus.msg = msg; - this.set("status", newstatus); - }, - disconnect: function () { - if (this.get("status").status !== "disconnected") { - this.connection.destroy(); - this.set_connection_status("disconnected"); - } - }, - - /** - * Connects to the specified url. - * - * @param {string} url - * @returns {Promise} - */ - connect: function (url) { - var self = this; - this.connection = new Session(undefined, url, { use_cors: true }); - this.host = url; - if (this.pos.config.iface_print_via_proxy) { - this.connect_to_printer(); - } - this.set_connection_status("connecting", {}); - - return this.message("handshake").then( - function (response) { - if (response) { - self.set_connection_status("connected"); - localStorage.hw_proxy_url = url; - self.keepalive(); - } else { - self.set_connection_status("disconnected"); - console.error("Connection refused by the Proxy"); - } - }, - function () { - self.set_connection_status("disconnected"); - console.error("Could not connect to the Proxy"); - } - ); - }, - - connect_to_printer: function () { - this.printer = new Printer(this.host, this.pos); - }, - - /** - * Find a proxy and connects to it. - * - * @param {Object} [options] - * @param {string} [options.force_ip] only try to connect to the specified ip. - * @param {string} [options.port] @see find_proxy - * @param {function} [options.progress] @see find_proxy - * @returns {Promise} - */ - autoconnect: function (options) { - var self = this; - this.set_connection_status("connecting", {}); - if (this.pos.config.iface_print_via_proxy) { - this.connect_to_printer(); - } - var found_url = new Promise(function () {}); - - if (options.force_ip) { - // if the ip is forced by server config, bailout on fail - found_url = this.try_hard_to_connect(options.force_ip, options); - } else if (localStorage.hw_proxy_url) { - // try harder when we remember a good proxy url - found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options).catch( - function () { - if (window.location.protocol != "https:") { - return self.find_proxy(options); - } - } - ); - } else { - // just find something quick - if (window.location.protocol != "https:") { - found_url = this.find_proxy(options); - } - } - - var successProm = found_url.then(function (url) { - return self.connect(url); - }); - - successProm.catch(function () { - self.set_connection_status("disconnected"); - }); - - return successProm; - }, - - // starts a loop that updates the connection status - keepalive: function () { - var self = this; - - function status() { - var always = function () { - setTimeout(status, 5000); - }; - self.connection - .rpc("/hw_proxy/status_json", {}, { shadow: true, timeout: 2500 }) - .then( - function (driver_status) { - self.set_connection_status("connected", driver_status); - }, - function () { - if (self.get("status").status !== "connecting") { - self.set_connection_status("disconnected"); - } - } - ) - .then(always, always); - } - - if (!this.keptalive) { - this.keptalive = true; - status(); - } - }, - - /** - * @param {string} name - * @param {Object} [params] - * @returns {Promise} - */ - message: function (name, params) { - var callbacks = this.notifications[name] || []; - for (var i = 0; i < callbacks.length; i++) { - callbacks[i](params); - } - if (this.get("status").status !== "disconnected") { - return this.connection.rpc("/hw_proxy/" + name, params || {}, { shadow: true }); - } else { - return Promise.reject(); - } - }, - - /** - * Tries several time to connect to a known proxy url. - * - * @param {*} url - * @param {Object} [options] - * @param {string} [options.port=8069] what port to listen to - * @returns {Promise} - */ - try_hard_to_connect: function (url, options) { - options = options || {}; - var protocol = window.location.protocol; - var port = !options.port && protocol == "https:" ? ":443" : ":" + (options.port || "8069"); - - this.set_connection_status("connecting"); - - if (url.indexOf("//") < 0) { - url = protocol + "//" + url; - } - - if (url.indexOf(":", 5) < 0) { - url = url + port; - } - - // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries - function try_real_hard_to_connect(url, retries) { - return Promise.resolve( - $.ajax({ - url: url + "/hw_proxy/hello", - method: "GET", - timeout: 1000, - }).then( - function () { - return Promise.resolve(url); - }, - function (resp) { - if (retries > 0) { - return try_real_hard_to_connect(url, retries - 1); - } else { - return Promise.reject([resp.statusText, url]); - } - } - ) - ); - } - - return try_real_hard_to_connect(url, 3); - }, - - /** - * Returns as a promise a valid host url that can be used as proxy. - * - * @param {Object} [options] - * @param {string} [options.port] what port to listen to (default 8069) - * @param {function} [options.progress] callback for search progress ( fac in [0,1] ) - * @returns {Promise} will be resolved with the proxy valid url - */ - find_proxy: function (options) { - options = options || {}; - var self = this; - var port = ":" + (options.port || "8069"); - var urls = []; - var found = false; - var parallel = 8; - var threads = []; - var progress = 0; - - urls.push("http://localhost" + port); - for (var i = 0; i < 256; i++) { - urls.push("http://192.168.0." + i + port); - urls.push("http://192.168.1." + i + port); - urls.push("http://10.0.0." + i + port); - } - - var prog_inc = 1 / urls.length; - - function update_progress() { - progress = found ? 1 : progress + prog_inc; - if (options.progress) { - options.progress(progress); - } - } - - function thread() { - var url = urls.shift(); - - if (!url || found || !self.searching_for_proxy) { - return Promise.resolve(); - } - - return Promise.resolve( - $.ajax({ - url: url + "/hw_proxy/hello", - method: "GET", - timeout: 400, - }).then( - function () { - found = true; - update_progress(); - return Promise.resolve(url); - }, - function () { - update_progress(); - return thread(); - } - ) - ); - } - - this.searching_for_proxy = true; - - var len = Math.min(parallel, urls.length); - for (i = 0; i < len; i++) { - threads.push(thread()); - } - - return new Promise(function (resolve, reject) { - Promise.all(threads).then(function (results) { - var urls = []; - for (var i = 0; i < results.length; i++) { - if (results[i]) { - urls.push(results[i]); - } - } - resolve(urls[0]); - }); - }); - }, - - stop_searching: function () { - this.searching_for_proxy = false; - this.set_connection_status("disconnected"); - }, - - // this allows the client to be notified when a proxy call is made. The notification - // callback will be executed with the same arguments as the proxy call - add_notification: function (name, callback) { - if (!this.notifications[name]) { - this.notifications[name] = []; - } - this.notifications[name].push(callback); - }, - - /** - * Returns the weight on the scale. - * - * @returns {Promise} - */ - scale_read: function () { - var self = this; - if (self.use_debug_weight) { - return Promise.resolve({ weight: this.debug_weight, unit: "Kg", info: "ok" }); - } - return new Promise(function (resolve, reject) { - self.message("scale_read", {}).then( - function (weight) { - resolve(weight); - }, - function () { - //failed to read weight - resolve({ weight: 0.0, unit: "Kg", info: "ok" }); - } - ); - }); - }, - - // sets a custom weight, ignoring the proxy returned value. - debug_set_weight: function (kg) { - this.use_debug_weight = true; - this.debug_weight = kg; - }, - - // resets the custom weight and re-enable listening to the proxy for weight values - debug_reset_weight: function () { - this.use_debug_weight = false; - this.debug_weight = 0; - }, - - update_customer_facing_display: function (html) { - if (this.posbox_supports_display && this.get("status").status == "connected") { - return this.message("customer_facing_display", { html: html }, { timeout: 5000 }); - } - }, - - /** - * @param {string} html - * @returns {Promise} - */ - take_ownership_over_customer_screen: function (html) { - return this.message("take_control", { html: html }); - }, - - /** - * @returns {Promise} - */ - test_ownership_of_customer_screen: function () { - if (this.connection) { - return this.message("test_ownership", {}); - } - return Promise.reject({ abort: true }); - }, - - // asks the proxy to log some information, as with the debug.log you can provide several arguments. - log: function () { - return this.message("log", { arguments: _.toArray(arguments) }); - }, -}); diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index dd051cb3b13..fb2ffe1ad01 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -13,7 +13,8 @@ import { ProductConfiguratorPopup } from "@point_of_sale/js/Popups/ProductConfig import { EditListPopup } from "@point_of_sale/js/Popups/EditListPopup"; import { markRaw, reactive } from "@odoo/owl"; import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; -import { escape } from "@web/core/utils/strings"; +import { escape } from "@web/core/utils/strings"; +import { Mutex } from "@web/core/utils/concurrency"; var QWeb = core.qweb; var _t = core._t; @@ -97,6 +98,7 @@ export class PosGlobalState extends PosModel { this.db = new PosDB(); // a local database used to search trough products and categories & store pending orders this.debug = config.isDebug(); //debug mode this.unwatched = markRaw({}); + this.pushOrderMutex = new Mutex(); // Business data; loaded from the server at launch this.company_logo = null; @@ -461,19 +463,15 @@ export class PosGlobalState extends PosModel { const ordersToSync = this.db.get_unpaid_orders_to_sync(ordersUidsToSync); const ordersResponse = await this._save_to_server(ordersToSync, { draft: true }); const orders = [...this.ordersToUpdateSet].map((order) => order); - ordersResponse.forEach((orderResponseData) => - this._updateOrder(orderResponseData, orders) - ); + ordersResponse.forEach((orderResponseData) => this._updateOrder(orderResponseData, orders)); this.ordersToUpdateSet.clear(); } addOrderToUpdateSet() { - this.ordersToUpdateSet.add(this.selectedOrder) + this.ordersToUpdateSet.add(this.selectedOrder); } // created this hook for modularity _updateOrder(ordersResponseData, orders) { - const order = orders.find( - (order) => order.name === ordersResponseData.pos_reference - ); + const order = orders.find((order) => order.name === ordersResponseData.pos_reference); if (order) { order.server_id = ordersResponseData.id; return order; @@ -654,7 +652,7 @@ export class PosGlobalState extends PosModel { }); let removeSelected = true; newOrdersJsons.forEach((json) => { - let isSelectedOrder = this._createOrder(json); + const isSelectedOrder = this._createOrder(json); if (removeSelected && isSelectedOrder) { removeSelected = false; } @@ -664,7 +662,11 @@ export class PosGlobalState extends PosModel { } } _shouldRemoveOrder(order) { - return (!this.selectedOrder || (this.selectedOrder.uid != order.uid)) && order.server_id && !order.finalized; + return ( + (!this.selectedOrder || this.selectedOrder.uid != order.uid) && + order.server_id && + !order.finalized + ); } _shouldRemoveSelectedOrder(removeSelected) { return removeSelected && this.selectedOrder.server_id && !this.selectedOrder.finalized; @@ -714,10 +716,10 @@ export class PosGlobalState extends PosModel { }); } async _addPricelists(ordersJson) { - let pricelistsToGet = []; + const pricelistsToGet = []; ordersJson.forEach((order) => { let found = false; - for (let pricelist of this.pricelists) { + for (const pricelist of this.pricelists) { if (pricelist.id === order.pricelist_id) { found = true; break; @@ -735,10 +737,11 @@ export class PosGlobalState extends PosModel { return message; } async _getPricelistJson(pricelistsToGet) { - return await this.env.services.orm.call("pos.session", "get_pos_ui_product_pricelists_by_ids", [ - [odoo.pos_session_id], - pricelistsToGet, - ]); + return await this.env.services.orm.call( + "pos.session", + "get_pos_ui_product_pricelists_by_ids", + [[odoo.pos_session_id], pricelistsToGet] + ); } _addPosPricelists(pricelistsJson) { if (!this.config.use_pricelist) { @@ -748,15 +751,18 @@ export class PosGlobalState extends PosModel { let message = ""; const pricelistsNames = pricelistsJson.map((pricelist) => { return pricelist.display_name; - }) - message = _.str.sprintf(_t("%s fiscal position(s) added to the configuration."), pricelistsNames.join(", ")); + }); + message = _.str.sprintf( + _t("%s fiscal position(s) added to the configuration."), + pricelistsNames.join(", ") + ); return message; } async _addFiscalPositions(ordersJson) { - let fiscalPositionToGet = []; + const fiscalPositionToGet = []; ordersJson.forEach((order) => { let found = false; - for (let fp of this.fiscal_positions) { + for (const fp of this.fiscal_positions) { if (fp.id === order.fiscal_position_id) { found = true; break; @@ -774,22 +780,26 @@ export class PosGlobalState extends PosModel { return message; } async _getFiscalPositionJson(fiscalPositionToGet) { - return await this.env.services.orm.call("pos.session", "get_pos_ui_account_fiscal_positions_by_ids", [ - [odoo.pos_session_id], - fiscalPositionToGet, - ]); + return await this.env.services.orm.call( + "pos.session", + "get_pos_ui_account_fiscal_positions_by_ids", + [[odoo.pos_session_id], fiscalPositionToGet] + ); } _addPosFiscalPosition(fiscalPositionJson) { this.fiscal_positions.push(...fiscalPositionJson); let message = ""; const fiscalPositionNames = fiscalPositionJson.map((fp) => { return fp.display_name; - }) - message = _.str.sprintf(_t("%s fiscal position(s) added to the configuration."), fiscalPositionNames.join(", ")); + }); + message = _.str.sprintf( + _t("%s fiscal position(s) added to the configuration."), + fiscalPositionNames.join(", ") + ); return message; } sortOrders() { - this.orders.sort((a, b) => (a.name > b.name) ? 1 : -1) + this.orders.sort((a, b) => (a.name > b.name ? 1 : -1)); } async getProductInfo(product, quantity) { const order = this.get_order(); @@ -951,9 +961,9 @@ export class PosGlobalState extends PosModel { orderlines.scrollTop(orderlines.prop("scrollHeight")); } else if ( this.config.iface_customer_facing_display_via_proxy && - this.env.proxy.posbox_supports_display + this.hardwareProxy.customerDisplayAvailable ) { - this.env.proxy.update_customer_facing_display(rendered_html); + this.hardwareProxy.updateCustomerDisplay(rendered_html); } }); } @@ -1006,42 +1016,13 @@ export class PosGlobalState extends PosModel { }); } - // saves the order locally and try to send it to the backend. - // it returns a promise that succeeds after having tried to send the order and all the other pending orders. - push_orders(order, opts) { - opts = opts || {}; - var self = this; - - if (order) { - this.db.add_order(order.export_as_JSON()); - } - - return new Promise((resolve, reject) => { - this.env.posMutex.exec(async () => { - try { - resolve(await self._flush_orders(self.db.get_orders(), opts)); - } catch (error) { - reject(error); - } - }); - }); + push_orders(opts = {}) { + return this.pushOrderMutex.exec(() => this._flush_orders(this.db.get_orders(), opts)); } - push_single_order(order, opts) { - opts = opts || {}; - const self = this; - const order_id = self.db.add_order(order.export_as_JSON()); - - return new Promise((resolve, reject) => { - this.env.posMutex.exec(async () => { - const order = self.db.get_order(order_id); - try { - resolve(await self._flush_orders([order], opts)); - } catch (error) { - reject(error); - } - }); - }); + push_single_order(order) { + const order_id = this.db.add_order(order.export_as_JSON()); + return this.pushOrderMutex.exec(() => this._flush_orders([this.db.get_order(order_id)])); } // Send validated orders to the backend. @@ -2783,7 +2764,7 @@ export class Payment extends PosModel { }; } //exports as JSON for receipt printing - export_for_printing(){ + export_for_printing() { const ticket = escape(this.ticket).replace(/\n/g, "
"); // formatting return { cid: this.cid, diff --git a/addons/point_of_sale/static/src/js/pos_env.js b/addons/point_of_sale/static/src/js/pos_env.js deleted file mode 100644 index 765ecc6b3ab..00000000000 --- a/addons/point_of_sale/static/src/js/pos_env.js +++ /dev/null @@ -1,16 +0,0 @@ -/* @odoo-module */ - -// This module is basically web.env but with added fields -// that are specific to point_of_sale and extensions. - -import env from "web.env"; -import concurrency from "web.concurrency"; -import { JobQueue, ProxyDevice } from "@point_of_sale/js/devices"; - -// Create new env object base on web.env. -// Mutating this new object won't affect the original object. -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.posMutex = new concurrency.Mutex(); diff --git a/addons/point_of_sale/static/src/xml/Chrome.xml b/addons/point_of_sale/static/src/xml/Chrome.xml index 0a2c6146e50..0df3e6e76a8 100644 --- a/addons/point_of_sale/static/src/xml/Chrome.xml +++ b/addons/point_of_sale/static/src/xml/Chrome.xml @@ -33,9 +33,6 @@

Connecting to devices

-
diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/ProxyStatus.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/ProxyStatus.xml index 3e45e0085eb..cd661e9e4f3 100644 --- a/addons/point_of_sale/static/src/xml/ChromeWidgets/ProxyStatus.xml +++ b/addons/point_of_sale/static/src/xml/ChromeWidgets/ProxyStatus.xml @@ -2,26 +2,26 @@ -
- - +
+ + - + - + - - - - + + + +
diff --git a/addons/point_of_sale/static/src/xml/Popups/ClosePosPopup.xml b/addons/point_of_sale/static/src/xml/Popups/ClosePosPopup.xml index 5fe74463d83..d4a66513acb 100644 --- a/addons/point_of_sale/static/src/xml/Popups/ClosePosPopup.xml +++ b/addons/point_of_sale/static/src/xml/Popups/ClosePosPopup.xml @@ -89,7 +89,7 @@
-
+
diff --git a/addons/pos_epson_printer/static/src/js/pos_epson_printer.js b/addons/pos_epson_printer/static/src/js/pos_epson_printer.js index aa916a9e3d4..cb00de0aac6 100644 --- a/addons/pos_epson_printer/static/src/js/pos_epson_printer.js +++ b/addons/pos_epson_printer/static/src/js/pos_epson_printer.js @@ -9,7 +9,7 @@ patch(PosGlobalState.prototype, "pos_epson_printer.PosGlobalState", { var self = this; return this._super(...arguments).then(function () { if (self.config.other_devices && self.config.epson_printer_ip) { - self.env.proxy.printer = new EpsonPrinter(self.config.epson_printer_ip, self); + self.hardwareProxy.printer = new EpsonPrinter(self.config.epson_printer_ip, self); } }); }, diff --git a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js index 8a056443a0f..054b9ee4af1 100644 --- a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js @@ -18,6 +18,7 @@ export class TipScreen extends Component { this.posReceiptContainer = useRef("pos-receipt-container"); this.popup = useService("popup"); this.orm = useService("orm"); + this.hardwareProxy = useService("hardware_proxy"); this.state = this.currentOrder.uiState.TipScreen; this._totalAmount = this.currentOrder.get_total_with_tax(); @@ -124,7 +125,7 @@ export class TipScreen extends Component { total: this.env.pos.format_currency(this.totalAmount), }); - if (this.env.proxy.printer) { + if (this.hardwareProxy.printer) { await this._printIoT(receipt); } else { await this._printWeb(receipt); @@ -133,7 +134,7 @@ export class TipScreen extends Component { } async _printIoT(receipt) { - const printResult = await this.env.proxy.printer.print_receipt(receipt); + const printResult = await this.hardwareProxy.printer.print_receipt(receipt); if (!printResult.successful) { await this.popup.add(ErrorPopup, { title: printResult.message.title, diff --git a/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml index 93e2b41d2a7..f12b5b9b646 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml @@ -11,7 +11,7 @@ Back - + Reprint receipts diff --git a/addons/pos_six/static/src/js/payment_six.js b/addons/pos_six/static/src/js/payment_six.js index 5f3b712a5fa..23d4cc3fb50 100644 --- a/addons/pos_six/static/src/js/payment_six.js +++ b/addons/pos_six/static/src/js/payment_six.js @@ -155,9 +155,9 @@ export const PaymentSix = PaymentInterface.extend({ _.forEach(receipts, (receipt) => { if ( receipt.recipient === timapi.constants.Recipient.merchant && - this.pos.env.proxy.printer + this.pos.hardwareProxy.printer ) { - this.pos.env.proxy.printer.print_receipt( + this.pos.hardwareProxy.printer.print_receipt( "
" + escape(receipt.value).replace(/\n/g, "
") + "
"