diff --git a/addons/l10n_fr_pos_cert/static/src/js/pos.js b/addons/l10n_fr_pos_cert/static/src/js/pos.js index f3bc8e575f2..34a1752fd68 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/pos.js +++ b/addons/l10n_fr_pos_cert/static/src/js/pos.js @@ -1,11 +1,10 @@ /** @odoo-module */ import { PosGlobalState, Order, Orderline } from "@point_of_sale/js/models"; -import core from "web.core"; +import { _t } from "@web/core/l10n/translation"; import { patch } from "@web/core/utils/patch"; import { sprintf } from "@web/core/utils/strings"; import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; -var _t = core._t; patch(PosGlobalState.prototype, "l10n_fr_pos_cert.PosGlobalState", { is_french_country() { 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 6c977b60bee..ac3b0e03ae8 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 @@ -1,6 +1,6 @@ /** @odoo-module */ -import { parse } from "web.field_utils"; +import { parseFloat } from "@web/views/fields/parsers"; import { Transition } from "@web/core/transition"; import { constrain, getLimits, useMovable } from "@point_of_sale/app/movable_hook"; import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; @@ -65,7 +65,7 @@ export class DebugWidget extends Component { this.state.isShown = !this.state.isShown; } setWeight() { - var weightInKg = parse.float(this.state.weightInput); + var weightInKg = parseFloat(this.state.weightInput); if (!isNaN(weightInKg)) { this.hardwareProxy.setDebugWeight(weightInKg); } 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 index 840f5e1c80f..3818b50801e 100644 --- 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 @@ -1,9 +1,11 @@ /** @odoo-module */ -import { Printer } from "@point_of_sale/js/printers"; + +import { HWPrinter } from "../printer/hw_printer"; 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"; +import { deduceUrl } from "@point_of_sale/js/utils"; /** * This object interfaces with the local proxy to communicate to the various hardware devices @@ -27,7 +29,7 @@ export class HardwareProxy extends EventBus { effect( (info) => { if (info.status === "connected" && this.printer) { - this.printer.print_receipt(); + this.printer.printReceipt(); } }, [this.connectionInfo] @@ -68,7 +70,7 @@ export class HardwareProxy extends EventBus { } connectToPrinter() { - this.printer = new Printer(this.host, this.pos); + this.printer = new HWPrinter({ rpc: this.rpc, url: this.host }); } /** @@ -88,13 +90,7 @@ export class HardwareProxy extends EventBus { 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)); - } + url = deduceUrl(url); if (await this.checkProxyAvailability(url)) { this.host = url; 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 250672d14b8..137b0173959 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 @@ -2,7 +2,7 @@ import { _t } from "@web/core/l10n/translation"; import { sprintf } from "@web/core/utils/strings"; -import { renderToString } from "@web/core/utils/render"; +import { renderToElement } from "@web/core/utils/render"; import { useAutofocus, useService } from "@web/core/utils/hooks"; import { parseFloat, InvalidNumberError } from "@web/views/fields/parsers"; import { useState } from "@odoo/owl"; @@ -66,7 +66,7 @@ export class CashMovePopup extends AbstractAwaitablePopup { extras, ]); if (this.hardwareProxy.printer) { - const renderedReceipt = renderToString("point_of_sale.CashMoveReceipt", { + const renderedReceipt = renderToElement("point_of_sale.CashMoveReceipt", { _receipt: { type, reason, @@ -77,7 +77,7 @@ export class CashMovePopup extends AbstractAwaitablePopup { company: this.pos.globalState.company, }, }); - const printResult = await this.hardwareProxy.printer.print_receipt(renderedReceipt); + const printResult = await this.hardwareProxy.printer.printReceipt(renderedReceipt); if (!printResult.successful) { this.popup.add(ErrorPopup, { title: printResult.message.title, diff --git a/addons/point_of_sale/static/src/app/number_buffer_service.js b/addons/point_of_sale/static/src/app/number_buffer_service.js index 885c9381e19..abfc054c29c 100644 --- a/addons/point_of_sale/static/src/app/number_buffer_service.js +++ b/addons/point_of_sale/static/src/app/number_buffer_service.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import { parse } from "web.field_utils"; +import { parseFloat as oParseFloat } from "@web/views/fields/parsers"; import { barcodeService } from "@barcodes/barcode_service"; import { registry } from "@web/core/registry"; import { EventBus, onWillDestroy, useComponent } from "@odoo/owl"; @@ -109,7 +109,7 @@ class NumberBuffer extends EventBus { * @returns {number} float equivalent of the value of buffer */ getFloat() { - return parse.float(this.get()); + return oParseFloat(this.get()); } /** * @param {Object} config Use to setup the buffer @@ -284,8 +284,8 @@ class NumberBuffer extends EventBus { } } else if (input[0] === "+" && !isNaN(parseFloat(input))) { // when input is like '+10', '+50', etc - const inputValue = parse.float(input.slice(1)); - const currentBufferValue = this.state.buffer ? parse.float(this.state.buffer) : 0; + const inputValue = oParseFloat(input.slice(1)); + const currentBufferValue = this.state.buffer ? oParseFloat(this.state.buffer) : 0; this.state.buffer = this.component.env.pos.formatFixed(inputValue + currentBufferValue); } else if (!isNaN(parseInt(input, 10))) { if (this.state.toStartOver) { diff --git a/addons/point_of_sale/static/src/app/printer/base_printer.js b/addons/point_of_sale/static/src/app/printer/base_printer.js new file mode 100644 index 00000000000..e0d7a3dccdb --- /dev/null +++ b/addons/point_of_sale/static/src/app/printer/base_printer.js @@ -0,0 +1,124 @@ +/* @odoo-module */ +/* global html2canvas */ + +import { _t } from "@web/core/l10n/translation"; + +/** + * Implements basic printer functions. + */ +export class BasePrinter { + constructor() { + this.setup(...arguments); + } + + setup() { + this.receiptQueue = []; + } + + /** + * Add the receipt to the queue of receipts to be printed and process it. + * We clear the print queue if printing is not successful. + * @param {String} receipt: The receipt to be printed, in HTML + * @returns {{ successful: boolean; message?: { title: string; body?: string }}} + */ + async printReceipt(receipt) { + if (receipt) { + this.receiptQueue.push(receipt); + } + let image, printResult; + while (this.receiptQueue.length > 0) { + receipt = this.receiptQueue.shift(); + image = await this.htmlToImg(receipt); + try { + printResult = await this.sendPrintingJob(image); + } catch { + // Error in communicating to the IoT box. + this.receiptQueue.length = 0; + return this.getActionError(); + } + // rpc call is okay but printing failed because + // IoT box can't find a printer. + if (!printResult || printResult.result === false) { + this.receiptQueue.length = 0; + return this.getResultsError(printResult); + } + } + return { successful: true }; + } + + async sendPrintingJob() { + throw new Error("Not implemented"); + } + + openCashbox() { + throw new Error("Not implemented"); + } + + /** + * Generate a jpeg image from a canvas + * @param {DOMElement} canvas + */ + processCanvas(canvas) { + return canvas.toDataURL("image/jpeg").replace("data:image/jpeg;base64,", ""); + } + + /** + * This function renders `el` as an image for printing assuming the following + * - a `div.pos-receipt-print` is present in the dom and will serve as the container + * - a `.pos-receipt` is the root element of `el`. + * @param {HTMLElement} el The element to be converted as image + * @returns {HTMLCanvasElement} + */ + async htmlToImg(el) { + const elClone = el.cloneNode(true); + const receiptContainer = document.querySelector(".pos-receipt-print"); + receiptContainer.appendChild(elClone); + const receipt = receiptContainer.querySelector(".pos-receipt"); + // Odoo RTL support automatically flip left into right but html2canvas + // won't work as expected if the receipt is aligned to the right of the + // screen so we need to flip it back. + const parent = receipt.parentElement; + parent.style.left = 0; + parent.style.right = "auto"; + const canvas = await html2canvas(receipt, { + height: Math.ceil(receipt.clientHeight), + width: Math.ceil(receipt.clientWidth), + scale: 1, + }); + receipt.remove(); + return this.processCanvas(canvas); + } + + /** + * Return value of this method will be the result of calling `printReceipt` + * if it failed to connect to the IoT box. + */ + getActionError() { + return { + successful: false, + message: { + title: _t("Connection to IoT Box failed"), + body: _t("Please check if the IoT Box is still connected."), + }, + }; + } + + /** + * Return value of this method will be the result of calling `printReceipt` + * if the result coming from the IoT box is empty. + */ + getResultsError(_printResult) { + return { + successful: false, + message: { + title: _t("Connection to the printer failed"), + body: _t( + "Please check if the printer is still connected. \n" + + "Some browsers don't allow HTTP calls from websites to devices in the network (for security reasons). " + + "If it is the case, you will need to follow Odoo's documentation for " + + "'Self-signed certificate for ePOS printers' and 'Secure connection (HTTPS)' to solve the issue" + ), + }, + }; + } +} diff --git a/addons/point_of_sale/static/src/app/printer/hw_printer.js b/addons/point_of_sale/static/src/app/printer/hw_printer.js new file mode 100644 index 00000000000..477e4cc78a3 --- /dev/null +++ b/addons/point_of_sale/static/src/app/printer/hw_printer.js @@ -0,0 +1,39 @@ +/* @odoo-module */ + +import { BasePrinter } from "@point_of_sale/app/printer/base_printer"; + +/** + * Printer that sends print requests thru /hw_proxy endpoints. + * Doesn't require pos_iot to be installed. + */ +export class HWPrinter extends BasePrinter { + /** + * @param {Object} params + * @param {Function} params.rpc the web's rpc service. + * @param {string} params.url full address of the iot box. E.g. `http://10.23.45.67:8069`. + */ + setup(params) { + super.setup(...arguments); + const { rpc, url } = params; + this.rpc = rpc; + this.url = url; + } + + sendAction(data) { + return this.rpc(`${this.url}/hw_proxy/default_printer_action`, { data }); + } + + /** + * @override + */ + openCashbox() { + return this.sendAction({ action: "cashbox" }); + } + + /** + * @override + */ + sendPrintingJob(img) { + return this.sendAction({ action: "print_receipt", receipt: img }); + } +} diff --git a/addons/point_of_sale/static/src/backend/tours/point_of_sale.js b/addons/point_of_sale/static/src/backend/tours/point_of_sale.js index 1609eaaab35..fd450512132 100644 --- a/addons/point_of_sale/static/src/backend/tours/point_of_sale.js +++ b/addons/point_of_sale/static/src/backend/tours/point_of_sale.js @@ -1,40 +1,38 @@ /** @odoo-module */ -import { _t } from "web.core"; -import { Markup } from "web.utils"; +import { _t } from "@web/core/l10n/translation"; +import { markup } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { stepUtils } from "@web_tour/tour_service/tour_utils"; -registry.category("web_tour.tours").add( - "point_of_sale_tour", - { - url: "/web", - rainbowMan: false, - sequence: 45, - steps: [ +registry.category("web_tour.tours").add("point_of_sale_tour", { + url: "/web", + rainbowMan: false, + sequence: 45, + steps: [ stepUtils.showAppsMenuItem(), { trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]', - content: Markup(_t("Ready to launch your point of sale?")), + content: markup(_t("Ready to launch your point of sale?")), width: 215, position: "right", edition: "community", }, { trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]', - content: Markup(_t("Ready to launch your point of sale?")), + content: markup(_t("Ready to launch your point of sale?")), width: 215, position: "bottom", edition: "enterprise", }, { trigger: ".o_pos_kanban button.oe_kanban_action_button", - content: Markup( + content: markup( _t( "
Ready to have a look at the POS Interface? Let's start our first session.
" ) ), position: "bottom", }, - ] + ], }); diff --git a/addons/point_of_sale/static/src/css/pos_receipts.css b/addons/point_of_sale/static/src/css/pos_receipts.css index 1c245703870..9ac97fdb1bc 100644 --- a/addons/point_of_sale/static/src/css/pos_receipts.css +++ b/addons/point_of_sale/static/src/css/pos_receipts.css @@ -68,6 +68,10 @@ font-size: 75%; } +.pos-payment-terminal-receipt pre { + font-family: inherit; +} + .responsive-price { display: flex; flex-wrap: wrap; 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 e058c7ae9b9..07ec8fe58f3 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { useService } from "@web/core/utils/hooks"; -import { renderToString } from "@web/core/utils/render"; +import { renderToElement } from "@web/core/utils/render"; import { ErrorPopup } from "../Popups/ErrorPopup"; import { Component } from "@odoo/owl"; @@ -24,14 +24,14 @@ export class SaleDetailsButton extends Component { "get_sale_details", [false, false, false, [this.env.pos.pos_session.id]] ); - const report = renderToString( + const report = renderToElement( "SaleDetailsReport", Object.assign({}, saleDetails, { date: new Date().toLocaleString(), pos: this.env.pos, }) ); - const { successful, message } = await this.hardwareProxy.printer.print_receipt(report); + const { successful, message } = await this.hardwareProxy.printer.printReceipt(report); if (!successful) { await this.popup.add(ErrorPopup, { title: message.title, 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 15e3bd8e42a..4806556d125 100644 --- a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js @@ -20,8 +20,8 @@ export class AbstractReceiptScreen extends Component { } async _printReceipt() { if (this.hardwareProxy.printer) { - const printResult = await this.hardwareProxy.printer.print_receipt( - this.orderReceipt.el.innerHTML + const printResult = await this.hardwareProxy.printer.printReceipt( + this.orderReceipt.el.firstChild ); if (printResult.successful) { return true; diff --git a/addons/point_of_sale/static/src/js/Popups/NumberPopup.js b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js index e373d5565da..f8f9900f5d2 100644 --- a/addons/point_of_sale/static/src/js/Popups/NumberPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import core from "web.core"; -var _t = core._t; +import { _t } from "@web/core/l10n/translation"; import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { useService } from "@web/core/utils/hooks"; import { useState } from "@odoo/owl"; diff --git a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js index c522a9c2899..d20c5607a23 100644 --- a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js +++ b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js @@ -1,10 +1,10 @@ /** @odoo-module */ -import { _t } from "web.core"; -import { getDataURLFromFile } from "web.utils"; +import { _t } from "@web/core/l10n/translation"; +import { getDataURLFromFile } from "@web/core/utils/urls"; import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { useService } from "@web/core/utils/hooks"; -import { Component, onMounted, onWillUnmount } from "@odoo/owl"; +import { Component, onMounted, onWillUnmount } from "@odoo/owl"; export class PartnerDetailsEdit extends Component { static template = "PartnerDetailsEdit"; 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 704a45dc584..8584024e16c 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 @@ -1,10 +1,10 @@ /** @odoo-module */ -import { parse } from "web.field_utils"; +import { parseFloat } from "@web/views/fields/parsers"; import { useErrorHandlers } from "@point_of_sale/js/custom_hooks"; import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; -import utils from "web.utils"; +import { floatIsZero } from "@web/core/utils/numbers"; import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; @@ -135,7 +135,7 @@ export class PaymentScreen extends Component { this.render(true); } openCashbox() { - this.hardwareProxy.printer.open_cashbox(); + this.hardwareProxy.printer.openCashbox(); } async addTip() { // click_tip @@ -150,7 +150,7 @@ export class PaymentScreen extends Component { }); if (confirmed) { - this.currentOrder.set_tip(parse.float(payload)); + this.currentOrder.set_tip(parseFloat(payload)); } } async toggleShippingDatePicker() { @@ -220,7 +220,7 @@ export class PaymentScreen extends Component { (this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) && this.env.pos.config.iface_cashdrawer ) { - this.hardwareProxy.printer.open_cashbox(); + this.hardwareProxy.printer.openCashbox(); } this.currentOrder.initialize_validation_date(); @@ -475,10 +475,7 @@ export class PaymentScreen extends Component { // the current order is fully paid and due is zero. if ( this.currentOrder.is_paid() && - utils.float_is_zero( - this.currentOrder.get_due(), - this.env.pos.currency.decimal_places - ) && + floatIsZero(this.currentOrder.get_due(), this.env.pos.currency.decimal_places) && this.env.pos.config.auto_validate_terminal_payment ) { this.validateOrder(false); diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderSummary.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderSummary.js index 9da6a5025be..4297fb16677 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderSummary.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderSummary.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { Component } from "@odoo/owl"; -import { float_is_zero } from "web.utils"; +import { floatIsZero } from "@web/core/utils/numbers"; export class OrderSummary extends Component { static template = "OrderSummary"; @@ -14,7 +14,7 @@ export class OrderSummary extends Component { const totalWithoutTax = this.props.order.get_total_without_tax(); const taxAmount = total - totalWithoutTax; return { - hasTax: !float_is_zero(taxAmount, this.env.pos.currency.decimal_places), + hasTax: !floatIsZero(taxAmount, this.env.pos.currency.decimal_places), displayAmount: this.env.pos.format_currency(taxAmount), }; } 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 d13c0302bdd..7e71e1e5e5e 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 @@ -4,7 +4,7 @@ import { ControlButtonsMixin } from "@point_of_sale/js/ControlButtonsMixin"; import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { useBarcodeReader } from "@point_of_sale/app/barcode_reader_hook"; -import { parse } from "web.field_utils"; +import { parseFloat } from "@web/views/fields/parsers"; import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; @@ -97,7 +97,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { }); return; } - const parsedInput = (buffer && parse.float(buffer)) || 0; + const parsedInput = (buffer && parseFloat(buffer)) || 0; if (lastId != selectedLine.cid) { this._showDecreaseQuantityPopup(); } else if (currentQuantity < parsedInput) { @@ -203,7 +203,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { startingValue: 0, title: this.env._t("Set the new quantity"), }); - const newQuantity = inputNumber && inputNumber !== "" ? parse.float(inputNumber) : null; + const newQuantity = inputNumber && inputNumber !== "" ? parseFloat(inputNumber) : null; if (confirmed && newQuantity !== null) { const order = this.env.pos.get_order(); const selectedLine = this.env.pos.get_order().get_selected_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 1f4551af774..e90252ab228 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 @@ -1,7 +1,5 @@ /** @odoo-module */ -import { Printer } from "@point_of_sale/js/printers"; -import { is_email } from "web.utils"; import { useErrorHandlers } from "@point_of_sale/js/custom_hooks"; import { AbstractReceiptScreen } from "@point_of_sale/js/Misc/AbstractReceiptScreen"; import { OfflineErrorPopup } from "@point_of_sale/js/Popups/OfflineErrorPopup"; @@ -10,6 +8,7 @@ import { OrderReceipt } from "./OrderReceipt"; import { onMounted, useRef, status } from "@odoo/owl"; import { usePos } from "@point_of_sale/app/pos_hook"; import { useService } from "@web/core/utils/hooks"; +import { BasePrinter } from "@point_of_sale/app/printer/base_printer"; export class ReceiptScreen extends AbstractReceiptScreen { static template = "ReceiptScreen"; @@ -28,7 +27,6 @@ export class ReceiptScreen extends AbstractReceiptScreen { this.orderUiState = this.currentOrder.uiState.ReceiptScreen; this.orderUiState.inputEmail = this.orderUiState.inputEmail || (partner && partner.email) || ""; - this.is_email = is_email; onMounted(() => { // Here, we send a task to the event loop that handles @@ -59,7 +57,7 @@ export class ReceiptScreen extends AbstractReceiptScreen { this.buttonMailReceipt.el.className = "fa fa-fw fa-spin fa-circle-o-notch"; this.orderUiState.emailNotice = ""; - if (!is_email(this.orderUiState.inputEmail)) { + if (!this.isValidEmail()) { this.orderUiState.emailSuccessful = false; this.buttonMailReceipt.el.className = "fa fa-paper-plane"; this.orderUiState.emailNotice = this.env._t("Invalid email."); @@ -81,6 +79,10 @@ export class ReceiptScreen extends AbstractReceiptScreen { this.buttonMailReceipt.el.className = "fa fa-paper-plane"; }, 1000); } + isValidEmail() { + // A basic check of whether the `inputEmail` is an email or not. + return /^.+@.+$/.test(this.orderUiState.inputEmail); + } get orderAmountPlusTip() { const order = this.currentOrder; const orderTotalAmount = order.get_total_with_tax(); @@ -165,8 +167,8 @@ export class ReceiptScreen extends AbstractReceiptScreen { ); } async _sendReceiptToCustomer() { - const printer = new Printer(null, this.env.pos); - const receiptString = this.orderReceipt.el.innerHTML; + const printer = new BasePrinter(); + const receiptString = this.orderReceipt.el.firstChild; const ticketImage = await printer.htmlToImg(receiptString); const order = this.currentOrder; const partner = order.get_partner(); 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 5c9bf046846..e62a40120b2 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 @@ -1,6 +1,6 @@ /** @odoo-module */ -import { round_precision as round_pr } from "web.utils"; +import { roundPrecision as round_pr } from "@web/core/utils/numbers"; import { registry } from "@web/core/registry"; import { usePos } from "@point_of_sale/app/pos_hook"; import { Component, onMounted, onWillUnmount, useExternalListener, useState } from "@odoo/owl"; diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderlineDetails.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderlineDetails.js index ee1d486fdf7..95fa217ce2e 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderlineDetails.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderlineDetails.js @@ -2,8 +2,8 @@ import { Component } from "@odoo/owl"; import { sprintf } from "@web/core/utils/strings"; -import { format } from "web.field_utils"; -import { round_precision as round_pr } from "web.utils"; +import { formatFloat } from "@web/views/fields/formatters"; +import { roundPrecision as round_pr } from "@web/core/utils/numbers"; /** * @props {pos.order.line} line @@ -19,7 +19,7 @@ export class OrderlineDetails extends Component { const decimals = this.env.pos.dp["Product Unit of Measure"]; const rounding = Math.max(unit.rounding, Math.pow(10, -decimals)); const roundedQuantity = round_pr(quantity, rounding); - return format.float(roundedQuantity, { digits: [69, decimals] }); + return formatFloat(roundedQuantity, { digits: [69, decimals] }); }; return { productName: line.get_full_product_name(), diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js index fd8680aa7a8..5b34e2a8161 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js @@ -4,7 +4,8 @@ import { Order } from "@point_of_sale/js/models"; import { IndependentToOrderScreen } from "@point_of_sale/js/Misc/IndependentToOrderScreen"; import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; -import { parse, format } from "web.field_utils"; +import { deserializeDateTime } from "@web/core/l10n/dates"; +import { parseFloat } from "@web/views/fields/parsers"; import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; @@ -19,6 +20,8 @@ import { usePos } from "@point_of_sale/app/pos_hook"; import { onMounted, useState } from "@odoo/owl"; import { sprintf } from "@web/core/utils/strings"; +const { DateTime } = luxon; + export class TicketScreen extends IndependentToOrderScreen { static template = "TicketScreen"; static components = { @@ -203,7 +206,7 @@ export class TicketScreen extends IndependentToOrderScreen { if (buffer == null || buffer == "") { toRefundDetail.qty = 0; } else { - const quantity = Math.abs(parse.float(buffer)); + const quantity = Math.abs(parseFloat(buffer)); if (quantity > refundableQty) { this.numberBuffer.reset(); this.popup.add(ErrorPopup, { @@ -671,11 +674,11 @@ export class TicketScreen extends IndependentToOrderScreen { const idsNotInCache = ordersInfo.filter( (orderInfo) => !(orderInfo[0] in this._state.syncedOrders.cache) ); - const idsNotUpToDate = ordersInfo.filter( - (orderInfo) => - format.datetime(moment(orderInfo[1]), {}, {}) > - format.datetime(moment(this._state.syncedOrders.cacheDate), {}, { timezone: false }) - ); + // If no cacheDate, then assume reasonable earlier date. + const cacheDate = this._state.syncedOrders.cacheDate || DateTime.fromMillis(0); + const idsNotUpToDate = ordersInfo.filter((orderInfo) => { + return deserializeDateTime(orderInfo[1]) > cacheDate; + }); const idsToLoad = idsNotInCache.concat(idsNotUpToDate).map((info) => info[0]); if (idsToLoad.length > 0) { const fetchedOrders = await this.orm.call("pos.order", "export_for_ui", [idsToLoad]); @@ -691,7 +694,7 @@ export class TicketScreen extends IndependentToOrderScreen { ); }); //Update the datetime indicator of the cache refresh - this._state.syncedOrders.cacheDate = new Date(); + this._state.syncedOrders.cacheDate = DateTime.local(); } const ids = ordersInfo.map((info) => info[0]); diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 64d3a32bfff..c642e20cf64 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -1,7 +1,7 @@ /** @odoo-module */ -import core from "web.core"; -import utils from "web.utils"; +import { unaccent } from "@web/core/utils/strings"; + /* The PosDB holds reference to data that is either * - static: does not change between pos reloads * - persistent : must stay between reloads ( orders ) @@ -18,10 +18,10 @@ import utils from "web.utils"; */ const CACHE = {}; -export const PosDB = core.Class.extend({ - name: "openerp_pos_db", //the prefix of the localstorage data - limit: 100, // the maximum number of results returned by a search - init: function (options) { +export class PosDB { + name = "openerp_pos_db"; //the prefix of the localstorage data + limit = 100; // the maximum number of results returned by a search + constructor(options) { options = options || {}; this.name = options.name || this.name; this.limit = options.limit || this.limit; @@ -48,7 +48,7 @@ export const PosDB = core.Class.extend({ this.category_childs = {}; this.category_parent = {}; this.category_search_string = {}; - }, + } /** * sets an uuid to prevent conflict in locally stored data between multiple PoS Configs. By @@ -57,14 +57,14 @@ export const PosDB = core.Class.extend({ * * @param {string} uuid Unique identifier of the PoS Config linked to the current session. */ - set_uuid: function (uuid) { + set_uuid(uuid) { this.name = this.name + "_" + uuid; - }, + } /* returns the category object from its id. If you pass a list of id as parameters, you get * a list of category objects. */ - get_category_by_id: function (categ_id) { + get_category_by_id(categ_id) { if (categ_id instanceof Array) { var list = []; for (var i = 0, len = categ_id.length; i < len; i++) { @@ -79,26 +79,26 @@ export const PosDB = core.Class.extend({ } else { return this.category_by_id[categ_id]; } - }, + } /* returns a list of the category's child categories ids, or an empty list * if a category has no childs */ - get_category_childs_ids: function (categ_id) { + get_category_childs_ids(categ_id) { return this.category_childs[categ_id] || []; - }, + } /* returns a list of all ancestors (parent, grand-parent, etc) categories ids * starting from the root category to the direct parent */ - get_category_ancestors_ids: function (categ_id) { + get_category_ancestors_ids(categ_id) { return this.category_ancestors[categ_id] || []; - }, + } /* returns the parent category's id of a category, or the root_category_id if no parent. * the root category is parent of itself. */ - get_category_parent_id: function (categ_id) { + get_category_parent_id(categ_id) { return this.category_parent[categ_id] || this.root_category_id; - }, + } /* adds categories definitions to the database. categories is a list of categories objects as * returned by the openerp server. Categories must be inserted before the products or the * product/ categories association may (will) not work properly */ - add_categories: function (categories) { + add_categories(categories) { var self = this; if (!this.category_by_id[this.root_category_id]) { this.category_by_id[this.root_category_id] = { @@ -132,8 +132,8 @@ export const PosDB = core.Class.extend({ } } make_ancestors(this.root_category_id, []); - }, - category_contains: function (categ_id, product_id) { + } + category_contains(categ_id, product_id) { var product = this.product_by_id[product_id]; if (product) { var cid = product.pos_categ_id[0]; @@ -143,9 +143,9 @@ export const PosDB = core.Class.extend({ return !!cid; } return false; - }, + } /* loads a record store from the database. returns default if nothing is found */ - load: function (store, deft) { + load(store, deft) { if (CACHE[store] !== undefined) { return CACHE[store]; } @@ -157,13 +157,13 @@ export const PosDB = core.Class.extend({ } else { return deft; } - }, + } /* saves a record store to the database */ - save: function (store, data) { + save(store, data) { localStorage[this.name + "_" + store] = JSON.stringify(data); CACHE[store] = data; - }, - _product_search_string: function (product) { + } + _product_search_string(product) { var str = product.display_name; if (product.barcode) { str += "|" + product.barcode; @@ -179,8 +179,8 @@ export const PosDB = core.Class.extend({ } str = product.id + ":" + str.replace(/[\n:]/g, "") + "\n"; return str; - }, - add_products: function (products) { + } + add_products(products) { var stored_categories = this.product_by_category_id; if (!(products instanceof Array)) { @@ -192,7 +192,7 @@ export const PosDB = core.Class.extend({ continue; } if (product.available_in_pos) { - var search_string = utils.unaccent(this._product_search_string(product)); + var search_string = unaccent(this._product_search_string(product)); var categ_id = product.pos_categ_id ? product.pos_categ_id[0] : this.root_category_id; @@ -227,8 +227,8 @@ export const PosDB = core.Class.extend({ this.product_by_barcode[product.barcode] = product; } } - }, - add_packagings: function (product_packagings) { + } + add_packagings(product_packagings) { var self = this; Object.values(product_packagings || {}).map((product_packaging) => { const products = Object.values(self.product_by_id); @@ -236,8 +236,8 @@ export const PosDB = core.Class.extend({ self.product_packaging_by_barcode[product_packaging.barcode] = product_packaging; } }); - }, - _partner_search_string: function (partner) { + } + _partner_search_string(partner) { var str = partner.name || ""; if (partner.barcode) { str += "|" + partner.barcode; @@ -262,8 +262,8 @@ export const PosDB = core.Class.extend({ } str = "" + partner.id + ":" + str.replace(":", "").replace(/\n/g, " ") + "\n"; return str; - }, - add_partners: function (partners) { + } + add_partners(partners) { var updated = {}; var new_write_date = ""; var partner; @@ -335,20 +335,20 @@ export const PosDB = core.Class.extend({ searchString += this._partner_search_string(partner); } - this.partner_search_strings[chunkId] = utils.unaccent(searchString); + this.partner_search_strings[chunkId] = unaccent(searchString); } return Object.keys(updated).length; - }, - get_partner_write_date: function () { + } + get_partner_write_date() { return this.partner_write_date || "1970-01-01 00:00:00"; - }, - get_partner_by_id: function (id) { + } + get_partner_by_id(id) { return this.partner_by_id[id]; - }, - get_partner_by_barcode: function (barcode) { + } + get_partner_by_barcode(barcode) { return this.partner_by_barcode[barcode]; - }, - get_partners_sorted: function (max_count) { + } + get_partners_sorted(max_count) { max_count = max_count ? Math.min(this.partner_sorted.length, max_count) : this.partner_sorted.length; @@ -357,13 +357,13 @@ export const PosDB = core.Class.extend({ partners.push(this.partner_by_id[this.partner_sorted[i]]); } return partners; - }, - search_partner: function (query) { + } + search_partner(query) { try { // eslint-disable-next-line no-useless-escape query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g, "."); query = query.replace(/ /g, ".+"); - var re = RegExp("([0-9]+):.*?" + utils.unaccent(query), "gi"); + var re = RegExp("([0-9]+):.*?" + unaccent(query), "gi"); } catch { return []; } @@ -380,15 +380,15 @@ export const PosDB = core.Class.extend({ } } return results; - }, + } /* removes all the data from the database. TODO : being able to selectively remove data */ - clear: function () { + clear() { for (var i = 0, len = arguments.length; i < len; i++) { localStorage.removeItem(this.name + "_" + arguments[i]); } - }, + } /* this internal methods returns the count of properties in an object. */ - _count_props: function (obj) { + _count_props(obj) { var count = 0; for (var prop in obj) { if (Object.hasOwnProperty.call(obj, prop)) { @@ -396,19 +396,19 @@ export const PosDB = core.Class.extend({ } } return count; - }, - get_product_by_id: function (id) { + } + get_product_by_id(id) { return this.product_by_id[id]; - }, - get_product_by_barcode: function (barcode) { + } + get_product_by_barcode(barcode) { if (this.product_by_barcode[barcode]) { return this.product_by_barcode[barcode]; } else if (this.product_packaging_by_barcode[barcode]) { return this.product_by_id[this.product_packaging_by_barcode[barcode].product_id[0]]; } return undefined; - }, - get_product_by_category: function (category_id) { + } + get_product_by_category(category_id) { var product_ids = this.product_by_category_id[category_id]; var list = []; if (product_ids) { @@ -421,17 +421,17 @@ export const PosDB = core.Class.extend({ } } return list; - }, + } /* returns a list of products with : * - a category that is or is a child of category_id, * - a name, package or barcode containing the query (case insensitive) */ - search_product_in_category: function (category_id, query) { + search_product_in_category(category_id, query) { try { // eslint-disable-next-line no-useless-escape query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g, "."); query = query.replace(/ /g, ".+"); - var re = RegExp("([0-9]+):.*?" + utils.unaccent(query), "gi"); + var re = RegExp("([0-9]+):.*?" + unaccent(query), "gi"); } catch { return []; } @@ -450,12 +450,12 @@ export const PosDB = core.Class.extend({ } } return results; - }, + } /* from a product id, and a list of category ids, returns * true if the product belongs to one of the provided category * or one of its child categories. */ - is_product_in_category: function (category_ids, product_id) { + is_product_in_category(category_ids, product_id) { let cat = this.get_product_by_id(product_id).pos_categ_id[0]; while (cat) { for (const cat_id of category_ids) { @@ -467,10 +467,10 @@ export const PosDB = core.Class.extend({ cat = this.get_category_parent_id(cat); } return false; - }, + } /* paid orders */ - add_order: function (order) { + add_order(order) { var order_id = order.uid; var orders = this.load("orders", []); @@ -490,19 +490,19 @@ export const PosDB = core.Class.extend({ orders.push({ id: order_id, data: order }); this.save("orders", orders); return order_id; - }, - remove_order: function (order_id) { + } + remove_order(order_id) { var orders = this.load("orders", []); orders = orders.filter((order) => order.id !== order_id); this.save("orders", orders); - }, - remove_all_orders: function () { + } + remove_all_orders() { this.save("orders", []); - }, - get_orders: function () { + } + get_orders() { return this.load("orders", []); - }, - get_order: function (order_id) { + } + get_order(order_id) { var orders = this.get_orders(); for (var i = 0, len = orders.length; i < len; i++) { if (orders[i].id === order_id) { @@ -510,10 +510,10 @@ export const PosDB = core.Class.extend({ } } return undefined; - }, + } /* working orders */ - save_unpaid_order: function (order) { + save_unpaid_order(order) { var order_id = order.uid; var orders = this.load("unpaid_orders", []); var serialized = order.export_as_JSON(); @@ -529,36 +529,36 @@ export const PosDB = core.Class.extend({ orders.push({ id: order_id, data: serialized }); this.save("unpaid_orders", orders); return order_id; - }, - remove_unpaid_order: function (order) { + } + remove_unpaid_order(order) { var orders = this.load("unpaid_orders", []); orders = orders.filter((o) => o.id !== order.uid); this.save("unpaid_orders", orders); - }, - remove_all_unpaid_orders: function () { + } + remove_all_unpaid_orders() { this.save("unpaid_orders", []); - }, - get_unpaid_orders: function () { + } + get_unpaid_orders() { var saved = this.load("unpaid_orders", []); var orders = []; for (var i = 0; i < saved.length; i++) { orders.push(saved[i].data); } return orders; - }, + } /** * Return the orders with requested ids if they are unpaid. * @param {array