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, "
") + "
"