From 8d50daeefa4a208ca1d85a3da5c213f363be5dcd Mon Sep 17 00:00:00 2001 From: Joseph Caburnay Date: Tue, 25 Apr 2023 06:53:00 +0000 Subject: [PATCH] [REF] point_of_sale,*: minimize the use of legacy web modules *: l10n_fr_pos_cert,portal_rating,pos_adyen,pos_epson_printer,pos_loyalty, pos_mercury,pos_restaurant,pos_restaurant_adyen,pos_restaurant_stripe, pos_sale,pos_six,pos_stripe,web This is part of the continuous effort of refactoring pos addons towards using more modern modules and programming patterns (such as services). After this commit, point_of_sale addons are now left with the use of the legacy `web.concurrency` module because of the `MutexedDropPrevious`. It's okay to keep it because it's relatively an independent module compared to other legacy web module. The following summarizes the changes in this commit. - import `_t` from `@web/core/l10n/translation`. - convert `PosDB` to js native class - remove use of `format` in `TicketScreen` - To determine the cached orders are up-to-date, we now deserialize the dates coming from the server using web's `deserializeDateTime` function. Then, instead of initiating `cacheDate` as native js Date, we use the luxon's `DateTime` which is supported by the web date utility methods. - There is no need for the `format` function from `web.utils` legacy module. - convert `PaymentInterface` to native class - remove use of `web.config` module - remove use of `web.time` module - use `serializeDateTime` from web. - remove use of `web.rpc` - remove use of `web.utils` module - introduce simple check for email address input - Replace use of `web.utils.Markup` with `@odoo/owl.markup`. - 'web.utils'.{round_decimals,round_precision,float_is_zero} copied to '@web/core/utils/numbers'.{roundDecimals,roundPrecision,floatIsZero}. - These helper functions are not removed from the web addon because they are also used from other addons that are not linked to pos. - `floatIsZero` is now computed by directly comparing the result of `roundDecimals` to zero. This works because rounding a decimal number which will result to zero will exactly give zero. - remove use of `web.field_utils` - Replace `web.field_utils.parse.float` with `@web/views/fields/parsers.parseFloat`. - Replace `web.field_utils.format.float` with `@web/views/fields/formatters.formatFloat`. - Replace `web.field_utils.format.date` with `@web/core/l10n/dates.formatDate`. - Replace `web.field_utils.format.datetime` with `@web/core/l10n/dates.formatDateTime`. - convert `PrinterMixin` and dependents to native class - `PrinterMixin` is converted to `BasePrinter`. - `Printer` is converted to `HWPrinter` (extending `BasePrinter`). - `EpsonPrinter` retained its name and is converted to extend `BasePrinter`. - Moreover, we also removed the convoluted `PrintResultGenerator`, replaced by simply creating object with the following signature: ```js { successful: boolean; message?: { title: string, body?: string } } ``` - remove use of `web.Session` - replace use of `qweb.render` - We use `renderToElement` as replacement to templates that produces valid html. - Note that `renderToElement` is introduced in `@web/core/utils/render` module which is extracted from the original `renderToString` method. - For the epson printer template, we kept the xml layout and manually add required xml element to contruct the xml that will be sent to the epson in making print requests. - use `Mutex` from `@web/core/utils/concurrency` - remove use of `Markup` when rendering receipt info (`ticket`) in order to render new lines. - Remove use of `jquery` in `htmlToImg`. closes odoo/odoo#117231 Related: odoo/enterprise#39078 Signed-off-by: Samuel Degueldre --- addons/l10n_fr_pos_cert/static/src/js/pos.js | 3 +- .../static/src/app/debug/debug_widget.js | 4 +- .../src/app/hardware_proxy/hardware_proxy.js | 16 +- .../navbar/cash_move_popup/cash_move_popup.js | 6 +- .../static/src/app/number_buffer_service.js | 8 +- .../static/src/app/printer/base_printer.js | 124 ++++++++ .../static/src/app/printer/hw_printer.js | 39 +++ .../static/src/backend/tours/point_of_sale.js | 24 +- .../static/src/css/pos_receipts.css | 4 + .../src/js/ChromeWidgets/SaleDetailsButton.js | 6 +- .../src/js/Misc/AbstractReceiptScreen.js | 4 +- .../static/src/js/Popups/NumberPopup.js | 3 +- .../PartnerListScreen/PartnerDetailsEdit.js | 6 +- .../js/Screens/PaymentScreen/PaymentScreen.js | 15 +- .../js/Screens/ProductScreen/OrderSummary.js | 4 +- .../js/Screens/ProductScreen/ProductScreen.js | 6 +- .../js/Screens/ReceiptScreen/ReceiptScreen.js | 14 +- .../src/js/Screens/ScaleScreen/ScaleScreen.js | 2 +- .../Screens/TicketScreen/OrderlineDetails.js | 6 +- .../js/Screens/TicketScreen/TicketScreen.js | 19 +- addons/point_of_sale/static/src/js/db.js | 180 +++++------ addons/point_of_sale/static/src/js/models.js | 74 ++--- addons/point_of_sale/static/src/js/payment.js | 33 +- .../point_of_sale/static/src/js/printers.js | 171 ----------- addons/point_of_sale/static/src/js/utils.js | 17 ++ .../Screens/ReceiptScreen/OrderReceipt.xml | 9 +- .../Screens/ReceiptScreen/ReceiptScreen.xml | 2 +- .../static/tests/tours/helpers/utils.js | 3 +- .../static/tests/tours/pricelist_tour.js | 3 +- .../static/src/js/portal_chatter.js | 4 +- .../pos_adyen/static/src/js/payment_adyen.js | 118 +++----- .../src/js/{printers.js => epson_printer.js} | 285 +++++++++--------- .../js/{pos_epson_printer.js => models.js} | 4 +- .../static/src/xml/epos_templates.xml | 19 +- .../static/src/js/models.js | 4 +- addons/pos_loyalty/static/src/js/Loyalty.js | 21 +- .../static/src/js/PaymentScreen.js | 2 +- .../static/src/js/Screens/TicketScreen.js | 6 +- .../static/src/js/Screens/TipScreen.js | 12 +- addons/pos_restaurant/static/src/js/models.js | 26 +- .../pos_restaurant/static/src/js/payment.js | 5 +- .../static/src/xml/TipReceipt.xml | 2 +- .../static/src/xml/multiprint.xml | 6 +- .../static/src/js/payment_adyen.js | 9 +- .../static/src/js/payment_stripe.js | 3 +- .../SaleOrderManagementScreen.js | 6 +- addons/pos_six/static/src/js/models.js | 2 +- addons/pos_six/static/src/js/payment_six.js | 54 ++-- .../static/src/js/payment_stripe.js | 149 ++++----- addons/web/static/src/core/utils/numbers.js | 51 ++++ addons/web/static/src/core/utils/render.js | 10 +- .../static/tests/core/utils/numbers_tests.js | 139 +++++++++ 52 files changed, 942 insertions(+), 800 deletions(-) create mode 100644 addons/point_of_sale/static/src/app/printer/base_printer.js create mode 100644 addons/point_of_sale/static/src/app/printer/hw_printer.js delete mode 100644 addons/point_of_sale/static/src/js/printers.js rename addons/pos_epson_printer/static/src/js/{printers.js => epson_printer.js} (59%) rename addons/pos_epson_printer/static/src/js/{pos_epson_printer.js => models.js} (71%) create mode 100644 addons/web/static/tests/core/utils/numbers_tests.js 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} ids order_ids. * @return {array} list of orders. */ - get_unpaid_orders_to_sync: function (ids) { + get_unpaid_orders_to_sync(ids) { const savedOrders = this.load("unpaid_orders", []); return savedOrders.filter( (order) => ids.includes(order.id) && (order.data.server_id || order.data.lines.length || order.data.statement_ids.length) ); - }, + } /** * Add a given order to the orders to be removed from the server. * @@ -566,28 +566,28 @@ export const PosDB = core.Class.extend({ * after syncing. This function will add the server_id of the order to a list of orders still to be removed. * @param {object} order object. */ - set_order_to_remove_from_server: function (order) { + set_order_to_remove_from_server(order) { if (order.server_id !== undefined) { var to_remove = this.load("unpaid_orders_to_remove", []); to_remove.push(order.server_id); this.save("unpaid_orders_to_remove", to_remove); } - }, + } /** * Get a list of server_ids of orders to be removed. * @return {array} list of server_ids. */ - get_ids_to_remove_from_server: function () { + get_ids_to_remove_from_server() { return this.load("unpaid_orders_to_remove", []); - }, + } /** * Remove server_ids from the list of orders to be removed. * @param {array} ids */ - set_ids_removed_from_server: function (ids) { + set_ids_removed_from_server(ids) { var to_remove = this.load("unpaid_orders_to_remove", []); to_remove = to_remove.filter((id) => !ids.includes(id)); this.save("unpaid_orders_to_remove", to_remove); - }, -}); + } +} diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index b2a4a6e70a9..5e2b4adf5ad 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -2,22 +2,29 @@ /* global waitForWebfonts */ import { PosDB } from "@point_of_sale/js/db"; -import config from "web.config"; -import field_utils from "web.field_utils"; -import time from "web.time"; -import utils from "web.utils"; +import { formatFloat } from "@web/views/fields/formatters"; +// FIXME POSREF - unify use of native parseFloat and web's parseFloat. We probably don't need the native version. +import { parseFloat as oParseFloat } from "@web/views/fields/parsers"; import { batched, uuidv4 } from "@point_of_sale/js/utils"; +import { formatDate, formatDateTime, serializeDateTime } from "@web/core/l10n/dates"; +import { + roundDecimals as round_di, + roundPrecision as round_pr, + floatIsZero, +} from "@web/core/utils/numbers"; import { ErrorPopup } from "./Popups/ErrorPopup"; import { ProductConfiguratorPopup } from "@point_of_sale/js/Popups/ProductConfiguratorPopup"; 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, sprintf } from "@web/core/utils/strings"; +import { sprintf } from "@web/core/utils/strings"; import { Mutex } from "@web/core/utils/concurrency"; import { memoize } from "@web/core/utils/functions"; import { _t } from "@web/core/l10n/translation"; import { renderToString } from "@web/core/utils/render"; +const { DateTime } = luxon; + /* Returns an array containing all elements of the given * array corresponding to the rule function {agg} and without duplicates * @@ -38,10 +45,6 @@ export function uniqueBy(array, agg) { return [...map.values()]; } -var round_di = utils.round_decimals; -var round_pr = utils.round_precision; -const Markup = utils.Markup; - /** * Gets a product image as a base64 string so that it can be sent to the * customer display, as the display won't be able to fetch it, since the image @@ -138,7 +141,6 @@ export class PosGlobalState extends PosModel { super.setup(...arguments); 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(); @@ -1443,11 +1445,11 @@ export class PosGlobalState extends PosModel { } isProductQtyZero(qty) { - return utils.float_is_zero(qty, this.dp["Product Unit of Measure"]); + return floatIsZero(qty, this.dp["Product Unit of Measure"]); } formatProductQty(qty) { - return field_utils.format.float(qty, { + return formatFloat(qty, { digits: [true, this.dp["Product Unit of Measure"]], }); } @@ -1474,7 +1476,7 @@ export class PosGlobalState extends PosModel { if (typeof amount === "number") { amount = round_di(amount, decimals).toFixed(decimals); - amount = field_utils.format.float(round_di(amount, decimals), { + amount = formatFloat(round_di(amount, decimals), { digits: [69, decimals], }); } @@ -1934,7 +1936,7 @@ export class Orderline extends PosModel { ? discount : isNaN(parseFloat(discount)) ? 0 - : field_utils.parse.float("" + discount); + : oParseFloat("" + discount); var disc = Math.min(Math.max(parsed_discount || 0, 0), 100); this.discount = disc; this.discountStr = "" + disc; @@ -1977,7 +1979,7 @@ export class Orderline extends PosModel { var quant = typeof quantity === "number" ? quantity - : field_utils.parse.float("" + (quantity ? quantity : 0)); + : oParseFloat("" + (quantity ? quantity : 0)); if (this.refunded_orderline_id in this.pos.toRefundLines) { const toRefundDetail = this.pos.toRefundLines[this.refunded_orderline_id]; const maxQtyToRefund = @@ -2013,7 +2015,7 @@ export class Orderline extends PosModel { var decimals = this.pos.dp["Product Unit of Measure"]; var rounding = Math.max(unit.rounding, Math.pow(10, -decimals)); this.quantity = round_pr(quant, rounding); - this.quantityStr = field_utils.format.float(this.quantity, { + this.quantityStr = formatFloat(this.quantity, { digits: [69, decimals], }); } else { @@ -2140,7 +2142,7 @@ export class Orderline extends PosModel { this.get_unit().is_pos_groupable && // don't merge discounted orderlines this.get_discount() === 0 && - utils.float_is_zero( + floatIsZero( price - order_line_price - orderline.get_price_extra(), this.pos.currency.decimal_places ) && @@ -2247,7 +2249,7 @@ export class Orderline extends PosModel { ? price : isNaN(parseFloat(price)) ? 0 - : field_utils.parse.float("" + price); + : oParseFloat("" + price); this.price = round_di(parsed_price || 0, this.pos.dp["Product Price"]); } get_unit_price() { @@ -2573,7 +2575,7 @@ export class Payment extends PosModel { return this.amount; } get_amount_str() { - return field_utils.format.float(this.amount, { + return formatFloat(this.amount, { digits: [69, this.pos.currency.decimal_places], }); } @@ -2632,7 +2634,7 @@ export class Payment extends PosModel { //exports as JSON for server communication export_as_JSON() { return { - name: time.datetime_to_str(new Date()), + name: serializeDateTime(DateTime.local()), payment_method_id: this.payment_method.id, amount: this.get_amount(), payment_status: this.payment_status, @@ -2645,12 +2647,11 @@ export class Payment extends PosModel { } //exports as JSON for receipt printing export_for_printing() { - const ticket = escape(this.ticket).replace(/\n/g, "
"); // formatting return { cid: this.cid, amount: this.get_amount(), name: this.name, - ticket: Markup(ticket), + ticket: this.ticket, }; } // If payment status is a non-empty string, then it is an electronic payment. @@ -2860,17 +2861,8 @@ export class Order extends PosModel { return json; } _exportShippingDateForPrinting() { - const shippingDate = new Date(this.shippingDate); - const localeShippingDate = field_utils.format.date( - moment(shippingDate), - {}, - { timezone: false } - ); - const exportedDate = { - localestring: localeShippingDate, - validationDate: shippingDate, - }; - return exportedDate; + const shippingDate = DateTime.fromJSDate(new Date(this.shippingDate)); + return formatDate(shippingDate); } export_for_printing() { var orderlines = []; @@ -2951,11 +2943,7 @@ export class Order extends PosModel { receipt.footer = (isHeaderOrFooter && this.pos.config.receipt_footer) || ""; if (!receipt.date.localestring && (!this.state || this.state == "draft")) { - receipt.date.localestring = field_utils.format.datetime( - moment(new Date()), - {}, - { timezone: false } - ); + receipt.date.localestring = formatDateTime(DateTime.local()); } return receipt; @@ -3112,11 +3100,7 @@ export class Order extends PosModel { initialize_validation_date() { this.validation_date = new Date(); - this.formatted_validation_date = field_utils.format.datetime( - moment(this.validation_date), - {}, - { timezone: false } - ); + this.formatted_validation_date = formatDateTime(DateTime.fromJSDate(this.validation_date)); } set_tip(tip) { @@ -3591,7 +3575,7 @@ export class Order extends PosModel { var rounding_applied = total - remaining; // because floor and ceil doesn't include decimals in calculation, we reuse the value of the half-up and adapt it. - if (utils.float_is_zero(rounding_applied, this.pos.currency.decimal_places)) { + if (floatIsZero(rounding_applied, this.pos.currency.decimal_places)) { // https://xkcd.com/217/ return 0; } else if ( @@ -3632,7 +3616,7 @@ export class Order extends PosModel { } if ( - !utils.float_is_zero( + !floatIsZero( line.amount - round_pr(line.amount, this.pos.cash_rounding[0].rounding), 6 ) diff --git a/addons/point_of_sale/static/src/js/payment.js b/addons/point_of_sale/static/src/js/payment.js index c8ba7149096..d2e48c2601f 100644 --- a/addons/point_of_sale/static/src/js/payment.js +++ b/addons/point_of_sale/static/src/js/payment.js @@ -1,14 +1,10 @@ /** @odoo-module */ -import core from "web.core"; - /** * Implement this interface to support a new payment method in the POS: * - import const PaymentInterface from "@point_of_sale/js/payment"; - * var MyPayment = PaymentInterface.extend({ - * ... - * }) + * import { PaymentInterface } from "@point_of_sale/js/payment"; + * class MyPayment extends PaymentInterface {} * * To connect the interface to the right payment methods register it: * @@ -22,22 +18,25 @@ import core from "web.core"; * by overriding the loader_params of the models in the back end * in the `pos.session` model */ -// FIXME POSREF convert this to a class. -export const PaymentInterface = core.Class.extend({ - init: function (pos, payment_method) { +export class PaymentInterface { + constructor(pos, payment_method) { + this.setup(pos, payment_method); + } + + setup(pos, payment_method) { this.pos = pos; this.payment_method = payment_method; this.supports_reversals = false; - }, + } /** * Call this function to enable UI elements that allow a user to * reverse a payment. This requires that you implement * send_payment_reversal. */ - enable_reversals: function () { + enable_reversals() { this.supports_reversals = true; - }, + } /** * Called when a user clicks the "Send" button in the @@ -55,7 +54,7 @@ export const PaymentInterface = core.Class.extend({ * the payment should be retried. Rejected when the status of the * paymentline will be manually updated. */ - send_payment_request: function (cid) {}, + send_payment_request(cid) {} /** * Called when a user removes a payment line that's still waiting @@ -69,7 +68,7 @@ export const PaymentInterface = core.Class.extend({ * @param {string} cid - The id of the paymentline * @returns {Promise} */ - send_payment_cancel: function (order, cid) {}, + send_payment_cancel(order, cid) {} /** * This is an optional method. When implementing this make sure to @@ -81,12 +80,12 @@ export const PaymentInterface = core.Class.extend({ * @param {string} cid - The id of the paymentline * @returns {Promise} returns true if the reversal was successful. */ - send_payment_reversal: function (cid) {}, + send_payment_reversal(cid) {} /** * Called when the payment screen in the POS is closed (by * e.g. clicking the "Back" button). Could be used to cancel in * progress payments. */ - close: function () {}, -}); + close() {} +} diff --git a/addons/point_of_sale/static/src/js/printers.js b/addons/point_of_sale/static/src/js/printers.js deleted file mode 100644 index 9a8fee1bf61..00000000000 --- a/addons/point_of_sale/static/src/js/printers.js +++ /dev/null @@ -1,171 +0,0 @@ -/** @odoo-module */ -/* global html2canvas */ - -import Session from "web.Session"; -import core from "web.core"; -import { ErrorPopup } from "./Popups/ErrorPopup"; -var _t = core._t; - -// IMPROVEMENT: This is too much. We can get away from this class. -export class PrintResult { - constructor({ successful, message }) { - this.successful = successful; - this.message = message; - } -} - -export class PrintResultGenerator { - IoTActionError() { - return new PrintResult({ - successful: false, - message: { - title: _t("Connection to IoT Box failed"), - body: _t("Please check if the IoT Box is still connected."), - }, - }); - } - IoTResultError() { - return new PrintResult({ - 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" - ), - }, - }); - } - Successful() { - return new PrintResult({ - successful: true, - }); - } -} - -export const PrinterMixin = { - init: function (pos) { - this.receipt_queue = []; - this.printResultGenerator = new PrintResultGenerator(); - this.pos = pos; - }, - - /** - * 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 {PrintResult} - */ - print_receipt: async function (receipt) { - if (receipt) { - this.receipt_queue.push(receipt); - } - let image, sendPrintResult; - while (this.receipt_queue.length > 0) { - receipt = this.receipt_queue.shift(); - image = await this.htmlToImg(receipt); - try { - sendPrintResult = await this.send_printing_job(image); - } catch { - // Error in communicating to the IoT box. - this.receipt_queue.length = 0; - return this.printResultGenerator.IoTActionError(); - } - // rpc call is okay but printing failed because - // IoT box can't find a printer. - if (!sendPrintResult || sendPrintResult.result === false) { - this.receipt_queue.length = 0; - return this.printResultGenerator.IoTResultError(sendPrintResult.printerErrorCode); - } - } - return this.printResultGenerator.Successful(); - }, - - /** - * Generate a jpeg image from a canvas - * @param {DOMElement} canvas - */ - process_canvas: function (canvas) { - return canvas.toDataURL("image/jpeg").replace("data:image/jpeg;base64,", ""); - }, - - /** - * Renders the html as an image to print it - * @param {String} receipt: The receipt to be printed, in HTML - */ - htmlToImg: function (receipt) { - $(".pos-receipt-print").html(receipt); - this.receipt = $(".pos-receipt-print>.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. - this.receipt.parent().css({ left: 0, right: "auto" }); - return html2canvas(this.receipt[0], { - height: Math.ceil(this.receipt.outerHeight() + this.receipt.offset().top), - width: Math.ceil(this.receipt.outerWidth() + 2 * this.receipt.offset().left), - scale: 1, - }).then((canvas) => { - $(".pos-receipt-print").empty(); - return this.process_canvas(canvas); - }); - }, - - _onIoTActionResult: function (data) { - if (this.pos && (data === false || data.result === false)) { - this.pos.env.services.popup.add(ErrorPopup, { - title: _t("Connection to the printer failed"), - body: _t("Please check if the printer is still connected."), - }); - } - }, - - _onIoTActionFail: function () { - if (this.pos) { - this.pos.env.services.popup.add(ErrorPopup, { - title: _t("Connection to IoT Box failed"), - body: _t("Please check if the IoT Box is still connected."), - }); - } - }, -}; - -export const Printer = core.Class.extend(PrinterMixin, { - init: function (url, pos) { - PrinterMixin.init.call(this, pos); - this.connection = new Session(undefined, url || "http://localhost:8069", { - use_cors: true, - }); - }, - - /** - * Sends a command to the connected proxy to open the cashbox - * (the physical box where you store the cash). Updates the status of - * the printer with the answer from the proxy. - */ - open_cashbox: function () { - var self = this; - return this.connection - .rpc("/hw_proxy/default_printer_action", { - data: { - action: "cashbox", - }, - }) - .then(self._onIoTActionResult.bind(self)) - .guardedCatch(self._onIoTActionFail.bind(self)); - }, - - /** - * Sends the printing command the connected proxy - * @param {String} img : The receipt to be printed, as an image - */ - send_printing_job: function (img) { - return this.connection.rpc("/hw_proxy/default_printer_action", { - data: { - action: "print_receipt", - receipt: img, - }, - }); - }, -}); diff --git a/addons/point_of_sale/static/src/js/utils.js b/addons/point_of_sale/static/src/js/utils.js index 499a82d0b7d..8a0473ad674 100644 --- a/addons/point_of_sale/static/src/js/utils.js +++ b/addons/point_of_sale/static/src/js/utils.js @@ -37,3 +37,20 @@ export function uuidv4() { return v.toString(16); }); } + +/** + * Formats the given `url` with correct protocol and port. + * Useful for communicating to local iot box instance. + * @param {string} url + * @returns {string} + */ +export function deduceUrl(url) { + const { protocol } = window.location; + if (!url.includes("//")) { + url = `${protocol}//${url}`; + } + if (url.indexOf(":", 6) < 0) { + url += ":" + (protocol === "https:" ? 443 : 8069); + } + return url; +} diff --git a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml index 21932ec5c18..d5a07e6b810 100644 --- a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml +++ b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml @@ -150,7 +150,7 @@
- +
                         
@@ -160,12 +160,7 @@
Expected delivery: - -
-
- -
-
+
diff --git a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml index 26d25c32db0..2973ec810ea 100644 --- a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml +++ b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml @@ -22,7 +22,7 @@
-