[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) <jcb@odoo.com>
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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<number>}
|
||||
*/
|
||||
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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="pos-rightheader">
|
||||
<div class="status-buttons">
|
||||
<CashierName />
|
||||
<SaleDetailsButton t-if="env.proxy.printer" isHeaderButton="true"/>
|
||||
<SaleDetailsButton t-if="hardwareProxy.printer" isHeaderButton="true"/>
|
||||
<ProxyStatus t-if="env.pos.config.use_proxy" />
|
||||
<CustomerFacingDisplayButton t-if="customerFacingDisplayButtonIsShown" />
|
||||
<SyncNotification />
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<string|Array>}
|
||||
*/
|
||||
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<string>} 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<Object>}
|
||||
*/
|
||||
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) });
|
||||
},
|
||||
});
|
||||
@@ -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, "<br />"); // formatting
|
||||
return {
|
||||
cid: this.cid,
|
||||
|
||||
@@ -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();
|
||||
@@ -33,9 +33,6 @@
|
||||
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
|
||||
<t t-if="pos.loadingSkipButtonIsShown">
|
||||
<h1 class="message">Connecting to devices</h1>
|
||||
<div class="button skip" t-on-click="() => this.env.proxy.stop_searching()">
|
||||
Skip
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="ProxyStatus" owl="1">
|
||||
<div class="oe_status js_proxy" t-on-click="() => this.trigger('connect-to-proxy')">
|
||||
<span t-if="state.msg and !env.isMobile" class="js_msg">
|
||||
<t t-esc="state.msg" />
|
||||
<div class="oe_status js_proxy" t-on-click="() => this.pos.connectToProxy()">
|
||||
<span t-if="this.message and !env.isMobile" class="js_msg">
|
||||
<t t-esc="this.message" />
|
||||
</span>
|
||||
<span t-if="state.status === 'connected'" class="js_connected oe_green">
|
||||
<span t-if="connectionInfo.status === 'connected' and !this.message" class="js_connected oe_green">
|
||||
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Connected"
|
||||
title="Proxy Connected"></i>
|
||||
</span>
|
||||
<span t-if="state.status === 'connecting'" class="js_connecting">
|
||||
<span t-if="connectionInfo.status === 'connecting'" class="js_connecting">
|
||||
<i class="fa fa-fw fa-spin fa-circle-o-notch" role="img" aria-label="Connecting to Proxy"
|
||||
title="Connecting to Proxy"></i>
|
||||
</span>
|
||||
<span t-if="state.status === 'warning'" class="js_warning oe_orange">
|
||||
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Warning"
|
||||
title="Proxy Warning"></i>
|
||||
</span>
|
||||
<span t-if="state.status === 'disconnected'" class="js_disconnected oe_red">
|
||||
<span t-elif="connectionInfo.status === 'disconnected'" class="js_disconnected oe_red">
|
||||
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Disconnected"
|
||||
title="Proxy Disconnected"></i>
|
||||
</span>
|
||||
<span t-elif="connectionInfo.status === 'warning' or this.message" class="js_warning oe_orange">
|
||||
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Warning"
|
||||
title="Proxy Warning"></i>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<i class="fa fa-download" role="img"/>
|
||||
</div>
|
||||
<!-- Print Sale Details -->
|
||||
<div t-if="env.proxy.printer" class="small button icon">
|
||||
<div t-if="hardwareProxy.printer" class="small button icon">
|
||||
<SaleDetailsButton/>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<span> </span>
|
||||
<span>Back</span>
|
||||
</span>
|
||||
<span class="button" t-if="env.proxy.printer" t-on-click="printTipReceipt">
|
||||
<span class="button" t-if="hardwareProxy.printer" t-on-click="printTipReceipt">
|
||||
<i class="fa fa-print"></i>
|
||||
<span> </span>
|
||||
<span>Reprint receipts</span>
|
||||
|
||||
@@ -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(
|
||||
"<div class='pos-receipt'><div class='pos-payment-terminal-receipt'>" +
|
||||
escape(receipt.value).replace(/\n/g, "<br />") +
|
||||
"</div></div>"
|
||||
|
||||
Reference in New Issue
Block a user