From b119cca37b2f345e17ef62de9aacb638e1c075ab Mon Sep 17 00:00:00 2001 From: Rahul Prajapati Date: Mon, 16 Jan 2023 13:49:30 +0000 Subject: [PATCH] [REF] point_of_sale, *: remove class and component registries *: l10n_co_pos, l10n_fr_pos_cert, l10n_gcc_pos, l10n_in_pos, l10n_sa_pos, pos_adyen, pos_discount, pos_epson_printer, pos_epson_printer_restaurant, pos_hr, pos_hr_restaurant, pos_loyalty, pos_mercury, pos_restaurant, pos_sale, pos_sale, pos_sale, pos_sale, pos_sale, pos_sale, pos_sale_loyalty, pos_sale_product_configurator, pos_six, pos_six, pos_six, pos_six, pos_stripe, pos_stripe Previously, the point of sale module used a custom inheritance system for models and components. This system being non-standard, it makes it hard for newcomers to understand how to use it or for people with experience in the pos code base to understand how things are done elsewhere. This commit removes the "registries" system from the pos modules and adapts existing code to use standard inheritance mechanisms, ie extending classes directly, and using patch to modify behaviour in place. task-3119628 closes odoo/odoo#109928 Related: odoo/enterprise#35795 Signed-off-by: Samuel Degueldre Co-authored-by: Samuel Degueldre --- .../static/src/js/PaymentScreen.js | 44 +- addons/l10n_co_pos/static/src/js/pos.js | 52 +- .../l10n_fr_pos_cert/static/src/js/Chrome.js | 34 +- .../static/src/js/PaymentScreen.js | 44 +- .../static/src/js/Popups/ClosePosPopup.js | 37 +- addons/l10n_fr_pos_cert/static/src/js/pos.js | 153 +- .../static/src/js/OrderReceipt.js | 29 +- addons/l10n_in_pos/static/src/js/receipt.js | 18 +- addons/l10n_sa_pos/static/src/js/models.js | 140 +- .../src/app/debug_widget/debug_widget.js | 20 +- .../src/app/error_handlers/error_handlers.js | 24 +- .../static/src/app/navbar/navbar.js | 22 +- addons/point_of_sale/static/src/entry/main.js | 16 +- addons/point_of_sale/static/src/js/Chrome.js | 50 +- .../src/js/ChromeWidgets/CashMoveButton.js | 24 +- .../src/js/ChromeWidgets/CashierName.js | 12 +- .../CustomerFacingDisplayButton.js | 12 +- .../src/js/ChromeWidgets/HeaderButton.js | 15 +- .../src/js/ChromeWidgets/ProxyStatus.js | 12 +- .../src/js/ChromeWidgets/SaleDetailsButton.js | 15 +- .../src/js/ChromeWidgets/SyncNotification.js | 12 +- .../src/js/ChromeWidgets/TicketButton.js | 12 +- .../static/src/js/ClassRegistry.js | 260 -- .../static/src/js/ComponentRegistry.js | 27 - .../static/src/js/ControlButtonsMixin.js | 14 +- addons/point_of_sale/static/src/js/Gui.js | 2 + .../src/js/Misc/AbstractReceiptScreen.js | 15 +- .../static/src/js/Misc/CurrencyAmount.js | 12 +- .../static/src/js/Misc/Draggable.js | 12 +- .../src/js/Misc/IndependentToOrderScreen.js | 6 +- .../static/src/js/Misc/MobileOrderWidget.js | 13 +- .../static/src/js/Misc/NumberBuffer.js | 2 +- .../static/src/js/Misc/SearchBar.js | 11 +- .../static/src/js/Notification.js | 12 +- .../src/js/Popups/AbstractAwaitablePopup.js | 8 +- .../static/src/js/Popups/AlertPopup.js | 23 +- .../static/src/js/Popups/CashMovePopup.js | 11 +- .../static/src/js/Popups/CashOpeningPopup.js | 17 +- .../static/src/js/Popups/ClosePosPopup.js | 26 +- .../static/src/js/Popups/ConfirmPopup.js | 27 +- .../src/js/Popups/ControlButtonPopup.js | 25 +- .../static/src/js/Popups/DatePickerPopup.js | 22 +- .../static/src/js/Popups/EditListInput.js | 12 +- .../static/src/js/Popups/EditListPopup.js | 29 +- .../static/src/js/Popups/ErrorBarcodePopup.js | 33 +- .../static/src/js/Popups/ErrorPopup.js | 24 +- .../src/js/Popups/ErrorTracebackPopup.js | 32 +- .../static/src/js/Popups/MoneyDetailsPopup.js | 15 +- .../static/src/js/Popups/NumberPopup.js | 42 +- .../static/src/js/Popups/OfflineErrorPopup.js | 29 +- .../static/src/js/Popups/OrderImportPopup.js | 25 +- .../src/js/Popups/PosPopupController.js | 63 +- .../src/js/Popups/ProductConfiguratorPopup.js | 76 +- .../static/src/js/Popups/ProductInfoPopup.js | 12 +- .../static/src/js/Popups/SelectionPopup.js | 29 +- .../static/src/js/Popups/TextAreaPopup.js | 27 +- .../static/src/js/Popups/TextInputPopup.js | 28 +- .../static/src/js/PosComponent.js | 14 +- .../point_of_sale/static/src/js/Registries.js | 27 - .../PartnerListScreen/PartnerDetailsEdit.js | 19 +- .../Screens/PartnerListScreen/PartnerLine.js | 12 +- .../PartnerListScreen/PartnerListScreen.js | 17 +- .../PaymentScreen/PSNumpadInputButton.js | 12 +- .../js/Screens/PaymentScreen/PaymentScreen.js | 80 +- .../PaymentScreen/PaymentScreenNumpad.js | 15 +- .../PaymentScreenPaymentLines.js | 12 +- .../PaymentScreen/PaymentScreenStatus.js | 12 +- .../Screens/ProductScreen/ActionpadWidget.js | 18 +- .../Screens/ProductScreen/CategoryButton.js | 12 +- .../OrderlineCustomerNoteButton.js | 17 +- .../ControlButtons/ProductInfoButton.js | 17 +- .../ControlButtons/RefundButton.js | 14 +- .../ControlButtons/SetFiscalPositionButton.js | 17 +- .../ControlButtons/SetPricelistButton.js | 25 +- .../js/Screens/ProductScreen/NumpadWidget.js | 20 +- .../js/Screens/ProductScreen/OrderSummary.js | 12 +- .../js/Screens/ProductScreen/OrderWidget.js | 19 +- .../src/js/Screens/ProductScreen/Orderline.js | 12 +- .../js/Screens/ProductScreen/ProductItem.js | 12 +- .../js/Screens/ProductScreen/ProductScreen.js | 72 +- .../Screens/ProductScreen/ProductsWidget.js | 16 +- .../ProductsWidgetControlPanel.js | 15 +- .../js/Screens/ReceiptScreen/OrderReceipt.js | 15 +- .../js/Screens/ReceiptScreen/ReceiptScreen.js | 269 +- .../ReceiptScreen/WrappedProductNameLines.js | 12 +- .../src/js/Screens/ScaleScreen/ScaleScreen.js | 13 +- .../ControlButtons/InvoiceButton.js | 17 +- .../ControlButtons/ReprintReceiptButton.js | 11 +- .../js/Screens/TicketScreen/OrderDetails.js | 14 +- .../Screens/TicketScreen/OrderlineDetails.js | 12 +- .../TicketScreen/ReprintReceiptScreen.js | 54 +- .../js/Screens/TicketScreen/TicketScreen.js | 73 +- .../static/src/js/barcode_reader.js | 4 +- .../static/src/js/custom_hooks.js | 17 +- addons/point_of_sale/static/src/js/db.js | 4 +- addons/point_of_sale/static/src/js/models.js | 60 +- addons/point_of_sale/static/src/js/payment.js | 6 +- addons/point_of_sale/static/src/js/pos_env.js | 6 +- .../point_of_sale/static/src/js/printers.js | 5 +- .../src/xml/Popups/PosPopupController.xml | 4 +- .../static/tests/unit/helpers/test_env.js | 8 +- .../tests/unit/test_ComponentRegistry.js | 412 --- .../static/tests/unit/test_NumberBuffer.js | 10 +- .../tests/unit/test_PosPopupController.js | 79 +- .../pos_adyen/static/src/js/PaymentScreen.js | 63 +- addons/pos_adyen/static/src/js/models.js | 46 +- .../pos_adyen/static/src/js/payment_adyen.js | 9 +- .../static/src/js/DiscountButton.js | 20 +- .../static/src/js/pos_epson_printer.js | 26 +- .../static/src/js/printers.js | 4 +- .../static/src/js/models.js | 22 +- addons/pos_hr/static/src/app/navbar.js | 9 + addons/pos_hr/static/src/js/CashierName.js | 50 +- addons/pos_hr/static/src/js/Chrome.js | 45 +- .../pos_hr/static/src/js/HeaderLockButton.js | 12 +- addons/pos_hr/static/src/js/LoginScreen.js | 39 +- addons/pos_hr/static/src/js/PaymentScreen.js | 21 +- .../static/src/js/SelectCashierMixin.js | 122 +- addons/pos_hr/static/src/js/models.js | 143 +- .../pos_hr_restaurant/static/src/js/Chrome.js | 13 +- .../src/js/ControlButtons/PromoCodeButton.js | 14 +- .../js/ControlButtons/ResetProgramsButton.js | 11 +- .../src/js/ControlButtons/RewardButton.js | 16 +- .../src/js/ControlButtons/eWalletButton.js | 18 +- addons/pos_loyalty/static/src/js/Loyalty.js | 3076 ++++++++--------- .../pos_loyalty/static/src/js/OrderSummary.js | 19 +- .../pos_loyalty/static/src/js/OrderWidget.js | 75 +- addons/pos_loyalty/static/src/js/Orderline.js | 53 +- .../pos_loyalty/static/src/js/PartnerLine.js | 33 +- .../static/src/js/PartnerListScreen.js | 27 +- .../static/src/js/PaymentScreen.js | 315 +- .../static/src/js/ProductScreen.js | 482 +-- .../pos_loyalty/static/src/js/TicketScreen.js | 93 +- .../pos_mercury/static/src/js/OrderReceipt.js | 33 +- .../static/src/js/PaymentScreen.js | 903 +++-- .../src/js/PaymentScreenPaymentLines.js | 45 +- .../static/src/js/PaymentTransactionPopup.js | 24 +- .../static/src/js/PosPopupController.js | 9 + .../static/src/js/ProductScreen.js | 36 +- .../pos_mercury/static/src/js/pos_mercury.js | 258 +- .../pos_restaurant/static/src/app/navbar.js | 9 + addons/pos_restaurant/static/src/js/Chrome.js | 181 +- .../src/js/ChromeWidgets/BackToFloorButton.js | 12 +- .../src/js/ChromeWidgets/TicketButton.js | 69 +- .../src/js/Popups/PosPopupController.js | 41 +- .../static/src/js/Resizeable.js | 12 +- .../static/src/js/Screens/BillScreen.js | 43 +- .../src/js/Screens/FloorScreen/EditBar.js | 12 +- .../js/Screens/FloorScreen/EditableTable.js | 16 +- .../src/js/Screens/FloorScreen/FloorScreen.js | 30 +- .../src/js/Screens/FloorScreen/TableWidget.js | 12 +- .../static/src/js/Screens/PaymentScreen.js | 76 +- .../ControlButtons/OrderlineNoteButton.js | 17 +- .../ControlButtons/PrintBillButton.js | 14 +- .../ControlButtons/SplitBillButton.js | 14 +- .../ControlButtons/SubmitOrderButton.js | 17 +- .../ControlButtons/TableGuestsButton.js | 20 +- .../ControlButtons/TransferOrderButton.js | 14 +- .../src/js/Screens/ProductScreen/Orderline.js | 75 +- .../js/Screens/ProductScreen/ProductScreen.js | 2 +- .../js/Screens/ReceiptScreen/ReceiptScreen.js | 66 +- .../SplitBillScreen/SplitBillScreen.js | 18 +- .../Screens/SplitBillScreen/SplitOrderline.js | 12 +- .../static/src/js/Screens/TicketScreen.js | 321 +- .../static/src/js/Screens/TipScreen.js | 22 +- addons/pos_restaurant/static/src/js/models.js | 1239 ++++--- .../pos_restaurant/static/src/js/payment.js | 2 +- .../static/src/js/payment_adyen.js | 2 +- .../static/src/js/payment_stripe.js | 2 +- .../MobileSaleOrderManagementScreen.js | 31 +- .../OrderManagementScreen/SaleOrderFetcher.js | 2 +- .../js/OrderManagementScreen/SaleOrderList.js | 15 +- .../SaleOrderManagementControlPanel.js | 22 +- .../SaleOrderManagementScreen.js | 85 +- .../js/OrderManagementScreen/SaleOrderRow.js | 12 +- .../static/src/js/SetSaleOrderButton.js | 14 +- addons/pos_sale/static/src/js/models.js | 152 +- .../pos_sale_loyalty/static/src/js/models.js | 16 +- .../static/src/js/models.js | 42 +- addons/pos_six/__manifest__.py | 1 - addons/pos_six/static/app/navbar.js | 9 + addons/pos_six/static/src/js/BalanceButton.js | 12 +- addons/pos_six/static/src/js/payment_six.js | 11 +- addons/pos_stripe/static/src/js/models.js | 2 +- .../static/src/js/payment_stripe.js | 13 +- 185 files changed, 5671 insertions(+), 6763 deletions(-) delete mode 100644 addons/point_of_sale/static/src/js/ClassRegistry.js delete mode 100644 addons/point_of_sale/static/src/js/ComponentRegistry.js delete mode 100644 addons/point_of_sale/static/src/js/Registries.js delete mode 100644 addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js create mode 100644 addons/pos_hr/static/src/app/navbar.js create mode 100644 addons/pos_mercury/static/src/js/PosPopupController.js create mode 100644 addons/pos_restaurant/static/src/app/navbar.js create mode 100644 addons/pos_six/static/app/navbar.js diff --git a/addons/l10n_co_pos/static/src/js/PaymentScreen.js b/addons/l10n_co_pos/static/src/js/PaymentScreen.js index f4053645395..47225819dae 100644 --- a/addons/l10n_co_pos/static/src/js/PaymentScreen.js +++ b/addons/l10n_co_pos/static/src/js/PaymentScreen.js @@ -1,30 +1,26 @@ /** @odoo-module */ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; import session from "web.session"; -const L10nCoPosPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - async _postPushOrderResolve(order, order_server_ids) { - try { - if (this.env.pos.is_colombian_country()) { - const result = await this.rpc({ - model: "pos.order", - method: "search_read", - domain: [["id", "in", order_server_ids]], - fields: ["name"], - context: session.user_context, - }); - order.set_l10n_co_dian(result[0].name || false); - } - } catch { - // FIXME this doesn't seem correct but is equivalent to return in finally which we had before. +patch(PaymentScreen.prototype, "l10n_co_pos.PaymentScreen", { + async _postPushOrderResolve(order, order_server_ids) { + const _super = this._super; + try { + if (this.env.pos.is_colombian_country()) { + const result = await this.rpc({ + model: "pos.order", + method: "search_read", + domain: [["id", "in", order_server_ids]], + fields: ["name"], + context: session.user_context, + }); + order.set_l10n_co_dian(result[0].name || false); } - return super._postPushOrderResolve(...arguments); + } catch { + // FIXME this doesn't seem correct but is equivalent to return in finally which we had before. } - }; - -Registries.Component.extend(PaymentScreen, L10nCoPosPaymentScreen); - -export default PaymentScreen; + return _super(...arguments); + }, +}); diff --git a/addons/l10n_co_pos/static/src/js/pos.js b/addons/l10n_co_pos/static/src/js/pos.js index 386b5aefead..2abfbe8dc2c 100644 --- a/addons/l10n_co_pos/static/src/js/pos.js +++ b/addons/l10n_co_pos/static/src/js/pos.js @@ -1,33 +1,29 @@ /** @odoo-module */ import { PosGlobalState, Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const L10nCoPosGlobalState = (PosGlobalState) => - class L10nCoPosGlobalState extends PosGlobalState { - is_colombian_country() { - return this.company.country.code === "CO"; - } - }; -Registries.Model.extend(PosGlobalState, L10nCoPosGlobalState); +patch(PosGlobalState.prototype, "l10n_co_pos.PosGlobalState", { + is_colombian_country() { + return this.company.country.code === "CO"; + }, +}); -const L10nCoPosOrder = (Order) => - class L10nCoPosOrder extends Order { - export_for_printing() { - var result = super.export_for_printing(...arguments); - result.l10n_co_dian = this.get_l10n_co_dian(); - return result; - } - set_l10n_co_dian(l10n_co_dian) { - this.l10n_co_dian = l10n_co_dian; - } - get_l10n_co_dian() { - return this.l10n_co_dian; - } - wait_for_push_order() { - var result = super.wait_for_push_order(...arguments); - result = Boolean(result || this.pos.is_colombian_country()); - return result; - } - }; -Registries.Model.extend(Order, L10nCoPosOrder); +patch(Order.prototype, "l10n_co_pos.Order", { + export_for_printing() { + const result = this._super(...arguments); + result.l10n_co_dian = this.get_l10n_co_dian(); + return result; + }, + set_l10n_co_dian(l10n_co_dian) { + this.l10n_co_dian = l10n_co_dian; + }, + get_l10n_co_dian() { + return this.l10n_co_dian; + }, + wait_for_push_order() { + var result = this._super(...arguments); + result = Boolean(result || this.pos.is_colombian_country()); + return result; + }, +}); diff --git a/addons/l10n_fr_pos_cert/static/src/js/Chrome.js b/addons/l10n_fr_pos_cert/static/src/js/Chrome.js index 2221fc96b8a..cec7f81edef 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/Chrome.js +++ b/addons/l10n_fr_pos_cert/static/src/js/Chrome.js @@ -1,24 +1,20 @@ /** @odoo-module */ -import Chrome from "@point_of_sale/js/Chrome"; -import Registries from "@point_of_sale/js/Registries"; +import { Chrome } from "@point_of_sale/js/Chrome"; +import { patch } from "@web/core/utils/patch"; +import { ClosePosPopup } from "@point_of_sale/js/Popups/ClosePosPopup"; -const PosFrCertChrome = (Chrome) => - class extends Chrome { - async start() { - await super.start(); - if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) { - const now = Date.now(); - const limitDate = new Date(this.env.pos.pos_session.start_at); - limitDate.setDate(limitDate.getDate() + 1); - if (limitDate.getTime() < now) { - const info = await this.env.pos.getClosePosInfo(); - this.showPopup("ClosePosPopup", { info: info }); - } +patch(Chrome.prototype, "l10n_fr_pos_cert.Chrome", { + async start() { + await this._super(); + if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) { + const now = Date.now(); + const limitDate = new Date(this.env.pos.pos_session.start_at); + limitDate.setDate(limitDate.getDate() + 1); + if (limitDate.getTime() < now) { + const info = await this.env.pos.getClosePosInfo(); + this.showPopup(ClosePosPopup, { info: info }); } } - }; - -Registries.Component.extend(Chrome, PosFrCertChrome); - -export default Chrome; + }, +}); diff --git a/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js b/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js index 5cad1898023..f4a6485c388 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js +++ b/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js @@ -1,30 +1,26 @@ /** @odoo-module */ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; import session from "web.session"; -const PosFrPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - async _postPushOrderResolve(order, order_server_ids) { - try { - if (this.env.pos.is_french_country()) { - const result = await this.rpc({ - model: "pos.order", - method: "search_read", - domain: [["id", "in", order_server_ids]], - fields: ["l10n_fr_hash"], - context: session.user_context, - }); - order.set_l10n_fr_hash(result[0].l10n_fr_hash || false); - } - } catch { - // FIXME this doesn't seem correct but is equivalent to return in finally which we had before. +patch(PaymentScreen.prototype, "l10n_fr_pos_cert.PaymentScreen", { + async _postPushOrderResolve(order, order_server_ids) { + const _super = this._super; + try { + if (this.env.pos.is_french_country()) { + const result = await this.rpc({ + model: "pos.order", + method: "search_read", + domain: [["id", "in", order_server_ids]], + fields: ["l10n_fr_hash"], + context: session.user_context, + }); + order.set_l10n_fr_hash(result[0].l10n_fr_hash || false); } - return super._postPushOrderResolve(...arguments); + } catch { + // FIXME this doesn't seem correct but is equivalent to return in finally which we had before. } - }; - -Registries.Component.extend(PaymentScreen, PosFrPaymentScreen); - -export default PaymentScreen; + return _super(...arguments); + }, +}); diff --git a/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js b/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js index aa24ac24e2a..9559d68c91d 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js +++ b/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js @@ -1,25 +1,20 @@ /** @odoo-module */ -import ClosePosPopup from "@point_of_sale/js/Popups/ClosePosPopup"; -import Registries from "@point_of_sale/js/Registries"; +import { ClosePosPopup } from "@point_of_sale/js/Popups/ClosePosPopup"; +import { patch } from "@web/core/utils/patch"; -const PosFrCertClosePopup = (ClosePosPopup) => - class extends ClosePosPopup { - sessionIsOutdated() { - let isOutdated = false; - if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) { - const now = Date.now(); - const limitDate = new Date(this.env.pos.pos_session.start_at); - limitDate.setDate(limitDate.getDate() + 1); - isOutdated = limitDate < now; - } - return isOutdated; +patch(ClosePosPopup.prototype, "l10n_fr_pos_cert.ClosePosPopup", { + sessionIsOutdated() { + let isOutdated = false; + if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) { + const now = Date.now(); + const limitDate = new Date(this.env.pos.pos_session.start_at); + limitDate.setDate(limitDate.getDate() + 1); + isOutdated = limitDate < now; } - canCancel() { - return super.canCancel() && !this.sessionIsOutdated(); - } - }; - -Registries.Component.extend(ClosePosPopup, PosFrCertClosePopup); - -export default ClosePosPopup; + return isOutdated; + }, + canCancel() { + return this._super() && !this.sessionIsOutdated(); + }, +}); 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 2e85a7bacac..3633c585e71 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/pos.js +++ b/addons/l10n_fr_pos_cert/static/src/js/pos.js @@ -3,91 +3,86 @@ import { Gui } from "@point_of_sale/js/Gui"; import { PosGlobalState, Order, Orderline } from "@point_of_sale/js/models"; import core from "web.core"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; var _t = core._t; -const L10nFrPosGlobalState = (PosGlobalState) => - class L10nFrPosGlobalState extends PosGlobalState { - is_french_country() { - var french_countries = ["FR", "MF", "MQ", "NC", "PF", "RE", "GF", "GP", "TF"]; - if (!this.company.country) { - Gui.showPopup("ErrorPopup", { - title: _t("Missing Country"), - body: _.str.sprintf( - _t("The company %s doesn't have a country set."), - this.company.name - ), - }); - return false; - } - return _.contains(french_countries, this.company.country.code); +patch(PosGlobalState.prototype, "l10n_fr_pos_cert.PosGlobalState", { + is_french_country() { + var french_countries = ["FR", "MF", "MQ", "NC", "PF", "RE", "GF", "GP", "TF"]; + if (!this.company.country) { + Gui.showPopup(ErrorPopup, { + title: _t("Missing Country"), + body: _.str.sprintf( + _t("The company %s doesn't have a country set."), + this.company.name + ), + }); + return false; } - disallowLineQuantityChange() { - const result = super.disallowLineQuantityChange(...arguments); - return this.is_french_country() || result; - } - }; -Registries.Model.extend(PosGlobalState, L10nFrPosGlobalState); + return _.contains(french_countries, this.company.country.code); + }, + disallowLineQuantityChange() { + const result = this._super(...arguments); + return this.is_french_country() || result; + }, +}); -const L10nFrOrder = (Order) => - class L10nFrOrder extends Order { - constructor() { - super(...arguments); - this.l10n_fr_hash = this.l10n_fr_hash || false; - this.save_to_db(); +patch(Order.prototype, "l10n_fr_pos_cert.Order", { + setup() { + this._super(...arguments); + this.l10n_fr_hash = this.l10n_fr_hash || false; + this.save_to_db(); + }, + export_for_printing() { + var result = this._super(...arguments); + result.l10n_fr_hash = this.get_l10n_fr_hash(); + return result; + }, + set_l10n_fr_hash(l10n_fr_hash) { + this.l10n_fr_hash = l10n_fr_hash; + }, + get_l10n_fr_hash() { + return this.l10n_fr_hash; + }, + wait_for_push_order() { + var result = this._super(...arguments); + result = Boolean(result || this.pos.is_french_country()); + return result; + }, + destroy(option) { + // SUGGESTION: It's probably more appropriate to apply this restriction + // in the TicketScreen. + if ( + option && + option.reason == "abandon" && + this.pos.is_french_country() && + this.get_orderlines().length + ) { + Gui.showPopup(ErrorPopup, { + title: _t("Fiscal Data Module error"), + body: _t("Deleting of orders is not allowed."), + }); + } else { + this._super(...arguments); } - export_for_printing() { - var result = super.export_for_printing(...arguments); - result.l10n_fr_hash = this.get_l10n_fr_hash(); - return result; - } - set_l10n_fr_hash(l10n_fr_hash) { - this.l10n_fr_hash = l10n_fr_hash; - } - get_l10n_fr_hash() { - return this.l10n_fr_hash; - } - wait_for_push_order() { - var result = super.wait_for_push_order(...arguments); - result = Boolean(result || this.pos.is_french_country()); - return result; - } - destroy(option) { - // SUGGESTION: It's probably more appropriate to apply this restriction - // in the TicketScreen. - if ( - option && - option.reason == "abandon" && - this.pos.is_french_country() && - this.get_orderlines().length - ) { - Gui.showPopup("ErrorPopup", { - title: _t("Fiscal Data Module error"), - body: _t("Deleting of orders is not allowed."), - }); - } else { - super.destroy(...arguments); - } - } - }; -Registries.Model.extend(Order, L10nFrOrder); + }, +}); -const L10nFrOrderline = (Orderline) => - class L10nFrOrderline extends Orderline { - can_be_merged_with(orderline) { - const order = this.pos.get_order(); - const orderlines = order.orderlines; - const lastOrderline = order.orderlines.at(orderlines.length - 1); +patch(Orderline.prototype, "l10n_fr_pos_cert.Orderline", { + can_be_merged_with(orderline) { + const order = this.pos.get_order(); + const orderlines = order.orderlines; + const lastOrderline = order.orderlines.at(orderlines.length - 1); - if ( - this.pos.is_french_country() && - (lastOrderline.product.id !== orderline.product.id || lastOrderline.quantity < 0) - ) { - return false; - } else { - return super.can_be_merged_with(...arguments); - } + if ( + this.pos.is_french_country() && + (lastOrderline.product.id !== orderline.product.id || lastOrderline.quantity < 0) + ) { + return false; + } else { + return this._super(...arguments); } - }; -Registries.Model.extend(Orderline, L10nFrOrderline); + }, +}); diff --git a/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js b/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js index 7d34022c5b1..0fa1073d650 100644 --- a/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js +++ b/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js @@ -1,19 +1,16 @@ /** @odoo-module */ -import OrderReceipt from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt"; -import Registries from "@point_of_sale/js/Registries"; +import { OrderReceipt } from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt"; +import { patch } from "@web/core/utils/patch"; -const OrderReceiptGCC = (OrderReceipt) => - class extends OrderReceipt { - get receiptEnv() { - const receipt_render_env = super.receiptEnv; - const receipt = receipt_render_env.receipt; - const country = receipt_render_env.order.pos.company.country; - receipt.is_gcc_country = ["SA", "AE", "BH", "OM", "QA", "KW"].includes( - country && country.code - ); - return receipt_render_env; - } - }; -Registries.Component.extend(OrderReceipt, OrderReceiptGCC); -export default OrderReceiptGCC; +patch(OrderReceipt.prototype, "l10n_gcc_pos.OrderReceipt", { + get receiptEnv() { + const receipt_render_env = this._super(); + const receipt = receipt_render_env.receipt; + const country = receipt_render_env.order.pos.company.country; + receipt.is_gcc_country = ["SA", "AE", "BH", "OM", "QA", "KW"].includes( + country && country.code + ); + return receipt_render_env; + }, +}); diff --git a/addons/l10n_in_pos/static/src/js/receipt.js b/addons/l10n_in_pos/static/src/js/receipt.js index f3218cb2195..5203eae852f 100644 --- a/addons/l10n_in_pos/static/src/js/receipt.js +++ b/addons/l10n_in_pos/static/src/js/receipt.js @@ -1,14 +1,12 @@ /** @odoo-module */ import { Orderline } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const L10nInOrderline = (Orderline) => - class L10nInOrderline extends Orderline { - export_for_printing() { - var line = super.export_for_printing(...arguments); - line.l10n_in_hsn_code = this.get_product().l10n_in_hsn_code; - return line; - } - }; -Registries.Model.extend(Orderline, L10nInOrderline); +patch(Orderline.prototype, "l10n_in_pos.Orderline", { + export_for_printing() { + const line = this._super(...arguments); + line.l10n_in_hsn_code = this.get_product().l10n_in_hsn_code; + return line; + }, +}); diff --git a/addons/l10n_sa_pos/static/src/js/models.js b/addons/l10n_sa_pos/static/src/js/models.js index c1485b589e1..b16347a193f 100644 --- a/addons/l10n_sa_pos/static/src/js/models.js +++ b/addons/l10n_sa_pos/static/src/js/models.js @@ -1,79 +1,75 @@ /** @odoo-module */ import { Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const PosL10nSAOrder = (Order) => - class PosL10nSAOrder extends Order { - export_for_printing() { - var result = super.export_for_printing(...arguments); - if (this.pos.company.country.code === "SA") { - result.is_settlement = this.is_settlement(); - if (!result.is_settlement) { - const codeWriter = new window.ZXing.BrowserQRCodeSvgWriter(); - const qr_values = this.compute_sa_qr_code( - result.company.name, - result.company.vat, - result.date.isostring, - result.total_with_tax, - result.total_tax - ); - const qr_code_svg = new XMLSerializer().serializeToString( - codeWriter.write(qr_values, 150, 150) - ); - result.qr_code = "data:image/svg+xml;base64," + window.btoa(qr_code_svg); - } +patch(Order.prototype, "l10n_sa_pos.Order", { + export_for_printing() { + const result = this._super(...arguments); + if (this.pos.company.country.code === "SA") { + result.is_settlement = this.is_settlement(); + if (!result.is_settlement) { + const codeWriter = new window.ZXing.BrowserQRCodeSvgWriter(); + const qr_values = this.compute_sa_qr_code( + result.company.name, + result.company.vat, + result.date.isostring, + result.total_with_tax, + result.total_tax + ); + const qr_code_svg = new XMLSerializer().serializeToString( + codeWriter.write(qr_values, 150, 150) + ); + result.qr_code = "data:image/svg+xml;base64," + window.btoa(qr_code_svg); } - return result; } - /** - * If the order is empty (there are no products) - * and all "pay_later" payments are negative, - * we are settling a customer's account. - * If the module pos_settle_due is not installed, - * the function always returns false (since "pay_later" doesn't exist) - * @returns {boolean} true if the current order is a settlement, else false - */ - is_settlement() { - return ( - this.is_empty() && - !!this.paymentlines.filter( - (paymentline) => - paymentline.payment_method.type === "pay_later" && paymentline.amount < 0 - ).length - ); + return result; + }, + /** + * If the order is empty (there are no products) + * and all "pay_later" payments are negative, + * we are settling a customer's account. + * If the module pos_settle_due is not installed, + * the function always returns false (since "pay_later" doesn't exist) + * @returns {boolean} true if the current order is a settlement, else false + */ + is_settlement() { + return ( + this.is_empty() && + !!this.paymentlines.filter( + (paymentline) => + paymentline.payment_method.type === "pay_later" && paymentline.amount < 0 + ).length + ); + }, + compute_sa_qr_code(name, vat, date_isostring, amount_total, amount_tax) { + /* Generate the qr code for Saudi e-invoicing. Specs are available at the following link at page 23 + https://zatca.gov.sa/ar/E-Invoicing/SystemsDevelopers/Documents/20210528_ZATCA_Electronic_Invoice_Security_Features_Implementation_Standards_vShared.pdf + */ + const seller_name_enc = this._compute_qr_code_field(1, name); + const company_vat_enc = this._compute_qr_code_field(2, vat); + const timestamp_enc = this._compute_qr_code_field(3, date_isostring); + const invoice_total_enc = this._compute_qr_code_field(4, amount_total.toString()); + const total_vat_enc = this._compute_qr_code_field(5, amount_tax.toString()); + + const str_to_encode = seller_name_enc.concat( + company_vat_enc, + timestamp_enc, + invoice_total_enc, + total_vat_enc + ); + + let binary = ""; + for (let i = 0; i < str_to_encode.length; i++) { + binary += String.fromCharCode(str_to_encode[i]); } - - compute_sa_qr_code(name, vat, date_isostring, amount_total, amount_tax) { - /* Generate the qr code for Saudi e-invoicing. Specs are available at the following link at page 23 - https://zatca.gov.sa/ar/E-Invoicing/SystemsDevelopers/Documents/20210528_ZATCA_Electronic_Invoice_Security_Features_Implementation_Standards_vShared.pdf - */ - const seller_name_enc = this._compute_qr_code_field(1, name); - const company_vat_enc = this._compute_qr_code_field(2, vat); - const timestamp_enc = this._compute_qr_code_field(3, date_isostring); - const invoice_total_enc = this._compute_qr_code_field(4, amount_total.toString()); - const total_vat_enc = this._compute_qr_code_field(5, amount_tax.toString()); - - const str_to_encode = seller_name_enc.concat( - company_vat_enc, - timestamp_enc, - invoice_total_enc, - total_vat_enc - ); - - let binary = ""; - for (let i = 0; i < str_to_encode.length; i++) { - binary += String.fromCharCode(str_to_encode[i]); - } - return btoa(binary); - } - - _compute_qr_code_field(tag, field) { - const textEncoder = new TextEncoder(); - const name_byte_array = Array.from(textEncoder.encode(field)); - const name_tag_encoding = [tag]; - const name_length_encoding = [name_byte_array.length]; - return name_tag_encoding.concat(name_length_encoding, name_byte_array); - } - }; -Registries.Model.extend(Order, PosL10nSAOrder); + return btoa(binary); + }, + _compute_qr_code_field(tag, field) { + const textEncoder = new TextEncoder(); + const name_byte_array = Array.from(textEncoder.encode(field)); + const name_tag_encoding = [tag]; + const name_length_encoding = [name_byte_array.length]; + return name_tag_encoding.concat(name_length_encoding, name_byte_array); + }, +}); diff --git a/addons/point_of_sale/static/src/app/debug_widget/debug_widget.js b/addons/point_of_sale/static/src/app/debug_widget/debug_widget.js index be2b992a424..378e34bf120 100644 --- a/addons/point_of_sale/static/src/app/debug_widget/debug_widget.js +++ b/addons/point_of_sale/static/src/app/debug_widget/debug_widget.js @@ -1,12 +1,14 @@ /** @odoo-module */ import { parse } from "web.field_utils"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; -import PosComponent from "@point_of_sale/js/PosComponent"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { usePos } from "@point_of_sale/app/pos_store"; import { registry } from "@web/core/registry"; import { Transition } from "@web/core/transition"; -import Draggable from "@point_of_sale/js/Misc/Draggable"; +import { Draggable } from "@point_of_sale/js/Misc/Draggable"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; +import { OrderImportPopup } from "@point_of_sale/js/Popups/OrderImportPopup"; const { onMounted, onWillUnmount, useRef, useState } = owl; @@ -21,7 +23,7 @@ export class DebugWidget extends PosComponent { weightInput: "", isPaidOrdersReady: false, isUnpaidOrdersReady: false, - buffer: NumberBuffer.get(), + buffer: numberBuffer.get(), }); // NOTE: Perhaps this can still be improved. @@ -50,11 +52,11 @@ export class DebugWidget extends PosComponent { } onMounted(() => { - NumberBuffer.on("buffer-update", this, this._onBufferUpdate); + numberBuffer.on("buffer-update", this, this._onBufferUpdate); }); onWillUnmount(() => { - NumberBuffer.off("buffer-update", this, this._onBufferUpdate); + numberBuffer.off("buffer-update", this, this._onBufferUpdate); }); } toggleWidget() { @@ -81,7 +83,7 @@ export class DebugWidget extends PosComponent { await this.env.barcode_reader.scan(ean); } async deleteOrders() { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Delete Paid Orders ?"), body: this.env._t( "This operation will permanently destroy all paid orders from the local storage. You will lose all the data. This operation cannot be undone." @@ -93,7 +95,7 @@ export class DebugWidget extends PosComponent { } } async deleteUnpaidOrders() { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Delete Unpaid Orders ?"), body: this.env._t( "This operation will destroy all unpaid orders in the browser. You will lose all the unsaved data and exit the point of sale. This operation cannot be undone." @@ -147,7 +149,7 @@ export class DebugWidget extends PosComponent { const file = event.target.files[0]; if (file) { const report = this.env.pos.import_orders(await file.text()); - await this.showPopup("OrderImportPopup", { report }); + await this.showPopup(OrderImportPopup, { report }); } } refreshDisplay() { diff --git a/addons/point_of_sale/static/src/app/error_handlers/error_handlers.js b/addons/point_of_sale/static/src/app/error_handlers/error_handlers.js index 7b7ffe081d0..8e9ab9ef39d 100644 --- a/addons/point_of_sale/static/src/app/error_handlers/error_handlers.js +++ b/addons/point_of_sale/static/src/app/error_handlers/error_handlers.js @@ -3,7 +3,10 @@ import { registry } from "@web/core/registry"; import { odooExceptionTitleMap } from "@web/core/errors/error_dialogs"; import { ConnectionLostError, RPCError } from "@web/core/network/rpc_service"; -import { Gui } from "@point_of_sale/js/Gui"; +import { Gui, GuiNotReadyError } from "@point_of_sale/js/Gui"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ErrorTracebackPopup } from "@point_of_sale/js/Popups/ErrorTracebackPopup"; +import { OfflineErrorPopup } from "@point_of_sale/js/Popups/OfflineErrorPopup"; export function identifyError(error) { return error && error.legacy ? error.message : error; @@ -15,9 +18,9 @@ function rpcErrorHandler(env, error, originalError) { const { message, data } = error; if (odooExceptionTitleMap.has(error.exceptionName)) { const title = odooExceptionTitleMap.get(error.exceptionName).toString(); - Gui.showPopup("ErrorPopup", { title, body: data.message }); + Gui.showPopup(ErrorPopup, { title, body: data.message }); } else { - Gui.showPopup("ErrorTracebackPopup", { + Gui.showPopup(ErrorTracebackPopup, { title: message, body: data.message + "\n" + data.debug + "\n", }); @@ -30,7 +33,7 @@ registry.category("error_handlers").add("rpcErrorHandler", rpcErrorHandler); function offlineErrorHandler(env, error, originalError) { error = identifyError(originalError); if (error instanceof ConnectionLostError) { - Gui.showPopup("OfflineErrorPopup", { + Gui.showPopup(OfflineErrorPopup, { title: env._t("Couldn't connect to the server"), body: env._t( "The operation couldn't be completed because you are offline. Check your internet connection and try again." @@ -44,12 +47,12 @@ registry.category("error_handlers").add("offlineErrorHandler", offlineErrorHandl function defaultErrorHandler(env, error, originalError) { error = identifyError(originalError); if (error instanceof Error) { - Gui.showPopup("ErrorTracebackPopup", { + Gui.showPopup(ErrorTracebackPopup, { title: `${error.name}: ${error.message}`, body: error.traceback, }); } else { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: env._t("Unknown Error"), body: env._t("Unable to show information about this error."), }); @@ -59,3 +62,12 @@ function defaultErrorHandler(env, error, originalError) { registry .category("error_handlers") .add("defaultErrorHandler", defaultErrorHandler, { sequence: 99 }); + +function guiNotReadyHandler(env, error, originalError) { + error = identifyError(originalError); + if (error instanceof GuiNotReadyError) { + console.error(originalError); + return true; + } +} +registry.category("error_handlers").add("guiNotReadyHandler", guiNotReadyHandler); diff --git a/addons/point_of_sale/static/src/app/navbar/navbar.js b/addons/point_of_sale/static/src/app/navbar/navbar.js index 3e6fe88a877..a90c56327f6 100644 --- a/addons/point_of_sale/static/src/app/navbar/navbar.js +++ b/addons/point_of_sale/static/src/app/navbar/navbar.js @@ -1,10 +1,30 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { usePos } from "@point_of_sale/app/pos_store"; +import { CashierName } from "@point_of_sale/js/ChromeWidgets/CashierName"; +import { CashMoveButton } from "@point_of_sale/js/ChromeWidgets/CashMoveButton"; +import { CustomerFacingDisplayButton } from "@point_of_sale/js/ChromeWidgets/CustomerFacingDisplayButton"; +import { HeaderButton } from "@point_of_sale/js/ChromeWidgets/HeaderButton"; +import { ProxyStatus } from "@point_of_sale/js/ChromeWidgets/ProxyStatus"; +import { SaleDetailsButton } from "@point_of_sale/js/ChromeWidgets/SaleDetailsButton"; +import { SyncNotification } from "@point_of_sale/js/ChromeWidgets/SyncNotification"; +import { TicketButton } from "@point_of_sale/js/ChromeWidgets/TicketButton"; + export class Navbar extends PosComponent { static template = "point_of_sale.Navbar"; + static components = { + // FIXME POSREF remove some of these components + CashierName, + CashMoveButton, + CustomerFacingDisplayButton, + HeaderButton, + ProxyStatus, + SaleDetailsButton, + SyncNotification, + TicketButton, + }; static props = { showCashMoveButton: Boolean, }; diff --git a/addons/point_of_sale/static/src/entry/main.js b/addons/point_of_sale/static/src/entry/main.js index 72f106e122e..512214219bc 100644 --- a/addons/point_of_sale/static/src/entry/main.js +++ b/addons/point_of_sale/static/src/entry/main.js @@ -1,16 +1,10 @@ /** @odoo-module */ import { startWebClient } from "@web/start"; - import { Chrome } from "@point_of_sale/js/Chrome"; -import Registries from "@point_of_sale/js/Registries"; -function startPosApp() { - Registries.Component.freeze(); - Registries.Model.freeze(); - // For consistency's sake, we should trigger"WEB_CLIENT_READY" on the bus when PosApp is mounted - // But we can't since mail and some other poll react on that cue, and we don't want those services started - startWebClient(Registries.Component.get(Chrome)); -} - -startPosApp(); +// FIXME POSREF no we shouldn't but if we did nothing should react, if it does it shouldn't be loaded +// For consistency's sake, we should trigger"WEB_CLIENT_READY" on the bus when PosApp is mounted +// But we can't since mail and some other poll react on that cue, and we don't want those services started +// FIXME POSREF stop using startWebclient: this is not a web client. +startWebClient(Chrome); diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js index 31cfe4abcff..167839189ed 100644 --- a/addons/point_of_sale/static/src/js/Chrome.js +++ b/addons/point_of_sale/static/src/js/Chrome.js @@ -3,10 +3,9 @@ import { loadCSS } from "@web/core/assets"; import { useListener, useBus, useService } from "@web/core/utils/hooks"; import BarcodeParser from "barcodes.BarcodeParser"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; -import Registries from "@point_of_sale/js/Registries"; -import IndependentToOrderScreen from "@point_of_sale/js/Misc/IndependentToOrderScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { IndependentToOrderScreen } from "@point_of_sale/js/Misc/IndependentToOrderScreen"; import { batched } from "@point_of_sale/js/utils"; import { debounce } from "@web/core/utils/timing"; import { Transition } from "@web/core/transition"; @@ -15,11 +14,17 @@ import { WithEnv } from "@web/core/utils/components"; import { Navbar } from "@point_of_sale/app/navbar/navbar"; // ChromeAdapter imports -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { PosGlobalState } from "@point_of_sale/js/models"; import { configureGui } from "@point_of_sale/js/Gui"; import { registry } from "@web/core/registry"; -import env from "@point_of_sale/js/pos_env"; +import { pos_env as env } from "@point_of_sale/js/pos_env"; + +import { Notification } from "./Notification"; +import { PosPopupController } from "./Popups/PosPopupController"; +import { ErrorTracebackPopup } from "./Popups/ErrorTracebackPopup"; +import { CashOpeningPopup } from "./Popups/CashOpeningPopup"; +import { ConfirmPopup } from "./Popups/ConfirmPopup"; import { onMounted, @@ -36,6 +41,7 @@ import { usePos } from "@point_of_sale/app/pos_store"; * Chrome is the root component of the PoS App. */ export class Chrome extends PosComponent { + static template = "Chrome"; // FIXME POSREF namespace templates setup() { // BEGIN ChromeAdapter ProductScreen.sortControlButtons(); @@ -43,7 +49,7 @@ export class Chrome extends PosComponent { // Instantiate PosGlobalState here to ensure that every extension // (or class overloads) is taken into consideration. - const pos = PosGlobalState.create({ env: markRaw(env) }); + const pos = new PosGlobalState({ env: markRaw(env) }); this.batchedCustomerDisplayRender = batched(() => { reactivePos.send_current_order_to_customer_facing_display(); @@ -95,7 +101,7 @@ export class Chrome extends PosComponent { useListener("close-notification", this._onCloseNotification); useListener("connect-to-proxy", this.connect_to_proxy); useBus(this.env.posbus, "start-cash-control", this.openCashControl); - NumberBuffer.activate(); + numberBuffer.activate(); this.state = usePos(); @@ -207,7 +213,7 @@ export class Chrome extends PosComponent { } } - return this.showPopup("ErrorTracebackPopup", { title, body, exitButtonIsShown: true }); + return this.showPopup(ErrorTracebackPopup, { title, body, exitButtonIsShown: true }); } registry.category("main_components").add("BlockUI", BlockUiFromRegistry); @@ -239,10 +245,13 @@ export class Chrome extends PosComponent { setupBarcodeParser() { if (!this.env.pos.company.nomenclature_id) { - const errorMessage = this.env._t("The barcode nomenclature setting is not configured. " + - "Make sure to configure it on your Point of Sale configuration settings"); - throw new Error(this.env._t("Missing barcode nomenclature"), { cause: { message: errorMessage } }); - + const errorMessage = this.env._t( + "The barcode nomenclature setting is not configured. " + + "Make sure to configure it on your Point of Sale configuration settings" + ); + throw new Error(this.env._t("Missing barcode nomenclature"), { + cause: { message: errorMessage }, + }); } const barcode_parser = new BarcodeParser({ nomenclature_id: this.env.pos.company.nomenclature_id, @@ -292,7 +301,7 @@ export class Chrome extends PosComponent { openCashControl() { if (this.shouldShowCashControl()) { - this.showPopup("CashOpeningPopup", { keepBehind: true }); + this.showPopup(CashOpeningPopup, { keepBehind: true }); } } @@ -315,7 +324,7 @@ export class Chrome extends PosComponent { const { name, props, resolve } = event.detail; this.state.tempScreen = { name, - component: this.constructor.components[name], + component: registry.category("pos_screens").get(name), props: { ...props, resolve }, }; } @@ -323,7 +332,7 @@ export class Chrome extends PosComponent { this.state.tempScreen = null; } __showScreen({ detail: { name, props = {} } }) { - const component = this.constructor.components[name]; + const component = registry.category("pos_screens").get(name); // 1. Set the information of the screen to display. this.mainScreen.name = name; this.mainScreen.component = component; @@ -383,7 +392,7 @@ export class Chrome extends PosComponent { "not close the session before the issue " + "has been resolved." ); - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Offline Orders"), body: reason, }); @@ -486,7 +495,6 @@ export class Chrome extends PosComponent { return this.env.pos && this.env.pos.config && this.env.pos.config.cash_control; } } -Chrome.template = "Chrome"; Object.defineProperty(Chrome, "components", { get() { return Object.assign( @@ -495,12 +503,10 @@ Object.defineProperty(Chrome, "components", { MainComponentsContainer, WithEnv, Navbar, + PosPopupController, + Notification, }, PosComponent.components ); }, }); - -Registries.Component.add(Chrome); - -export default Chrome; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js index 340a443655b..09d643886ac 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js @@ -1,18 +1,21 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; -import { _t } from "web.core"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { _lt } from "web.core"; import { renderToString } from "@web/core/utils/render"; +import { CashMovePopup } from "../Popups/CashMovePopup"; +import { ErrorPopup } from "../Popups/ErrorPopup"; const TRANSLATED_CASH_MOVE_TYPE = { - in: _t("in"), - out: _t("out"), + in: _lt("in"), + out: _lt("out"), }; -class CashMoveButton extends PosComponent { +export class CashMoveButton extends PosComponent { + static template = "point_of_sale.CashMoveButton"; + async onClick() { - const { confirmed, payload } = await this.showPopup("CashMovePopup"); + const { confirmed, payload } = await this.showPopup(CashMovePopup); if (!confirmed) { return; } @@ -37,7 +40,7 @@ class CashMoveButton extends PosComponent { }); const printResult = await this.env.proxy.printer.print_receipt(renderedReceipt); if (!printResult.successful) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: printResult.message.title, body: printResult.message.body, }); @@ -55,8 +58,3 @@ class CashMoveButton extends PosComponent { return result; } } -CashMoveButton.template = "point_of_sale.CashMoveButton"; - -Registries.Component.add(CashMoveButton); - -export default CashMoveButton; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js b/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js index db19c43c047..f67d7545a78 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js @@ -1,10 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; // Previously UsernameWidget -class CashierName extends PosComponent { +export class CashierName extends PosComponent { + static template = "CashierName"; + get username() { const { name } = this.env.pos.get_cashier(); return name ? name : ""; @@ -18,8 +19,3 @@ class CashierName extends PosComponent { return { "not-clickable": true }; } } -CashierName.template = "CashierName"; - -Registries.Component.add(CashierName); - -export default CashierName; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js index 4fce19efca4..76c14d5b633 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js @@ -1,11 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { useState } = owl; -class CustomerFacingDisplayButton extends PosComponent { +export class CustomerFacingDisplayButton extends PosComponent { + static template = "CustomerFacingDisplayButton"; + setup() { super.setup(); this.local = @@ -101,8 +102,3 @@ class CustomerFacingDisplayButton extends PosComponent { loop(); } } -CustomerFacingDisplayButton.template = "CustomerFacingDisplayButton"; - -Registries.Component.add(CustomerFacingDisplayButton); - -export default CustomerFacingDisplayButton; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js index ff38bf77b1d..24a32a7352a 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js @@ -1,18 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ClosePosPopup } from "../Popups/ClosePosPopup"; // Previously HeaderButtonWidget // This is the close session button -class HeaderButton extends PosComponent { +export class HeaderButton extends PosComponent { + static template = "HeaderButton"; + async onClick() { const info = await this.env.pos.getClosePosInfo(); - this.showPopup("ClosePosPopup", { info: info, keepBehind: true }); + this.showPopup(ClosePosPopup, { info: info, keepBehind: true }); } } -HeaderButton.template = "HeaderButton"; - -Registries.Component.add(HeaderButton); - -export default HeaderButton; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js b/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js index 618749a59a8..1fd73a7b4b8 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js @@ -1,12 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { onMounted, onWillUnmount, useState } = owl; // Previously ProxyStatusWidget -class ProxyStatus extends PosComponent { +export class ProxyStatus extends PosComponent { + static template = "ProxyStatus"; + setup() { super.setup(); const initialProxyStatus = this.env.proxy.get("status"); @@ -69,8 +70,3 @@ class ProxyStatus extends PosComponent { } } } -ProxyStatus.template = "ProxyStatus"; - -Registries.Component.add(ProxyStatus); - -export default ProxyStatus; 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 7f847461c28..cc02270d018 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js @@ -1,10 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { renderToString } from "@web/core/utils/render"; +import { ErrorPopup } from "../Popups/ErrorPopup"; + +export class SaleDetailsButton extends PosComponent { + static template = "SaleDetailsButton"; -class SaleDetailsButton extends PosComponent { async onClick() { // IMPROVEMENT: Perhaps put this logic in a parent component // so that for unit testing, we can check if this simple @@ -23,15 +25,10 @@ class SaleDetailsButton extends PosComponent { ); const printResult = await this.env.proxy.printer.print_receipt(report); if (!printResult.successful) { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: printResult.message.title, body: printResult.message.body, }); } } } -SaleDetailsButton.template = "SaleDetailsButton"; - -Registries.Component.add(SaleDetailsButton); - -export default SaleDetailsButton; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js b/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js index b22c1dc0557..393e5287295 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js @@ -1,15 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class SyncNotification extends PosComponent { + static template = "SyncNotification"; -class SyncNotification extends PosComponent { onClick() { this.env.pos.push_orders(null, { show_error: true }); } } -SyncNotification.template = "SyncNotification"; - -Registries.Component.add(SyncNotification); - -export default SyncNotification; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js index 611efa518fa..a11b33771f5 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js @@ -1,10 +1,11 @@ /** @odoo-module */ import { usePos } from "@point_of_sale/app/pos_store"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class TicketButton extends PosComponent { + static template = "TicketButton"; -class TicketButton extends PosComponent { setup() { this.pos = usePos(); } @@ -26,8 +27,3 @@ class TicketButton extends PosComponent { } } } -TicketButton.template = "TicketButton"; - -Registries.Component.add(TicketButton); - -export default TicketButton; diff --git a/addons/point_of_sale/static/src/js/ClassRegistry.js b/addons/point_of_sale/static/src/js/ClassRegistry.js deleted file mode 100644 index 24c66e25f54..00000000000 --- a/addons/point_of_sale/static/src/js/ClassRegistry.js +++ /dev/null @@ -1,260 +0,0 @@ -/** @odoo-module */ - -/** - * **Usage:** - * ``` - * const Registry = new ClassRegistry(); - * - * class A {} - * Registry.add(A); - * - * const AExt1 = A => class extends A {} - * Registry.extend(A, AExt1) - * - * const B = A => class extends A {} - * Registry.addByExtending(B, A) - * - * const AExt2 = A => class extends A {} - * Registry.extend(A, AExt2) - * - * Registry.get(A) - * // above returns: AExt2 -> AExt1 -> A - * // Basically, 'A' in the registry points to - * // the inheritance chain above. - * - * Registry.get(B) - * // above returns: B -> AExt2 -> AExt1 -> A - * // Even though B extends A before applying all - * // the extensions of A, when getting it from the - * // registry, the return points to a class with - * // inheritance chain that includes all the extensions - * // of 'A'. - * - * Registry.freeze() - * // Example 'B' above is lazy. Basically, it is only - * // computed when we call `get` from the registry. - * // If we know that no more dynamic inheritances will happen, - * // we can freeze the registry and cache the final form - * // of each class in the registry. - * ``` - * - * IMPROVEMENT: - * * So far, mixin can be accomplished by creating a method - * the takes a class and returns a class expression. This is then - * used before the extends keyword like so: - * - * ```js - * class A {} - * Registry.add(A) - * const Mixin = x => class extends x {} - * // apply mixin - * // | - * // v - * const B = x => class extends Mixin(x) {} - * Registry.addByExtending(B, A) - * ``` - * - * In the example, `|B| => B -> Mixin -> A`, and this is pretty convenient - * already. However, this can still be improved since classes are only - * compiled after `Registry.freeze()`. Perhaps, we can make the - * Mixins extensible as well, such as so: - * - * ``` - * class A {} - * Registry.add(A) - * const Mixin = x => class extends x {} - * Registry.add(Mixin) - * const OtherMixin = x => class extends x {} - * Registry.add(OtherMixin) - * const B = x => class extends x {} - * Registry.addByExtending(B, A, [Mixin, OtherMixin]) - * const ExtendMixin = x => class extends x {} - * Registry.extend(Mixin, ExtendMixin) - * ``` - * - * In the above, after `Registry.freeze()`, - * `|B| => B -> OtherMixin -> ExtendMixin -> Mixin -> A` - */ -class ClassRegistry { - constructor() { - // base name map - this.baseNameMap = {}; - // Object that maps `baseClass` to the class implementation extended in-place. - this.includedMap = new Map(); - // Object that maps `baseClassCB` to the array of callbacks to generate the extended class. - this.extendedCBMap = new Map(); - // Object that maps `baseClassCB` extended class to the `baseClass` of its super in the includedMap. - this.extendedSuperMap = new Map(); - // For faster access, we can `freeze` the registry so that instead of dynamically generating - // the extended classes, it is taken from the cache instead. - this.cache = new Map(); - } - /** - * Add a new class in the Registry. - * @param {Function} baseClass `class` - */ - add(baseClass) { - this.includedMap.set(baseClass, []); - this.baseNameMap[baseClass.name] = baseClass; - } - /** - * Add a new class in the Registry based on other class - * in the registry. - * @param {Function} baseClassCB `class -> class` - * @param {Function} base `class | class -> class` - */ - addByExtending(baseClassCB, base) { - this.extendedCBMap.set(baseClassCB, [baseClassCB]); - this.extendedSuperMap.set(baseClassCB, base); - this.baseNameMap[baseClassCB.name] = baseClassCB; - } - /** - * Extend in-place a class in the registry. E.g. - * ``` - * // Using the following notation: - * // * |A| - compiled class in the registry - * // * A - class or an extension callback - * // * |A| => A2 -> A1 -> A - * // - the above means, compiled class A - * // points to the class inheritance derived from - * // A2(A1(A)) - * - * class A {}; - * Registry.add(A); - * // |A| => A - * - * let A1 = x => class extends x {}; - * Registry.extend(A, A1); - * // |A| => A1 -> A - * - * let B = x => class extends x {}; - * Registry.addByExtending(B, A); - * // |B| => B -> |A| - * // |B| => B -> A1 -> A - * - * let B1 = x => class extends x {}; - * Registry.extend(B, B1); - * // |B| => B1 -> B -> |A| - * - * let C = x => class extends x {}; - * Registry.addByExtending(C, B); - * // |C| => C -> |B| - * - * let B2 = x => class extends x {}; - * Registry.extend(B, B2); - * // |B| => B2 -> B1 -> B -> |A| - * - * // Overall: - * // |A| => A1 -> A - * // |B| => B2 -> B1 -> B -> A1 -> A - * // |C| => C -> B2 -> B1 -> B -> A1 -> A - * ``` - * @param {Function} base `class | class -> class` - * @param {Function} extensionCB `class -> class` - */ - extend(base, extensionCB) { - if (typeof base === "string") { - base = this.baseNameMap[base]; - } - let extensionArray; - if (this.includedMap.get(base)) { - extensionArray = this.includedMap.get(base); - } else if (this.extendedCBMap.get(base)) { - extensionArray = this.extendedCBMap.get(base); - } else { - throw new Error( - `'${base.name}' is not in the Registry. Add it to Registry before extending.` - ); - } - extensionArray.push(extensionCB); - const locOfNewExtension = extensionArray.length - 1; - const self = this; - const oldCompiled = this.isFrozen ? this.cache.get(base) : null; - return { - remove() { - extensionArray.splice(locOfNewExtension, 1); - self._recompute(base, oldCompiled); - }, - compile() { - self._recompute(base); - }, - }; - } - _compile(base) { - let res; - if (this.includedMap.has(base)) { - res = this.includedMap.get(base).reduce((acc, ext) => ext(acc), base); - } else { - const superClass = this.extendedSuperMap.get(base); - const extensionCBs = this.extendedCBMap.get(base); - res = extensionCBs.reduce((acc, ext) => ext(acc), this._compile(superClass)); - } - Object.defineProperty(res, "name", { value: base.name }); - return res; - } - /** - * Return the compiled class (containing all the extensions) of the base class. - * @param {Function} base `class | class -> class` function used in adding the class - */ - get(base) { - if (typeof base === "string") { - base = this.baseNameMap[base]; - } - if (this.isFrozen) { - return this.cache.get(base); - } - return this._compile(base); - } - /** - * Uses the callbacks registered in the registry to compile the classes. - */ - freeze() { - // Step: Compile the `included classes`. - for (const [baseClass, extensionCBs] of this.includedMap.entries()) { - const compiled = extensionCBs.reduce((acc, ext) => ext(acc), baseClass); - this.cache.set(baseClass, compiled); - } - - // Step: Compile the `extended classes` based on `included classes`. - // Also gather those the are based on `extended classes`. - const remaining = []; - for (const [baseClassCB, extensionCBArray] of this.extendedCBMap.entries()) { - const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB)); - if (!compiled) { - remaining.push([baseClassCB, extensionCBArray]); - continue; - } - const extendedClass = extensionCBArray.reduce( - (acc, extensionCB) => extensionCB(acc), - compiled - ); - this.cache.set(baseClassCB, extendedClass); - } - - // Step: Compile the `extended classes` based on `extended classes`. - for (const [baseClassCB, extensionCBArray] of remaining) { - const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB)); - const extendedClass = extensionCBArray.reduce( - (acc, extensionCB) => extensionCB(acc), - compiled - ); - this.cache.set(baseClassCB, extendedClass); - } - - // Step: Set the name of the compiled classess - for (const [base, compiledClass] of this.cache.entries()) { - Object.defineProperty(compiledClass, "name", { value: base.name }); - } - - // Step: Set the flag to true; - this.isFrozen = true; - } - _recompute(base, old) { - if (typeof base === "string") { - base = this.baseNameMap[base]; - } - return old ? old : this._compile(base); - } -} - -export default ClassRegistry; diff --git a/addons/point_of_sale/static/src/js/ComponentRegistry.js b/addons/point_of_sale/static/src/js/ComponentRegistry.js deleted file mode 100644 index 1547aa930e0..00000000000 --- a/addons/point_of_sale/static/src/js/ComponentRegistry.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @odoo-module */ - -import PosComponent from "@point_of_sale/js/PosComponent"; -import ClassRegistry from "@point_of_sale/js/ClassRegistry"; - -class ComponentRegistry extends ClassRegistry { - freeze() { - super.freeze(); - // Make sure PosComponent has the compiled classes. - // This way, we don't need to explicitly declare that - // a set of components is children of another. - PosComponent.components = {}; - for (const [base, compiledClass] of this.cache.entries()) { - PosComponent.components[base.name] = compiledClass; - } - } - _recompute(base, old) { - const res = super._recompute(base, old); - if (typeof base === "string") { - base = this.baseNameMap[base]; - } - PosComponent.components[base.name] = res; - return res; - } -} - -export default ComponentRegistry; diff --git a/addons/point_of_sale/static/src/js/ControlButtonsMixin.js b/addons/point_of_sale/static/src/js/ControlButtonsMixin.js index 1ed763ce552..8d76cac4c01 100644 --- a/addons/point_of_sale/static/src/js/ControlButtonsMixin.js +++ b/addons/point_of_sale/static/src/js/ControlButtonsMixin.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import Registries from "@point_of_sale/js/Registries"; +/** @typedef {import("@odoo/owl").Component} Component */ /** * Component that has this mixin allows the use of `addControlButton`. @@ -9,7 +9,7 @@ import Registries from "@point_of_sale/js/Registries"; * control buttons can then be rendered in the Component. * @param {Function} x superclass */ -const ControlButtonsMixin = (x) => { +export const ControlButtonsMixin = (x) => { const controlButtonsToPosition = []; const sortedControlButtons = []; @@ -19,15 +19,13 @@ const ControlButtonsMixin = (x) => { .filter((cb) => { return cb.condition ? cb.condition.bind(this)() : true; }) - .map((cb) => - Object.assign({}, cb, { component: Registries.Component.get(cb.component) }) - ); + .map((cb) => Object.assign({}, cb, { component: cb.component })); } } /** * @param {Object} controlButton - * @param {Function} controlButton.component - * Base class that is added in the Registries.Component. + * @param {new (...args: any[]) => Component} controlButton.component the + * component class * @param {Function} controlButton.condition zero argument function that is bound * to the instance of ProductScreen, such that `this.env.pos` can be used * inside the function. @@ -103,5 +101,3 @@ const ControlButtonsMixin = (x) => { }; return Extended; }; - -export default ControlButtonsMixin; diff --git a/addons/point_of_sale/static/src/js/Gui.js b/addons/point_of_sale/static/src/js/Gui.js index f6d3c6806a4..ad7040df0e2 100644 --- a/addons/point_of_sale/static/src/js/Gui.js +++ b/addons/point_of_sale/static/src/js/Gui.js @@ -43,6 +43,7 @@ export const configureGui = ({ component }) => { ]); }; +export class GuiNotReadyError extends Error {} /** * Import this and consume like so: `Gui.showPopup(, )`. * Like you would call `showPopup` in a component. @@ -57,5 +58,6 @@ export const Gui = new Proxy(config, { if (availableMethods.has(key) && isMounted) { return component[key].bind(component); } + throw new GuiNotReadyError(`Attempted get ${key} on Gui when Chrome is not yet mounted.`); }, }); 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 003310210b1..3e6cf95e2e5 100644 --- a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js @@ -1,7 +1,8 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ConfirmPopup } from "../Popups/ConfirmPopup"; +import { ErrorPopup } from "../Popups/ErrorPopup"; const { useRef } = owl; @@ -11,7 +12,7 @@ const { useRef } = owl; * `order-receipt` in the template of the Component that extends * this abstract component. */ -class AbstractReceiptScreen extends PosComponent { +export class AbstractReceiptScreen extends PosComponent { setup() { super.setup(); this.orderReceipt = useRef("order-receipt"); @@ -24,7 +25,7 @@ class AbstractReceiptScreen extends PosComponent { if (printResult.successful) { return true; } else { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: printResult.message.title, body: "Do you want to print using the web printer?", }); @@ -45,7 +46,7 @@ class AbstractReceiptScreen extends PosComponent { window.print(); return true; } catch { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Printing is not supported on some browsers"), body: this.env._t( "Printing is not supported on some browsers due to no default printing protocol " + @@ -56,7 +57,3 @@ class AbstractReceiptScreen extends PosComponent { } } } - -Registries.Component.add(AbstractReceiptScreen); - -export default AbstractReceiptScreen; diff --git a/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js b/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js index 82481f61240..0ce8915526d 100644 --- a/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js +++ b/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js @@ -1,11 +1,7 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; -class CurrencyAmount extends PosComponent {} -CurrencyAmount.template = "CurrencyAmount"; - -Registries.Component.add(CurrencyAmount); - -export default CurrencyAmount; +export class CurrencyAmount extends PosComponent { + static template = "CurrencyAmount"; +} diff --git a/addons/point_of_sale/static/src/js/Misc/Draggable.js b/addons/point_of_sale/static/src/js/Misc/Draggable.js index 4739df8e668..5fd5e916659 100644 --- a/addons/point_of_sale/static/src/js/Misc/Draggable.js +++ b/addons/point_of_sale/static/src/js/Misc/Draggable.js @@ -1,8 +1,7 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { onMounted, useExternalListener } = owl; @@ -28,7 +27,9 @@ const { onMounted, useExternalListener } = owl; * * @trigger 'drag-end' when dragging ended with payload `{ loc: { top, left } }` */ -class Draggable extends PosComponent { +export class Draggable extends PosComponent { + static template = "Draggable"; + setup() { super.setup(); this.isDragging = false; @@ -137,8 +138,3 @@ class Draggable extends PosComponent { return posTop; } } -Draggable.template = "Draggable"; - -Registries.Component.add(Draggable); - -export default Draggable; diff --git a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js index c46d93f7294..c579be99371 100644 --- a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js +++ b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js @@ -1,13 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; -class IndependentToOrderScreen extends PosComponent { +export class IndependentToOrderScreen extends PosComponent { close() { const order = this.env.pos.get_order(); const { name: screenName } = order.get_screen_data(); this.showScreen(screenName); } } - -export default IndependentToOrderScreen; diff --git a/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js b/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js index ddbb2817fb0..813aff7e6b5 100644 --- a/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js +++ b/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class MobileOrderWidget extends PosComponent { + static template = "MobileOrderWidget"; -class MobileOrderWidget extends PosComponent { get order() { return this.env.pos.get_order(); } @@ -17,9 +18,3 @@ class MobileOrderWidget extends PosComponent { : 0; } } - -MobileOrderWidget.template = "MobileOrderWidget"; - -Registries.Component.add(MobileOrderWidget); - -export default MobileOrderWidget; diff --git a/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js b/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js index ff3e5736ae6..f91eb851e5d 100644 --- a/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js +++ b/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js @@ -317,4 +317,4 @@ class NumberBuffer extends EventBus { } } -export default new NumberBuffer(); +export const numberBuffer = new NumberBuffer(); diff --git a/addons/point_of_sale/static/src/js/Misc/SearchBar.js b/addons/point_of_sale/static/src/js/Misc/SearchBar.js index 4f74637dcd0..94db0f7bea3 100644 --- a/addons/point_of_sale/static/src/js/Misc/SearchBar.js +++ b/addons/point_of_sale/static/src/js/Misc/SearchBar.js @@ -1,8 +1,7 @@ /** @odoo-module */ import { useAutofocus, useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { useExternalListener, useState } = owl; @@ -29,7 +28,9 @@ const { useExternalListener, useState } = owl; * NOTE: The payload of the emitted event is accessible via the `detail` * field of the event. */ -class SearchBar extends PosComponent { +export class SearchBar extends PosComponent { + static template = "point_of_sale.SearchBar"; + setup() { super.setup(); useAutofocus(); @@ -108,7 +109,3 @@ class SearchBar extends PosComponent { this.state.showSearchFields = false; } } -SearchBar.template = "point_of_sale.SearchBar"; -Registries.Component.add(SearchBar); - -export default SearchBar; diff --git a/addons/point_of_sale/static/src/js/Notification.js b/addons/point_of_sale/static/src/js/Notification.js index f0d92fca7ee..9f029b2f40c 100644 --- a/addons/point_of_sale/static/src/js/Notification.js +++ b/addons/point_of_sale/static/src/js/Notification.js @@ -1,12 +1,13 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { onMounted } = owl; -class Notification extends PosComponent { +export class Notification extends PosComponent { + static template = "Notification"; + setup() { super.setup(); useListener("click", this.closeNotification); @@ -18,8 +19,3 @@ class Notification extends PosComponent { }); } } -Notification.template = "Notification"; - -Registries.Component.add(Notification); - -export default Notification; diff --git a/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js b/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js index 3041691a7d8..ed252d67a79 100644 --- a/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { useBus } from "@web/core/utils/hooks"; /** @@ -22,13 +22,13 @@ import { useBus } from "@web/core/utils/hooks"; * The concrete popup can now be instantiated and be awaited for * the user's response like so: * ```js - * const { confirmed, payload } = await this.showPopup('ConcretePopup'); + * const { confirmed, payload } = await this.showPopup(ConcretePopup); * // based on the implementation above, * // if confirmed, payload = 'result' * // otherwise, payload = null * ``` */ -class AbstractAwaitablePopup extends PosComponent { +export class AbstractAwaitablePopup extends PosComponent { setup() { super.setup(); if (this.props.confirmKey) { @@ -58,5 +58,3 @@ class AbstractAwaitablePopup extends PosComponent { return null; } } - -export default AbstractAwaitablePopup; diff --git a/addons/point_of_sale/static/src/js/Popups/AlertPopup.js b/addons/point_of_sale/static/src/js/Popups/AlertPopup.js index a6acd64d4f7..df5ac2126e7 100644 --- a/addons/point_of_sale/static/src/js/Popups/AlertPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/AlertPopup.js @@ -1,17 +1,14 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; -export class AlertPopup extends AbstractAwaitablePopup {} - -AlertPopup.template = "AlertPopup"; -AlertPopup.defaultProps = { - confirmText: _lt("Ok"), - title: "", - body: "", - cancelKey: false, -}; - -Registries.Component.add(AlertPopup); +export class AlertPopup extends AbstractAwaitablePopup { + static template = "AlertPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + title: "", + body: "", + cancelKey: false, + }; +} diff --git a/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js b/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js index a1cafb05b6a..91b4e0e6afa 100644 --- a/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js @@ -1,12 +1,13 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { parse } from "web.field_utils"; const { useRef, useState, onMounted } = owl; -class CashMovePopup extends AbstractAwaitablePopup { +export class CashMovePopup extends AbstractAwaitablePopup { + static template = "point_of_sale.CashMovePopup"; + setup() { super.setup(); this.state = useState({ @@ -49,7 +50,3 @@ class CashMovePopup extends AbstractAwaitablePopup { }; } } -CashMovePopup.template = "point_of_sale.CashMovePopup"; -Registries.Component.add(CashMovePopup); - -export default CashMovePopup; diff --git a/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js b/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js index 59c77266ba9..cfdf767b798 100644 --- a/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js @@ -1,11 +1,14 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import { MoneyDetailsPopup } from "./MoneyDetailsPopup"; const { useState } = owl; -class CashOpeningPopup extends AbstractAwaitablePopup { +export class CashOpeningPopup extends AbstractAwaitablePopup { + static template = "CashOpeningPopup"; + static defaultProps = { cancelKey: false }; + setup() { super.setup(); this.manualInputCashCount = null; @@ -27,7 +30,7 @@ class CashOpeningPopup extends AbstractAwaitablePopup { super.confirm(); } async openDetailsPopup() { - const { confirmed, payload } = await this.showPopup("MoneyDetailsPopup", { + const { confirmed, payload } = await this.showPopup(MoneyDetailsPopup, { moneyDetails: this.moneyDetails, total: this.manualInputCashCount ? 0 : this.state.openingCash, }); @@ -50,9 +53,3 @@ class CashOpeningPopup extends AbstractAwaitablePopup { } } } - -CashOpeningPopup.template = "CashOpeningPopup"; -CashOpeningPopup.defaultProps = { cancelKey: false }; -Registries.Component.add(CashOpeningPopup); - -export default CashOpeningPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js index dc58a0ff984..68fdc6a2988 100644 --- a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js @@ -1,12 +1,19 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { identifyError } from "@point_of_sale/app/error_handlers/error_handlers"; import { ConnectionLostError } from "@web/core/network/rpc_service"; +import { SaleDetailsButton } from "../ChromeWidgets/SaleDetailsButton"; +import { ConfirmPopup } from "./ConfirmPopup"; +import { ErrorPopup } from "./ErrorPopup"; +import { MoneyDetailsPopup } from "./MoneyDetailsPopup"; + const { useState } = owl; -class ClosePosPopup extends AbstractAwaitablePopup { +export class ClosePosPopup extends AbstractAwaitablePopup { + static components = { SaleDetailsButton }; + static template = "ClosePosPopup"; + setup() { super.setup(); this.manualInputCashCount = false; @@ -24,7 +31,7 @@ class ClosePosPopup extends AbstractAwaitablePopup { if (!this.cashControl || !this.hasDifference()) { this.closeSession(); } else if (this.hasUserAuthority()) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Payments Difference"), body: this.env._t( "Do you want to accept payments difference and post a profit/loss journal entry?" @@ -34,7 +41,7 @@ class ClosePosPopup extends AbstractAwaitablePopup { this.closeSession(); } } else { - await this.showPopup("ConfirmPopup", { + await this.showPopup(ConfirmPopup, { title: this.env._t("Payments Difference"), body: _.str.sprintf( this.env._t( @@ -54,7 +61,7 @@ class ClosePosPopup extends AbstractAwaitablePopup { } } async openDetailsPopup() { - const { confirmed, payload } = await this.showPopup("MoneyDetailsPopup", { + const { confirmed, payload } = await this.showPopup(MoneyDetailsPopup, { moneyDetails: this.moneyDetails, total: this.manualInputCashCount ? 0 @@ -175,7 +182,7 @@ class ClosePosPopup extends AbstractAwaitablePopup { throw error; } else { // FIXME POSREF: why are we catching errors here but not anywhere else in this method? - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Closing session error"), body: this.env._t( "An error has occurred when trying to close the session.\n" + @@ -210,8 +217,3 @@ class ClosePosPopup extends AbstractAwaitablePopup { return pm.type == "bank" && pm.number !== 0; } } - -ClosePosPopup.template = "ClosePosPopup"; -Registries.Component.add(ClosePosPopup); - -export default ClosePosPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js b/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js index 106ba159911..6d138708c65 100644 --- a/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js @@ -1,19 +1,18 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + // formerly ConfirmPopupWidget -class ConfirmPopup extends AbstractAwaitablePopup {} -ConfirmPopup.template = "ConfirmPopup"; -ConfirmPopup.defaultProps = { - confirmText: _lt("Ok"), - cancelText: _lt("Cancel"), - title: _lt("Confirm ?"), - body: "", -}; - -Registries.Component.add(ConfirmPopup); - -export default ConfirmPopup; +export class ConfirmPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "ConfirmPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + cancelText: _lt("Cancel"), + title: _lt("Confirm ?"), + body: "", + }; +} diff --git a/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js b/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js index 2d4eb65b541..d47b9f8d6a4 100644 --- a/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js @@ -1,10 +1,19 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; -class ControlButtonPopup extends AbstractAwaitablePopup { +import { Draggable } from "../Misc/Draggable"; + +export class ControlButtonPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "ControlButtonPopup"; + static defaultProps = { + cancelText: _lt("Back"), + controlButtons: [], + confirmKey: false, + }; + /** * @param {Object} props * @param {string} props.startingValue @@ -14,13 +23,3 @@ class ControlButtonPopup extends AbstractAwaitablePopup { this.controlButtons = this.props.controlButtons; } } -ControlButtonPopup.template = "ControlButtonPopup"; -ControlButtonPopup.defaultProps = { - cancelText: _lt("Back"), - controlButtons: [], - confirmKey: false, -}; - -Registries.Component.add(ControlButtonPopup); - -export default ControlButtonPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/DatePickerPopup.js b/addons/point_of_sale/static/src/js/Popups/DatePickerPopup.js index e7a7864a277..98714b62269 100644 --- a/addons/point_of_sale/static/src/js/Popups/DatePickerPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/DatePickerPopup.js @@ -1,12 +1,18 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; const { onMounted, useRef, useState } = owl; -class DatePickerPopup extends AbstractAwaitablePopup { +export class DatePickerPopup extends AbstractAwaitablePopup { + static template = 'DatePickerPopup'; + static defaultProps = { + confirmText: _lt('Confirm'), + cancelText: _lt('Discard'), + title: _lt('DatePicker'), + }; + setup() { super.setup(); this.state = useState({shippingDate: this._today()}); @@ -21,13 +27,3 @@ class DatePickerPopup extends AbstractAwaitablePopup { } } -DatePickerPopup.template = 'DatePickerPopup'; -DatePickerPopup.defaultProps = { - confirmText: _lt('Confirm'), - cancelText: _lt('Discard'), - title: _lt('DatePicker'), -}; - -Registries.Component.add(DatePickerPopup); - -export default DatePickerPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/EditListInput.js b/addons/point_of_sale/static/src/js/Popups/EditListInput.js index 3b48b008fae..588b2a50f67 100644 --- a/addons/point_of_sale/static/src/js/Popups/EditListInput.js +++ b/addons/point_of_sale/static/src/js/Popups/EditListInput.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; /** * props { @@ -10,7 +9,9 @@ import Registries from "@point_of_sale/js/Registries"; * item: object, * } */ -class EditListInput extends PosComponent { +export class EditListInput extends PosComponent { + static template = "EditListInput"; + onKeyup(event) { if (event.key === "Enter" && event.target.value.trim() !== "") { this.props.createNewItem(); @@ -20,8 +21,3 @@ class EditListInput extends PosComponent { this.props.onInputChange(this.props.item._id, event.target.value); } } -EditListInput.template = "EditListInput"; - -Registries.Component.add(EditListInput); - -export default EditListInput; diff --git a/addons/point_of_sale/static/src/js/Popups/EditListPopup.js b/addons/point_of_sale/static/src/js/Popups/EditListPopup.js index c1aa8220674..7f0f042e70c 100644 --- a/addons/point_of_sale/static/src/js/Popups/EditListPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/EditListPopup.js @@ -1,10 +1,11 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { useAutoFocusToLast } from "@point_of_sale/js/custom_hooks"; import { _lt } from "@web/core/l10n/translation"; +import { EditListInput } from "./EditListInput"; + const { useState } = owl; /** @@ -25,7 +26,7 @@ const { useState } = owl; * * // supply the items to the popup and wait for user's response * // when user pressed `confirm` in the popup, the changes he made will be returned by the showPopup function. - * const { confirmed, payload: newNames } = await this.showPopup('EditListPopup', { + * const { confirmed, payload: newNames } = await this.showPopup(EditListPopup, { * title: "Can you confirm this item?", * array: names }) * @@ -39,7 +40,16 @@ const { useState } = owl; * } * ``` */ -class EditListPopup extends AbstractAwaitablePopup { +export class EditListPopup extends AbstractAwaitablePopup { + static components = { EditListInput }; + static template = "EditListPopup"; + static defaultProps = { + confirmText: _lt("Add"), + cancelText: _lt("Discard"), + array: [], + isSingleItem: false, + }; + /** * @param {String} title required title of popup * @param {Array} [props.array=[]] the array of { id, text } to be edited or an array of strings @@ -104,14 +114,3 @@ class EditListPopup extends AbstractAwaitablePopup { }; } } -EditListPopup.template = "EditListPopup"; -EditListPopup.defaultProps = { - confirmText: _lt("Add"), - cancelText: _lt("Discard"), - array: [], - isSingleItem: false, -}; - -Registries.Component.add(EditListPopup); - -export default EditListPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js b/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js index 9a43be839cc..7d715bbb68e 100644 --- a/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js @@ -1,26 +1,25 @@ /** @odoo-module */ -import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup"; -import Registries from "@point_of_sale/js/Registries"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + // formerly ErrorBarcodePopupWidget -class ErrorBarcodePopup extends ErrorPopup { +export class ErrorBarcodePopup extends ErrorPopup { + static components = { Draggable }; + static template = "ErrorBarcodePopup"; + static defaultProps = { + confirmText: _lt("Ok"), + cancelText: _lt("Cancel"), + title: _lt("Error"), + body: "", + message: _lt( + "The Point of Sale could not find any product, customer, employee or action associated with the scanned barcode." + ), + }; + get translatedMessage() { return this.env._t(this.props.message); } } -ErrorBarcodePopup.template = "ErrorBarcodePopup"; -ErrorBarcodePopup.defaultProps = { - confirmText: _lt("Ok"), - cancelText: _lt("Cancel"), - title: _lt("Error"), - body: "", - message: _lt( - "The Point of Sale could not find any product, customer, employee or action associated with the scanned barcode." - ), -}; - -Registries.Component.add(ErrorBarcodePopup); - -export default ErrorBarcodePopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js b/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js index d685efa91c8..00c3cca7d16 100644 --- a/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js @@ -1,11 +1,18 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; // formerly ErrorPopupWidget -class ErrorPopup extends AbstractAwaitablePopup { +export class ErrorPopup extends AbstractAwaitablePopup { + static template = "ErrorPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + title: _lt("Error"), + body: "", + cancelKey: false, + }; + setup() { super.setup(); owl.onMounted(this.onMounted); @@ -14,14 +21,3 @@ class ErrorPopup extends AbstractAwaitablePopup { this.playSound("error"); } } -ErrorPopup.template = "ErrorPopup"; -ErrorPopup.defaultProps = { - confirmText: _lt("Ok"), - title: _lt("Error"), - body: "", - cancelKey: false, -}; - -Registries.Component.add(ErrorPopup); - -export default ErrorPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js b/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js index 42dbb93a590..61212835e59 100644 --- a/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js @@ -1,11 +1,22 @@ /** @odoo-module */ -import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup"; -import Registries from "@point_of_sale/js/Registries"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { _lt } from "@web/core/l10n/translation"; // formerly ErrorTracebackPopupWidget -class ErrorTracebackPopup extends ErrorPopup { +export class ErrorTracebackPopup extends ErrorPopup { + static template = "ErrorTracebackPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + cancelText: _lt("Cancel"), + confirmKey: false, + title: _lt("Error with Traceback"), + body: "", + exitButtonIsShown: false, + exitButtonText: _lt("Exit Pos"), + exitButtonTrigger: "close-pos", + }; + get tracebackUrl() { const blob = new Blob([this.props.body]); const URL = window.URL || window.webkitURL; @@ -27,18 +38,3 @@ class ErrorTracebackPopup extends ErrorPopup { ); } } -ErrorTracebackPopup.template = "ErrorTracebackPopup"; -ErrorTracebackPopup.defaultProps = { - confirmText: _lt("Ok"), - cancelText: _lt("Cancel"), - confirmKey: false, - title: _lt("Error with Traceback"), - body: "", - exitButtonIsShown: false, - exitButtonText: _lt("Exit Pos"), - exitButtonTrigger: "close-pos", -}; - -Registries.Component.add(ErrorTracebackPopup); - -export default ErrorTracebackPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js b/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js index 5068a23317a..3657d0d5136 100644 --- a/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js @@ -1,11 +1,15 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; + +import { CurrencyAmount } from "../Misc/CurrencyAmount"; const { useState } = owl; -class MoneyDetailsPopup extends AbstractAwaitablePopup { +export class MoneyDetailsPopup extends AbstractAwaitablePopup { + static components = { CurrencyAmount }; + static template = "MoneyDetailsPopup"; + setup() { super.setup(); this.currency = this.env.pos.currency; @@ -51,8 +55,3 @@ class MoneyDetailsPopup extends AbstractAwaitablePopup { }; } } - -MoneyDetailsPopup.template = "MoneyDetailsPopup"; -Registries.Component.add(MoneyDetailsPopup); - -export default MoneyDetailsPopup; 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 c20515d8839..5bd0ee9824c 100644 --- a/addons/point_of_sale/static/src/js/Popups/NumberPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js @@ -2,15 +2,27 @@ import core from "web.core"; var _t = core._t; -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; + +import { Draggable } from "../Misc/Draggable"; const { useState } = owl; -// formerly NumberPopupWidget -class NumberPopup extends AbstractAwaitablePopup { +export class NumberPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "NumberPopup"; + static defaultProps = { + confirmText: _t("Confirm"), + cancelText: _t("Discard"), + title: _t("Confirm ?"), + body: "", + cheap: false, + startingValue: null, + isPassword: false, + }; + /** * @param {Object} props * @param {Boolean} props.isPassword Show password popup. @@ -32,7 +44,7 @@ class NumberPopup extends AbstractAwaitablePopup { .replace(".", this.decimalSeparator); } this.state = useState({ buffer: startingBuffer, toStartOver: this.props.isInputSelected }); - NumberBuffer.use({ + numberBuffer.use({ nonKeyboardInputEvent: "numpad-click-input", triggerAtEnter: "accept-input", triggerAtEscape: "close-this-popup", @@ -53,7 +65,7 @@ class NumberPopup extends AbstractAwaitablePopup { } } confirm(event) { - if (NumberBuffer.get()) { + if (numberBuffer.get()) { super.confirm(); } } @@ -61,20 +73,6 @@ class NumberPopup extends AbstractAwaitablePopup { this.trigger("numpad-click-input", { key }); } getPayload() { - return NumberBuffer.get(); + return numberBuffer.get(); } } -NumberPopup.template = "NumberPopup"; -NumberPopup.defaultProps = { - confirmText: _t("Confirm"), - cancelText: _t("Discard"), - title: _t("Confirm ?"), - body: "", - cheap: false, - startingValue: null, - isPassword: false, -}; - -Registries.Component.add(NumberPopup); - -export default NumberPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js b/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js index e8d878a720e..0f3d9bcb169 100644 --- a/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js @@ -1,28 +1,27 @@ /** @odoo-module */ -import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup"; -import Registries from "@point_of_sale/js/Registries"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + /** * This is a special kind of error popup as it introduces * an option to not show it again. */ -class OfflineErrorPopup extends ErrorPopup { +export class OfflineErrorPopup extends ErrorPopup { + static components = { Draggable }; + static template = "OfflineErrorPopup"; + static dontShow = false; + static defaultProps = { + confirmText: _lt("Ok"), + cancelText: _lt("Cancel"), + title: _lt("Offline Error"), + body: _lt("Either the server is inaccessible or browser is not connected online."), + }; + dontShowAgain() { this.constructor.dontShow = true; this.cancel(); } } -OfflineErrorPopup.template = "OfflineErrorPopup"; -OfflineErrorPopup.dontShow = false; -OfflineErrorPopup.defaultProps = { - confirmText: _lt("Ok"), - cancelText: _lt("Cancel"), - title: _lt("Offline Error"), - body: _lt("Either the server is inaccessible or browser is not connected online."), -}; - -Registries.Component.add(OfflineErrorPopup); - -export default OfflineErrorPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js b/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js index aa16638428b..014671cd0a8 100644 --- a/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js @@ -1,11 +1,20 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + // formerly OrderImportPopupWidget -class OrderImportPopup extends AbstractAwaitablePopup { +export class OrderImportPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "OrderImportPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + cancelKey: false, + body: "", + }; + get unpaidSkipped() { return ( (this.props.report.unpaid_skipped_existing || 0) + @@ -14,13 +23,3 @@ class OrderImportPopup extends AbstractAwaitablePopup { } getPayload() {} } -OrderImportPopup.template = "OrderImportPopup"; -OrderImportPopup.defaultProps = { - confirmText: _lt("Ok"), - cancelKey: false, - body: "", -}; - -Registries.Component.add(OrderImportPopup); - -export default OrderImportPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/PosPopupController.js b/addons/point_of_sale/static/src/js/Popups/PosPopupController.js index 9e01e95f02e..6dc72e4edde 100644 --- a/addons/point_of_sale/static/src/js/Popups/PosPopupController.js +++ b/addons/point_of_sale/static/src/js/Popups/PosPopupController.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import Registries from "@point_of_sale/js/Registries"; -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { useBus } from "@web/core/utils/hooks"; /** @@ -25,7 +24,10 @@ import { useBus } from "@web/core/utils/hooks"; * @typedef {{ id: number, resolve: Function, keepBehind?: boolean, cancelKey?: string, confirmKey?: string }} BasePopupProps * @typedef {{ name: string, component: AbstractAwaitablePopup, props: BasePopupProps, key: string }} Popup */ -class PosPopupController extends PosComponent { +export class PosPopupController extends PosComponent { + static template = "point_of_sale.PosPopupController"; + static components = {}; + setup() { super.setup(); useBus(this.env.posbus, "show-popup", this._showPopup); @@ -33,24 +35,33 @@ class PosPopupController extends PosComponent { owl.useExternalListener(window, "keyup", this._onWindowKeyup); this.popups = owl.useState([]); } + /** + * A popup can be cancelled/confirmed with 'Escape'/'Enter' key by default. + * Also, if it's not the top popup, it is hidden from the view. + * This can be overridden by the default props of the popop component + * and the props used in requesting to show the popup. + * + * @param {AbstractAwaitablePopup} popupComponent + * @param {Object} props + * @returns {BasePopupProps} + */ _showPopup(event) { - let { id, name, props, resolve } = event.detail; + let { id, component, props, resolve } = event.detail; props = Object.assign(props || {}, { id, resolve }); - const component = this.constructor.components[name]; - if (!component) { - throw new Error( - `'${name}' is not found. Make sure the file is loaded and the component is properly registered using 'Registries.Component.add'.` - ); - } if (component.dontShow) { resolve(); return; } this.popups.push({ - name, component, - props: this._constructPopupProps(component, props), - key: `${name}-${id}`, + props: { + keepBehind: false, + cancelKey: "Escape", + confirmKey: "Enter", + // FIXME POSREF assigning the default props by hand defeats the point of default props + ...component.defaultProps, + ...props, + }, }); } _closePopup(event) { @@ -76,28 +87,6 @@ class PosPopupController extends PosComponent { this.env.posbus.trigger(`confirm-popup-${this.topPopup.props.id}`); } } - /** - * A popup can be cancelled/confirmed with 'Escape'/'Enter' key by default. - * Also, if it's not the top popup, it is hidden from the view. - * This can be overridden by the default props of the popop component - * and the props used in requesting to show the popup. - * - * @param {AbstractAwaitablePopup} popupComponent - * @param {Object} props - * @returns {BasePopupProps} - */ - _constructPopupProps(popupComponent, props) { - const defaultProps = popupComponent.defaultProps || {}; - return Object.assign( - { - keepBehind: false, - cancelKey: "Escape", - confirmKey: "Enter", - }, - defaultProps, - props - ); - } /** * @returns {boolean} Hide the element of this component when this returns false. */ @@ -120,7 +109,3 @@ class PosPopupController extends PosComponent { return this.topPopup === popup || popup.props.keepBehind; } } -PosPopupController.template = "point_of_sale.PosPopupController"; -Registries.Component.add(PosPopupController); - -export default PosPopupController; diff --git a/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js b/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js index cb00c72f03f..ea213adf559 100644 --- a/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js @@ -1,35 +1,9 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; const { useState, useSubEnv } = owl; -export class ProductConfiguratorPopup extends AbstractAwaitablePopup { - setup() { - super.setup(); - useSubEnv({ attribute_components: [] }); - } - - getPayload() { - var selected_attributes = []; - var price_extra = 0.0; - - this.env.attribute_components.forEach((attribute_component) => { - const { value, extra } = attribute_component.getValue(); - selected_attributes.push(value); - price_extra += extra; - }); - - return { - selected_attributes, - price_extra, - }; - } -} -ProductConfiguratorPopup.template = "ProductConfiguratorPopup"; -Registries.Component.add(ProductConfiguratorPopup); - export class BaseProductAttribute extends PosComponent { setup() { super.setup(); @@ -60,6 +34,8 @@ export class BaseProductAttribute extends PosComponent { } export class RadioProductAttribute extends BaseProductAttribute { + static template = "RadioProductAttribute"; + setup() { super.setup(); owl.onMounted(this.onMounted); @@ -72,13 +48,41 @@ export class RadioProductAttribute extends BaseProductAttribute { $(this.el).find('input[type="radio"]:first').prop("checked", true); } } -RadioProductAttribute.template = "RadioProductAttribute"; -Registries.Component.add(RadioProductAttribute); -export class SelectProductAttribute extends BaseProductAttribute {} -SelectProductAttribute.template = "SelectProductAttribute"; -Registries.Component.add(SelectProductAttribute); +export class SelectProductAttribute extends BaseProductAttribute { + static template = "SelectProductAttribute"; +} -export class ColorProductAttribute extends BaseProductAttribute {} -ColorProductAttribute.template = "ColorProductAttribute"; -Registries.Component.add(ColorProductAttribute); +export class ColorProductAttribute extends BaseProductAttribute { + static template = "ColorProductAttribute"; +} + +export class ProductConfiguratorPopup extends AbstractAwaitablePopup { + static template = "ProductConfiguratorPopup"; + static components = { + RadioProductAttribute, + SelectProductAttribute, + ColorProductAttribute + }; + + setup() { + super.setup(); + useSubEnv({ attribute_components: [] }); + } + + getPayload() { + var selected_attributes = []; + var price_extra = 0.0; + + this.env.attribute_components.forEach((attribute_component) => { + const { value, extra } = attribute_component.getValue(); + selected_attributes.push(value); + price_extra += extra; + }); + + return { + selected_attributes, + price_extra, + }; + } +} diff --git a/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js b/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js index 9e7b9ce05c3..2216a680ffe 100644 --- a/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; /** * Props: @@ -9,7 +8,10 @@ import Registries from "@point_of_sale/js/Registries"; * info: {object of data} * } */ -class ProductInfoPopup extends AbstractAwaitablePopup { +export class ProductInfoPopup extends AbstractAwaitablePopup { + static template = "ProductInfoPopup"; + static defaultProps = { confirmKey: false }; + setup() { super.setup(); Object.assign(this, this.props.info); @@ -25,7 +27,3 @@ class ProductInfoPopup extends AbstractAwaitablePopup { return isAccessibleToEveryUser || isCashierManager; } } - -ProductInfoPopup.template = "ProductInfoPopup"; -ProductInfoPopup.defaultProps = { confirmKey: false }; -Registries.Component.add(ProductInfoPopup); diff --git a/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js b/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js index 124b53ec395..2877561953b 100644 --- a/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js @@ -1,13 +1,24 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + const { useState } = owl; // formerly SelectionPopupWidget -class SelectionPopup extends AbstractAwaitablePopup { +export class SelectionPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "SelectionPopup"; + static defaultProps = { + cancelText: _lt("Cancel"), + title: _lt("Select"), + body: "", + list: [], + confirmKey: false, + }; + /** * Value of the `item` key of the selected element in the Selection * Array is the payload of this popup. @@ -43,15 +54,3 @@ class SelectionPopup extends AbstractAwaitablePopup { return selected && selected.item; } } -SelectionPopup.template = "SelectionPopup"; -SelectionPopup.defaultProps = { - cancelText: _lt("Cancel"), - title: _lt("Select"), - body: "", - list: [], - confirmKey: false, -}; - -Registries.Component.add(SelectionPopup); - -export default SelectionPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js b/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js index 42123d93933..b331c81af61 100644 --- a/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js @@ -1,15 +1,25 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + const { onMounted, useRef, useState } = owl; // formerly TextAreaPopupWidget // IMPROVEMENT: This code is very similar to TextInputPopup. // Combining them would reduce the code. -class TextAreaPopup extends AbstractAwaitablePopup { +export class TextAreaPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "TextAreaPopup"; + static defaultProps = { + confirmText: _lt("Add"), + cancelText: _lt("Discard"), + title: "", + body: "", + }; + /** * @param {Object} props * @param {string} props.startingValue @@ -27,14 +37,3 @@ class TextAreaPopup extends AbstractAwaitablePopup { return this.state.inputValue; } } -TextAreaPopup.template = "TextAreaPopup"; -TextAreaPopup.defaultProps = { - confirmText: _lt("Add"), - cancelText: _lt("Discard"), - title: "", - body: "", -}; - -Registries.Component.add(TextAreaPopup); - -export default TextAreaPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js b/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js index 26aa028f40d..2ab6a397b05 100644 --- a/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js @@ -1,13 +1,22 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; const { onMounted, useRef, useState } = owl; // formerly TextInputPopupWidget -class TextInputPopup extends AbstractAwaitablePopup { +export class TextInputPopup extends AbstractAwaitablePopup { + static template = "TextInputPopup"; + static defaultProps = { + confirmText: _lt("Confirm"), + cancelText: _lt("Discard"), + title: "", + body: "", + startingValue: "", + placeholder: "", + }; + setup() { super.setup(); this.state = useState({ inputValue: this.props.startingValue }); @@ -21,16 +30,3 @@ class TextInputPopup extends AbstractAwaitablePopup { return this.state.inputValue; } } -TextInputPopup.template = "TextInputPopup"; -TextInputPopup.defaultProps = { - confirmText: _lt("Confirm"), - cancelText: _lt("Discard"), - title: "", - body: "", - startingValue: "", - placeholder: "", -}; - -Registries.Component.add(TextInputPopup); - -export default TextInputPopup; diff --git a/addons/point_of_sale/static/src/js/PosComponent.js b/addons/point_of_sale/static/src/js/PosComponent.js index 1e5d9a425ba..1eba2538564 100644 --- a/addons/point_of_sale/static/src/js/PosComponent.js +++ b/addons/point_of_sale/static/src/js/PosComponent.js @@ -4,7 +4,8 @@ import { LegacyComponent } from "@web/legacy/legacy_component"; let nextId = 0; -class PosComponent extends LegacyComponent { +export class PosComponent extends LegacyComponent { + static components = {}; /** * This function is available to all Components that inherit this class. * The goal of this function is to show an awaitable dialog (popup) that @@ -13,10 +14,7 @@ class PosComponent extends LegacyComponent { * * ``` * async getUserName() { - * const userResponse = await this.showPopup( - * 'TextInputPopup', - * { title: 'What is your name?' } - * ); + * const userResponse = await this.showPopup(TextInputPopup, { title: 'What is your name?' }); * // at this point, the TextInputPopup is displayed. Depending on how the popup is defined, * // say the input contains the name, the result of the interaction with the user is * // saved in `userResponse`. @@ -27,9 +25,9 @@ class PosComponent extends LegacyComponent { * @param {String} name Name of the popup component * @param {Object} props Object that will be used to render to popup */ - showPopup(name, props) { + showPopup(component, props) { return new Promise((resolve) => { - this.env.posbus.trigger("show-popup", { name, props, resolve, id: nextId++ }); + this.env.posbus.trigger("show-popup", { component, props, resolve, id: nextId++ }); }); } showTempScreen(name, props) { @@ -61,5 +59,3 @@ class PosComponent extends LegacyComponent { this.trigger("close-notification"); } } - -export default PosComponent; diff --git a/addons/point_of_sale/static/src/js/Registries.js b/addons/point_of_sale/static/src/js/Registries.js deleted file mode 100644 index b55a3424f9a..00000000000 --- a/addons/point_of_sale/static/src/js/Registries.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @odoo-module */ -/** - * This definition contains all the instances of ClassRegistry. - */ - -import ComponentRegistry from "@point_of_sale/js/ComponentRegistry"; -import ClassRegistry from "@point_of_sale/js/ClassRegistry"; - -class ModelRegistry extends ClassRegistry { - add(baseClass) { - super.add(baseClass); - /** - * Introduce a static method (`create`) to each base class that can be - * conveniently use to create an instance of the extended version - * of the class. - */ - baseClass.create = (...args) => { - const ExtendedClass = this.get(baseClass); - return new ExtendedClass(...args); - }; - } -} - -export const Component = new ComponentRegistry(); -export const Model = new ModelRegistry(); - -export default { Component, Model }; 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 24e969f0604..8b1bfcc0df7 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 @@ -2,12 +2,14 @@ import { _t } from "web.core"; import { getDataURLFromFile } from "web.utils"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; const { onMounted, onWillUnmount } = owl; -class PartnerDetailsEdit extends PosComponent { +export class PartnerDetailsEdit extends PosComponent { + static template = "PartnerDetailsEdit"; + setup() { super.setup(); this.intFields = ["country_id", "state_id", "property_product_pricelist"]; @@ -56,7 +58,7 @@ class PartnerDetailsEdit extends PosComponent { } } if ((!this.props.partner.name && !processedChanges.name) || processedChanges.name === "") { - return this.showPopup("ErrorPopup", { + return this.showPopup(ErrorPopup, { title: _t("A Customer Name Is Required"), }); } @@ -66,7 +68,7 @@ class PartnerDetailsEdit extends PosComponent { async uploadImage(event) { const file = event.target.files[0]; if (!file.type.match(/image.*/)) { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Unsupported File Format"), body: this.env._t( "Only web-compatible Image formats such as .png or .jpeg are supported." @@ -114,7 +116,7 @@ class PartnerDetailsEdit extends PosComponent { const img = new Image(); img.addEventListener("load", () => resolve(img)); img.addEventListener("error", () => { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Loading Image Error"), body: this.env._t("Encountered error when loading image. Please try again."), }); @@ -124,8 +126,3 @@ class PartnerDetailsEdit extends PosComponent { }); } } -PartnerDetailsEdit.template = "PartnerDetailsEdit"; - -Registries.Component.add(PartnerDetailsEdit); - -export default PartnerDetailsEdit; diff --git a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerLine.js b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerLine.js index b3f6d75ac82..99e5733c289 100644 --- a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerLine.js +++ b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerLine.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class PartnerLine extends PosComponent { + static template = "PartnerLine"; -class PartnerLine extends PosComponent { get highlight() { return this._isPartnerSelected ? "highlight" : ""; } @@ -11,8 +12,3 @@ class PartnerLine extends PosComponent { return this.props.partner === this.props.selectedPartner; } } -PartnerLine.template = "PartnerLine"; - -Registries.Component.add(PartnerLine); - -export default PartnerLine; diff --git a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerListScreen.js b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerListScreen.js index 447e6699ece..9e282d64504 100644 --- a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerListScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerListScreen.js @@ -1,11 +1,14 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { registry } from "@web/core/registry"; import { debounce } from "@web/core/utils/timing"; import { useListener } from "@web/core/utils/hooks"; +import { PartnerLine } from "./PartnerLine"; +import { PartnerDetailsEdit } from "./PartnerDetailsEdit"; + const { onWillUnmount, useRef } = owl; /** @@ -23,7 +26,10 @@ const { onWillUnmount, useRef } = owl; * * @props partner - originally selected partner */ -class PartnerListScreen extends PosComponent { +export class PartnerListScreen extends PosComponent { + static components = { PartnerDetailsEdit, PartnerLine }; + static template = "PartnerListScreen"; + setup() { super.setup(); useListener("click-save", () => this.env.bus.trigger("save-partner")); @@ -196,8 +202,5 @@ class PartnerListScreen extends PosComponent { return result; } } -PartnerListScreen.template = "PartnerListScreen"; -Registries.Component.add(PartnerListScreen); - -export default PartnerListScreen; +registry.category("pos_screens").add("PartnerListScreen", PartnerListScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js index c5146c7146e..6228ce74d1a 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js @@ -1,15 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class PSNumpadInputButton extends PosComponent { + static template = "PSNumpadInputButton"; -class PSNumpadInputButton extends PosComponent { get _class() { return this.props.changeClassTo || "input-button number-char"; } } -PSNumpadInputButton.template = "PSNumpadInputButton"; - -Registries.Component.add(PSNumpadInputButton); - -export default PSNumpadInputButton; 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 9eac2d4006c..0632c00c0c9 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,14 +1,30 @@ /** @odoo-module */ import { parse } from "web.field_utils"; -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { useErrorHandlers } from "@point_of_sale/js/custom_hooks"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { registry } from "@web/core/registry"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; import utils from "web.utils"; -class PaymentScreen extends PosComponent { +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { DatePickerPopup } from "@point_of_sale/js/Popups/DatePickerPopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; + +import { PaymentScreenNumpad } from "./PaymentScreenNumpad"; +import { PaymentScreenPaymentLines } from "./PaymentScreenPaymentLines"; +import { PaymentScreenStatus } from "./PaymentScreenStatus"; + +export class PaymentScreen extends PosComponent { + static template = "PaymentScreen"; + static components = { + PaymentScreenNumpad, + PaymentScreenPaymentLines, + PaymentScreenStatus, + }; + setup() { super.setup(); useListener("delete-payment-line", this.deletePaymentLine); @@ -23,14 +39,14 @@ class PaymentScreen extends PosComponent { this.payment_methods_from_config = this.env.pos.payment_methods.filter((method) => this.env.pos.config.payment_method_ids.includes(method.id) ); - NumberBuffer.use(this._getNumberBufferConfig); + numberBuffer.use(this._getNumberBufferConfig); useErrorHandlers(); this.payment_interface = null; this.error = false; } showMaxValueError() { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Maximum value reached"), body: this.env._t( "The amount cannot be higher than the due amount if you don't have a cash payment method configured." @@ -83,10 +99,10 @@ class PaymentScreen extends PosComponent { // original function: click_paymentmethods const result = this.currentOrder.add_paymentline(paymentMethod); if (result) { - NumberBuffer.reset(); + numberBuffer.reset(); return true; } else { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Error"), body: this.env._t("There is already an electronic payment in progress."), }); @@ -108,10 +124,10 @@ class PaymentScreen extends PosComponent { ) { return; } - if (NumberBuffer.get() === null) { + if (numberBuffer.get() === null) { this.deletePaymentLine({ detail: { cid: this.selectedPaymentLine.cid } }); } else { - this.selectedPaymentLine.set_amount(NumberBuffer.getFloat()); + this.selectedPaymentLine.set_amount(numberBuffer.getFloat()); } } toggleIsToInvoice() { @@ -128,7 +144,7 @@ class PaymentScreen extends PosComponent { const change = this.currentOrder.get_change(); const value = tip === 0 && change > 0 ? change : tip; - const { confirmed, payload } = await this.showPopup("NumberPopup", { + const { confirmed, payload } = await this.showPopup(NumberPopup, { title: tip ? this.env._t("Change Tip") : this.env._t("Add Tip"), startingValue: value, isInputSelected: true, @@ -140,7 +156,7 @@ class PaymentScreen extends PosComponent { } async toggleShippingDatePicker() { if (!this.currentOrder.getShippingDate()) { - const { confirmed, payload: shippingDate } = await this.showPopup("DatePickerPopup", { + const { confirmed, payload: shippingDate } = await this.showPopup(DatePickerPopup, { title: this.env._t("Select the shipping date"), }); if (confirmed) { @@ -164,12 +180,12 @@ class PaymentScreen extends PosComponent { .send_payment_cancel(this.currentOrder, cid) .then(function () { self.currentOrder.remove_paymentline(line); - NumberBuffer.reset(); + numberBuffer.reset(); self.render(true); }); } else if (line.get_payment_status() !== "waitingCancel") { this.currentOrder.remove_paymentline(line); - NumberBuffer.reset(); + numberBuffer.reset(); this.render(true); } } @@ -177,13 +193,13 @@ class PaymentScreen extends PosComponent { const { cid } = event.detail; const line = this.paymentLines.find((line) => line.cid === cid); this.currentOrder.select_paymentline(line); - NumberBuffer.reset(); + numberBuffer.reset(); this.render(true); } async validateOrder(isForceValidate) { if (this.env.pos.config.cash_rounding) { if (!this.env.pos.get_order().check_paymentlines_rounding()) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Rounding error in payment lines"), body: this.env._t( "The amount of your payment lines must be rounded to validate the transaction." @@ -243,7 +259,7 @@ class PaymentScreen extends PosComponent { syncOrderResult.map((res) => res.id) ); if (!postPushResult) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Error: no internet connection."), body: this.env._t( "Some, if not all, post-processing after syncing order failed." @@ -275,7 +291,7 @@ class PaymentScreen extends PosComponent { // Ask the user to sync the remaining unsynced orders. if (!hasError && syncOrderResult && this.env.pos.db.get_orders().length) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Remaining unsynced orders"), body: this.env._t( "There are unsynced orders. Do you want to sync these orders?" @@ -294,18 +310,19 @@ class PaymentScreen extends PosComponent { return !this.error ? "ReceiptScreen" : "ProductScreen"; } paymentMethodImage(id) { - if (this.paymentMethod.image) + if (this.paymentMethod.image) { return `/web/image/pos.payment.method/${id}/image`; - else if (this.paymentMethod.type === "cash") + } else if (this.paymentMethod.type === "cash") { return "/point_of_sale/static/src/img/money.png"; - else if(this.paymentMethod.type === "pay_later") + } else if (this.paymentMethod.type === "pay_later") { return "/point_of_sale/static/src/img/pay-later.png"; - else + } else { return "/point_of_sale/static/src/img/card-bank.png"; + } } async _isOrderValid(isForceValidate) { if (this.currentOrder.get_orderlines().length === 0 && this.currentOrder.is_to_invoice()) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Empty Order"), body: this.env._t( "There must be at least one product in your order before it can be validated and invoiced." @@ -319,7 +336,7 @@ class PaymentScreen extends PosComponent { ); if (splitPayments.length && !this.currentOrder.get_partner()) { const paymentMethod = splitPayments[0].payment_method; - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Customer Required"), body: _.str.sprintf( this.env._t("Customer is required for %s payment method."), @@ -336,7 +353,7 @@ class PaymentScreen extends PosComponent { (this.currentOrder.is_to_invoice() || this.currentOrder.getShippingDate()) && !this.currentOrder.get_partner() ) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Please select the Customer"), body: this.env._t( "You need to select the customer before you can invoice or ship an order." @@ -353,7 +370,7 @@ class PaymentScreen extends PosComponent { this.currentOrder.getShippingDate() && !(partner.name && partner.street && partner.city && partner.country_id) ) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Incorrect address for shipping"), body: this.env._t("The selected customer needs an address."), }); @@ -374,7 +391,7 @@ class PaymentScreen extends PosComponent { if (this.currentOrder.has_not_valid_rounding()) { var line = this.currentOrder.has_not_valid_rounding(); - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Incorrect rounding"), body: this.env._t( "You have to round your payments lines." + line.amount + " is not rounded." @@ -396,7 +413,7 @@ class PaymentScreen extends PosComponent { cash = cash || this.env.pos.payment_methods[i].is_cash_count; } if (!cash) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Cannot return change without a cash payment method"), body: this.env._t( "There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration" @@ -412,7 +429,7 @@ class PaymentScreen extends PosComponent { this.currentOrder.get_total_with_tax() > 0 && this.currentOrder.get_total_with_tax() * 1000 < this.currentOrder.get_total_paid() ) { - this.showPopup("ConfirmPopup", { + this.showPopup(ConfirmPopup, { title: this.env._t("Please Confirm Large Amount"), body: this.env._t("Are you sure that the customer wants to pay") + @@ -500,8 +517,5 @@ class PaymentScreen extends PosComponent { line.set_payment_status("done"); } } -PaymentScreen.template = "PaymentScreen"; -Registries.Component.add(PaymentScreen); - -export default PaymentScreen; +registry.category("pos_screens").add("PaymentScreen", PaymentScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js index 8db09d3f16f..e971c77cdb2 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js @@ -1,16 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +import { PSNumpadInputButton } from "./PSNumpadInputButton"; + +export class PaymentScreenNumpad extends PosComponent { + static components = { PSNumpadInputButton }; + static template = "PaymentScreenNumpad"; -class PaymentScreenNumpad extends PosComponent { setup() { super.setup(); this.decimalPoint = this.env._t.database.parameters.decimal_point; } } -PaymentScreenNumpad.template = "PaymentScreenNumpad"; - -Registries.Component.add(PaymentScreenNumpad); - -export default PaymentScreenNumpad; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js index 7a0b4185284..13ed6001f21 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class PaymentScreenPaymentLines extends PosComponent { + static template = "PaymentScreenPaymentLines"; -class PaymentScreenPaymentLines extends PosComponent { formatLineAmount(paymentline) { return this.env.pos.format_currency_no_symbol(paymentline.get_amount()); } @@ -14,8 +15,3 @@ class PaymentScreenPaymentLines extends PosComponent { return {}; } } -PaymentScreenPaymentLines.template = "PaymentScreenPaymentLines"; - -Registries.Component.add(PaymentScreenPaymentLines); - -export default PaymentScreenPaymentLines; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js index 2cdc39e7081..54e9673af7c 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class PaymentScreenStatus extends PosComponent { + static template = "PaymentScreenStatus"; -class PaymentScreenStatus extends PosComponent { get changeText() { return this.env.pos.format_currency(this.props.order.get_change()); } @@ -18,8 +19,3 @@ class PaymentScreenStatus extends PosComponent { ); } } -PaymentScreenStatus.template = "PaymentScreenStatus"; - -Registries.Component.add(PaymentScreenStatus); - -export default PaymentScreenStatus; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js index b7b8ece117e..4c3b5ff5a26 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js @@ -1,23 +1,19 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; /** * @props partner * @emits click-partner * @emits click-pay */ -class ActionpadWidget extends PosComponent { +export class ActionpadWidget extends PosComponent { + static template = "ActionpadWidget"; + static defaultProps = { + isActionButtonHighlighted: false, + }; + get isLongName() { return this.props.partner && this.props.partner.name.length > 10; } } -ActionpadWidget.template = "ActionpadWidget"; -ActionpadWidget.defaultProps = { - isActionButtonHighlighted: false, -}; - -Registries.Component.add(ActionpadWidget); - -export default ActionpadWidget; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/CategoryButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/CategoryButton.js index af034bf1d1a..ad7a7e02f12 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/CategoryButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/CategoryButton.js @@ -1,16 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class CategoryButton extends PosComponent { + static template = "CategoryButton"; -class CategoryButton extends PosComponent { get imageUrl() { const category = this.props.category; return `/web/image?model=pos.category&field=image_128&id=${category.id}&unique=${category.write_date}`; } } -CategoryButton.template = "CategoryButton"; - -Registries.Component.add(CategoryButton); - -export default CategoryButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineCustomerNoteButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineCustomerNoteButton.js index 2445f3e3d67..c1345e4a7d9 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineCustomerNoteButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineCustomerNoteButton.js @@ -1,11 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { TextAreaPopup } from "@point_of_sale/js/Popups/TextAreaPopup"; + +export class OrderlineCustomerNoteButton extends PosComponent { + static template = "OrderlineCustomerNoteButton"; -class OrderlineCustomerNoteButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -16,7 +18,7 @@ class OrderlineCustomerNoteButton extends PosComponent { return; } - const { confirmed, payload: inputNote } = await this.showPopup("TextAreaPopup", { + const { confirmed, payload: inputNote } = await this.showPopup(TextAreaPopup, { startingValue: selectedOrderline.get_customer_note(), title: this.env._t("Add Customer Note"), }); @@ -26,12 +28,7 @@ class OrderlineCustomerNoteButton extends PosComponent { } } } -OrderlineCustomerNoteButton.template = "OrderlineCustomerNoteButton"; ProductScreen.addControlButton({ component: OrderlineCustomerNoteButton, }); - -Registries.Component.add(OrderlineCustomerNoteButton); - -export default OrderlineCustomerNoteButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/ProductInfoButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/ProductInfoButton.js index 4c8440ca6fc..091ed2fd88f 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/ProductInfoButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/ProductInfoButton.js @@ -1,11 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { ProductInfoPopup } from "@point_of_sale/js/Popups/ProductInfoPopup"; + +export class ProductInfoButton extends PosComponent { + static template = "ProductInfoButton"; -class ProductInfoButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -16,17 +18,12 @@ class ProductInfoButton extends PosComponent { const product = orderline.get_product(); const quantity = orderline.get_quantity(); const info = await this.env.pos.getProductInfo(product, quantity); - this.showPopup("ProductInfoPopup", { info: info, product: product }); + this.showPopup(ProductInfoPopup, { info: info, product: product }); } } } -ProductInfoButton.template = "ProductInfoButton"; ProductScreen.addControlButton({ component: ProductInfoButton, position: ["before", "SetFiscalPositionButton"], }); - -Registries.Component.add(ProductInfoButton); - -export default ProductInfoButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/RefundButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/RefundButton.js index b21861ff640..56019ebc476 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/RefundButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/RefundButton.js @@ -1,11 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -class RefundButton extends PosComponent { +export class RefundButton extends PosComponent { + static template = "point_of_sale.RefundButton"; + setup() { super.setup(); useListener("click", this._onClick); @@ -19,7 +20,6 @@ class RefundButton extends PosComponent { }); } } -RefundButton.template = "point_of_sale.RefundButton"; ProductScreen.addControlButton({ component: RefundButton, @@ -27,7 +27,3 @@ ProductScreen.addControlButton({ return true; }, }); - -Registries.Component.add(RefundButton); - -export default RefundButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js index 0a0a55f2aec..0ee0b354ee4 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js @@ -1,11 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; + +export class SetFiscalPositionButton extends PosComponent { + static template = "SetFiscalPositionButton"; -class SetFiscalPositionButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -38,7 +40,7 @@ class SetFiscalPositionButton extends PosComponent { }); } const { confirmed, payload: selectedFiscalPosition } = await this.showPopup( - "SelectionPopup", + SelectionPopup, { title: this.env._t("Select Fiscal Position"), list: fiscalPosList, @@ -54,7 +56,6 @@ class SetFiscalPositionButton extends PosComponent { } } } -SetFiscalPositionButton.template = "SetFiscalPositionButton"; ProductScreen.addControlButton({ component: SetFiscalPositionButton, @@ -63,7 +64,3 @@ ProductScreen.addControlButton({ }, position: ["before", "SetPricelistButton"], }); - -Registries.Component.add(SetFiscalPositionButton); - -export default SetFiscalPositionButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js index 2da6a0bcb2b..7473d55a0e8 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js @@ -1,11 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; + +export class SetPricelistButton extends PosComponent { + static template = "SetPricelistButton"; -class SetPricelistButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -24,21 +26,21 @@ class SetPricelistButton extends PosComponent { const selectionList = this.env.pos.pricelists.map((pricelist) => ({ id: pricelist.id, label: pricelist.name, - isSelected: this.currentOrder.pricelist - && pricelist.id === this.currentOrder.pricelist.id, + isSelected: + this.currentOrder.pricelist && pricelist.id === this.currentOrder.pricelist.id, item: pricelist, })); if (!this.env.pos.default_pricelist) { selectionList.push({ id: null, - label: this.env._t('Default Price'), + label: this.env._t("Default Price"), isSelected: !this.currentOrder.pricelist, item: null, - }) + }); } - const { confirmed, payload: selectedPricelist } = await this.showPopup("SelectionPopup", { + const { confirmed, payload: selectedPricelist } = await this.showPopup(SelectionPopup, { title: this.env._t("Select the pricelist"), list: selectionList, }); @@ -48,7 +50,6 @@ class SetPricelistButton extends PosComponent { } } } -SetPricelistButton.template = "SetPricelistButton"; ProductScreen.addControlButton({ component: SetPricelistButton, @@ -56,7 +57,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.use_pricelist && this.env.pos.pricelists.length > 0; }, }); - -Registries.Component.add(SetPricelistButton); - -export default SetPricelistButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/NumpadWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/NumpadWidget.js index 933fccd1983..d9a690a0583 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/NumpadWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/NumpadWidget.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; /** * @prop {'quantity' | 'price' | 'discount'} activeMode @@ -10,7 +9,13 @@ import Registries from "@point_of_sale/js/Registries"; * @event set-numpad-mode - triggered when mode button is clicked * @event numpad-click-input - triggered when numpad button is clicked */ -class NumpadWidget extends PosComponent { +export class NumpadWidget extends PosComponent { + static template = "NumpadWidget"; + static defaultProps = { + disabledModes: [], + disableSign: false, + }; + get hasPriceControlRights() { return ( this.env.pos.cashierHasPriceControlRights() && @@ -38,12 +43,3 @@ class NumpadWidget extends PosComponent { return this.env._t.database.parameters.decimal_point; } } -NumpadWidget.template = "NumpadWidget"; -NumpadWidget.defaultProps = { - disabledModes: [], - disableSign: false, -}; - -Registries.Component.add(NumpadWidget); - -export default NumpadWidget; 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 b77f9060b10..4fc28439648 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,10 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { float_is_zero } from "web.utils"; -class OrderSummary extends PosComponent { +export class OrderSummary extends PosComponent { + static template = "OrderSummary"; + getTotal() { return this.env.pos.format_currency(this.props.order.get_total_with_tax()); } @@ -18,8 +19,3 @@ class OrderSummary extends PosComponent { }; } } -OrderSummary.template = "OrderSummary"; - -Registries.Component.add(OrderSummary); - -export default OrderSummary; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderWidget.js index b03704f2a87..46feb8667f5 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderWidget.js @@ -1,12 +1,18 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { EditListPopup } from "@point_of_sale/js/Popups/EditListPopup"; + +import { Orderline } from "./Orderline"; +import { OrderSummary } from "./OrderSummary"; const { useEffect, useRef } = owl; -class OrderWidget extends PosComponent { +export class OrderWidget extends PosComponent { + static components = { Orderline, OrderSummary }; + static template = "OrderWidget"; + setup() { super.setup(); useListener("select-line", this._selectLine); @@ -42,7 +48,7 @@ class OrderWidget extends PosComponent { const orderline = event.detail.orderline; const isAllowOnlyOneLot = orderline.product.isAllowOnlyOneLot(); const packLotLinesToEdit = orderline.getPackLotLinesToEdit(isAllowOnlyOneLot); - const { confirmed, payload } = await this.showPopup("EditListPopup", { + const { confirmed, payload } = await this.showPopup(EditListPopup, { title: this.env._t("Lot/Serial Number(s) Required"), name: orderline.product.display_name, isSingleItem: isAllowOnlyOneLot, @@ -62,8 +68,3 @@ class OrderWidget extends PosComponent { this.order.select_orderline(event.detail.orderline); } } -OrderWidget.template = "OrderWidget"; - -Registries.Component.add(OrderWidget); - -export default OrderWidget; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js index 29aa8ee0126..7c4173a5e1d 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class Orderline extends PosComponent { + static template = "Orderline"; -class Orderline extends PosComponent { selectLine() { this.trigger("select-line", { orderline: this.props.line }); } @@ -19,8 +20,3 @@ class Orderline extends PosComponent { return this.props.line.get_customer_note(); } } -Orderline.template = "Orderline"; - -Registries.Component.add(Orderline); - -export default Orderline; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductItem.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductItem.js index 87a42cf4bc4..2eae514a991 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductItem.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductItem.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class ProductItem extends PosComponent { + static template = "ProductItem"; -class ProductItem extends PosComponent { /** * For accessibility, pressing should be like clicking the product. * is not considered because it conflicts with the barcode. @@ -40,8 +41,3 @@ class ProductItem extends PosComponent { } } } -ProductItem.template = "ProductItem"; - -Registries.Component.add(ProductItem); - -export default ProductItem; 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 d9f1ebe879b..20f1a85f680 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 @@ -1,16 +1,39 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ControlButtonsMixin from "@point_of_sale/js/ControlButtonsMixin"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ControlButtonsMixin } from "@point_of_sale/js/ControlButtonsMixin"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { registry } from "@web/core/registry"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; import { parse } from "web.field_utils"; +import { ProductConfiguratorPopup } from "@point_of_sale/js/Popups/ProductConfiguratorPopup"; +import { EditListPopup } from "@point_of_sale/js/Popups/EditListPopup"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ErrorBarcodePopup } from "@point_of_sale/js/Popups/ErrorBarcodePopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; +import { ControlButtonPopup } from "@point_of_sale/js/Popups/ControlButtonPopup"; + +import { ActionpadWidget } from "./ActionpadWidget"; +import { MobileOrderWidget } from "../../Misc/MobileOrderWidget"; +import { NumpadWidget } from "./NumpadWidget"; +import { OrderWidget } from "./OrderWidget"; +import { ProductsWidget } from "./ProductsWidget"; + const { onMounted, useState } = owl; -class ProductScreen extends ControlButtonsMixin(PosComponent) { +export class ProductScreen extends ControlButtonsMixin(PosComponent) { + static template = "ProductScreen"; + static components = { + ActionpadWidget, + MobileOrderWidget, + NumpadWidget, + OrderWidget, + ProductsWidget, + }; + setup() { super.setup(); useListener("update-selected-orderline", this._updateSelectedOrderline); @@ -27,16 +50,16 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { discount: this._barcodeDiscountAction, error: this._barcodeErrorAction, }); - NumberBuffer.use({ + numberBuffer.use({ nonKeyboardInputEvent: "numpad-click-input", triggerAtInput: "update-selected-orderline", useWithBarcode: true, }); onMounted(this.onMounted); - // Call `reset` when the `onMounted` callback in `NumberBuffer.use` is done. + // Call `reset` when the `onMounted` callback in `numberBuffer.use` is done. // We don't do this in the `mounted` lifecycle method because it is called before // the callbacks in `onMounted` hook. - onMounted(() => NumberBuffer.reset()); + onMounted(() => numberBuffer.reset()); this.state = useState({ mobile_pane: this.props.mobile_pane || "right", }); @@ -67,7 +90,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { product.attribute_line_ids, (id) => this.env.pos.attributes_by_ptal_id[id] ).filter((attr) => attr !== undefined); - const { confirmed, payload } = await this.showPopup("ProductConfiguratorPopup", { + const { confirmed, payload } = await this.showPopup(ProductConfiguratorPopup, { product: product, attributes: attributes, }); @@ -100,7 +123,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { packLotLinesToEdit = []; } } - const { confirmed, payload } = await this.showPopup("EditListPopup", { + const { confirmed, payload } = await this.showPopup(EditListPopup, { title: this.env._t("Lot/Serial Number(s) Required"), name: product.display_name, isSingleItem: isAllowOnlyOneLot, @@ -161,16 +184,16 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { } // Add the product after having the extra information. await this._addProduct(product, options); - NumberBuffer.reset(); + numberBuffer.reset(); } _setNumpadMode(event) { const { mode } = event.detail; - NumberBuffer.capture(); - NumberBuffer.reset(); + numberBuffer.capture(); + numberBuffer.reset(); this.env.pos.numpadMode = mode; } _selectLine() { - NumberBuffer.reset(); + numberBuffer.reset(); } async _updateSelectedOrderline(event) { if (this.env.pos.numpadMode === "quantity" && this.env.pos.disallowLineQuantityChange()) { @@ -187,7 +210,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { .get_quantity(); if (selectedLine.noDecrease) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Invalid action"), body: this.env._t("You are not allowed to change this quantity"), }); @@ -206,7 +229,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { const val = buffer === null ? "remove" : buffer; this._setValue(val); if (val == "remove") { - NumberBuffer.reset(); + numberBuffer.reset(); this.env.pos.numpadMode = "quantity"; } } @@ -216,7 +239,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { if (this.env.pos.numpadMode === "quantity") { const result = this.currentOrder.get_selected_orderline().set_quantity(val); if (!result) { - NumberBuffer.reset(); + numberBuffer.reset(); } } else if (this.env.pos.numpadMode === "discount") { this.currentOrder.get_selected_orderline().set_discount(val); @@ -296,7 +319,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { // Why? Because once we start declaring barcode actions in different // screens, these methods will also be declared over and over. _barcodeErrorAction(code) { - this.showPopup("ErrorBarcodePopup", { code: this._codeRepr(code) }); + this.showPopup(ErrorBarcodePopup, { code: this._codeRepr(code) }); } _codeRepr(code) { if (code.code.length > 32) { @@ -306,7 +329,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { } } async _displayAllControlPopup() { - await this.showPopup("ControlButtonPopup", { + await this.showPopup(ControlButtonPopup, { controlButtons: this.controlButtons, }); } @@ -316,7 +339,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { */ async _onScaleNotAvailable() {} async _showDecreaseQuantityPopup() { - const { confirmed, payload: inputNumber } = await this.showPopup("NumberPopup", { + const { confirmed, payload: inputNumber } = await this.showPopup(NumberPopup, { startingValue: 0, title: this.env._t("Set the new quantity"), }); @@ -347,7 +370,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { // IMPROVEMENT: This code snippet is very similar to selectPartner of PaymentScreen. const currentPartner = this.currentOrder.get_partner(); if (currentPartner && this.currentOrder.getHasRefundLines()) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Can't change customer"), body: _.str.sprintf( this.env._t( @@ -377,7 +400,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { (this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots) ) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Some Serial/Lot Numbers are missing"), body: this.env._t( "You are trying to sell products with serial/lot numbers, but some of them are not set.\nWould you like to proceed anyway?" @@ -396,8 +419,5 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { this.state.mobile_pane = this.state.mobile_pane === "left" ? "right" : "left"; } } -ProductScreen.template = "ProductScreen"; -Registries.Component.add(ProductScreen); - -export default ProductScreen; +registry.category("pos_screens").add("ProductScreen", ProductScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js index 5d1a0d6e55c..e59cd92d2e7 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js @@ -1,13 +1,18 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; import { usePos } from "@point_of_sale/app/pos_store"; +import { ProductItem } from "./ProductItem"; +import { ProductsWidgetControlPanel } from "./ProductsWidgetControlPanel"; + const { useState } = owl; -class ProductsWidget extends PosComponent { +export class ProductsWidget extends PosComponent { + static components = { ProductItem, ProductsWidgetControlPanel }; + static template = "ProductsWidget"; + /** * @param {Object} props * @param {number?} props.startCategoryId @@ -78,8 +83,3 @@ class ProductsWidget extends PosComponent { this.trigger("switch-category", 0); } } -ProductsWidget.template = "ProductsWidget"; - -Registries.Component.add(ProductsWidget); - -export default ProductsWidget; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidgetControlPanel.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidgetControlPanel.js index 7d1df6c49df..d1717a65775 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidgetControlPanel.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidgetControlPanel.js @@ -1,13 +1,17 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { debounce } from "@web/core/utils/timing"; import { usePos } from "@point_of_sale/app/pos_store"; +import { CategoryButton } from "./CategoryButton"; + import { onMounted, onWillUnmount, useState } from "@odoo/owl"; -class ProductsWidgetControlPanel extends PosComponent { +export class ProductsWidgetControlPanel extends PosComponent { + static components = { CategoryButton }; + static template = "ProductsWidgetControlPanel"; + setup() { super.setup(); this.pos = usePos(); @@ -88,8 +92,3 @@ class ProductsWidgetControlPanel extends PosComponent { return ProductIds; } } -ProductsWidgetControlPanel.template = "ProductsWidgetControlPanel"; - -Registries.Component.add(ProductsWidgetControlPanel); - -export default ProductsWidgetControlPanel; diff --git a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/OrderReceipt.js b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/OrderReceipt.js index 8de7f185c5f..437af9b4af0 100644 --- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/OrderReceipt.js +++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/OrderReceipt.js @@ -1,11 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +import { WrappedProductNameLines } from "./WrappedProductNameLines"; const { onWillUpdateProps } = owl; -class OrderReceipt extends PosComponent { +export class OrderReceipt extends PosComponent { + static components = { WrappedProductNameLines }; + static template = "OrderReceipt"; + setup() { super.setup(); this._receiptEnv = this.props.order.getOrderReceiptEnv(); @@ -41,8 +45,3 @@ class OrderReceipt extends PosComponent { ); } } -OrderReceipt.template = "OrderReceipt"; - -Registries.Component.add(OrderReceipt); - -export default OrderReceipt; 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 1018af53cdd..ea20f33efff 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 @@ -3,150 +3,149 @@ import { Printer } from "@point_of_sale/js/printers"; import { is_email } from "web.utils"; import { useErrorHandlers } from "@point_of_sale/js/custom_hooks"; -import Registries from "@point_of_sale/js/Registries"; -import AbstractReceiptScreen from "@point_of_sale/js/Misc/AbstractReceiptScreen"; +import { AbstractReceiptScreen } from "@point_of_sale/js/Misc/AbstractReceiptScreen"; +import { registry } from "@web/core/registry"; + +import { OrderReceipt } from "./OrderReceipt"; import { onMounted, useRef, status } from "@odoo/owl"; -const ReceiptScreen = (AbstractReceiptScreen) => { - class ReceiptScreen extends AbstractReceiptScreen { - setup() { - super.setup(); - useErrorHandlers(); - this.orderReceipt = useRef("order-receipt"); - const order = this.currentOrder; - const partner = order.get_partner(); - this.orderUiState = order.uiState.ReceiptScreen; - this.orderUiState.inputEmail = - this.orderUiState.inputEmail || (partner && partner.email) || ""; - this.is_email = is_email; +export class ReceiptScreen extends AbstractReceiptScreen { + static template = "ReceiptScreen"; + static components = { OrderReceipt }; - onMounted(() => { - // Here, we send a task to the event loop that handles - // the printing of the receipt when the component is mounted. - // We are doing this because we want the receipt screen to be - // displayed regardless of what happen to the handleAutoPrint - // call. - setTimeout(async () => { - if (status(this) === "mounted") { - const images = this.orderReceipt.el.getElementsByTagName("img"); - for (const image of images) { - await image.decode(); - } - await this.handleAutoPrint(); + setup() { + super.setup(); + useErrorHandlers(); + this.orderReceipt = useRef("order-receipt"); + const order = this.currentOrder; + const partner = order.get_partner(); + this.orderUiState = order.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 + // the printing of the receipt when the component is mounted. + // We are doing this because we want the receipt screen to be + // displayed regardless of what happen to the handleAutoPrint + // call. + setTimeout(async () => { + if (status(this) === "mounted") { + const images = this.orderReceipt.el.getElementsByTagName("img"); + for (const image of images) { + await image.decode(); } - }, 0); - }); - } - _addNewOrder() { - this.env.pos.add_new_order(); - } - async onSendEmail() { - if (!is_email(this.orderUiState.inputEmail)) { - this.orderUiState.emailSuccessful = false; - this.orderUiState.emailNotice = this.env._t("Invalid email."); - return; - } - try { - await this._sendReceiptToCustomer(); - this.orderUiState.emailSuccessful = true; - this.orderUiState.emailNotice = this.env._t("Email sent."); - } catch { - this.orderUiState.emailSuccessful = false; - this.orderUiState.emailNotice = this.env._t( - "Sending email failed. Please try again." - ); - } - } - get orderAmountPlusTip() { - const order = this.currentOrder; - const orderTotalAmount = order.get_total_with_tax(); - const tip_product_id = - this.env.pos.config.tip_product_id && this.env.pos.config.tip_product_id[0]; - const tipLine = order - .get_orderlines() - .find((line) => tip_product_id && line.product.id === tip_product_id); - const tipAmount = tipLine ? tipLine.get_all_prices().priceWithTax : 0; - const orderAmountStr = this.env.pos.format_currency(orderTotalAmount - tipAmount); - if (!tipAmount) { - return orderAmountStr; - } - const tipAmountStr = this.env.pos.format_currency(tipAmount); - return `${orderAmountStr} + ${tipAmountStr} tip`; - } - get currentOrder() { - return this.env.pos.get_order(); - } - get nextScreen() { - return { name: "ProductScreen" }; - } - whenClosing() { - this.orderDone(); - } - /** - * This function is called outside the rendering call stack. This way, - * we don't block the displaying of ReceiptScreen when it is mounted; additionally, - * any error that can happen during the printing does not affect the rendering. - */ - async handleAutoPrint() { - if (this._shouldAutoPrint()) { - await this.printReceipt(); - if (this.currentOrder._printed && this._shouldCloseImmediately()) { - this.whenClosing(); + await this.handleAutoPrint(); } - } + }, 0); + }); + } + _addNewOrder() { + this.env.pos.add_new_order(); + } + async onSendEmail() { + if (!is_email(this.orderUiState.inputEmail)) { + this.orderUiState.emailSuccessful = false; + this.orderUiState.emailNotice = this.env._t("Invalid email."); + return; } - orderDone() { - this.env.pos.removeOrder(this.currentOrder); - this._addNewOrder(); - const { name, props } = this.nextScreen; - this.showScreen(name, props); - if (this.env.pos.config.iface_customer_facing_display) { - this.env.pos.send_current_order_to_customer_facing_display(); - } - } - async printReceipt() { - const isPrinted = await this._printReceipt(); - if (isPrinted) { - this.currentOrder._printed = true; - } - } - _shouldAutoPrint() { - return this.env.pos.config.iface_print_auto && !this.currentOrder._printed; - } - _shouldCloseImmediately() { - var invoiced_finalized = this.currentOrder.is_to_invoice() - ? this.currentOrder.finalized - : true; - return ( - this.env.proxy.printer && - this.env.pos.config.iface_print_skip_screen && - invoiced_finalized + try { + await this._sendReceiptToCustomer(); + this.orderUiState.emailSuccessful = true; + this.orderUiState.emailNotice = this.env._t("Email sent."); + } catch { + this.orderUiState.emailSuccessful = false; + this.orderUiState.emailNotice = this.env._t( + "Sending email failed. Please try again." ); } - async _sendReceiptToCustomer() { - const printer = new Printer(null, this.env.pos); - const receiptString = this.orderReceipt.el.innerHTML; - const ticketImage = await printer.htmlToImg(receiptString); - const order = this.currentOrder; - const partner = order.get_partner(); - const orderName = order.get_name(); - const orderPartner = { - email: this.orderUiState.inputEmail, - name: partner ? partner.name : this.orderUiState.inputEmail, - }; - const order_server_id = this.env.pos.validated_orders_name_server_id_map[orderName]; - await this.rpc({ - model: "pos.order", - method: "action_receipt_to_customer", - args: [[order_server_id], orderName, orderPartner, ticketImage], - }); + } + get orderAmountPlusTip() { + const order = this.currentOrder; + const orderTotalAmount = order.get_total_with_tax(); + const tip_product_id = + this.env.pos.config.tip_product_id && this.env.pos.config.tip_product_id[0]; + const tipLine = order + .get_orderlines() + .find((line) => tip_product_id && line.product.id === tip_product_id); + const tipAmount = tipLine ? tipLine.get_all_prices().priceWithTax : 0; + const orderAmountStr = this.env.pos.format_currency(orderTotalAmount - tipAmount); + if (!tipAmount) { + return orderAmountStr; + } + const tipAmountStr = this.env.pos.format_currency(tipAmount); + return `${orderAmountStr} + ${tipAmountStr} tip`; + } + get currentOrder() { + return this.env.pos.get_order(); + } + get nextScreen() { + return { name: "ProductScreen" }; + } + whenClosing() { + this.orderDone(); + } + /** + * This function is called outside the rendering call stack. This way, + * we don't block the displaying of ReceiptScreen when it is mounted; additionally, + * any error that can happen during the printing does not affect the rendering. + */ + async handleAutoPrint() { + if (this._shouldAutoPrint()) { + await this.printReceipt(); + if (this.currentOrder._printed && this._shouldCloseImmediately()) { + this.whenClosing(); + } } } - ReceiptScreen.template = "ReceiptScreen"; - return ReceiptScreen; -}; + orderDone() { + this.env.pos.removeOrder(this.currentOrder); + this._addNewOrder(); + const { name, props } = this.nextScreen; + this.showScreen(name, props); + if (this.env.pos.config.iface_customer_facing_display) { + this.env.pos.send_current_order_to_customer_facing_display(); + } + } + async printReceipt() { + const isPrinted = await this._printReceipt(); + if (isPrinted) { + this.currentOrder._printed = true; + } + } + _shouldAutoPrint() { + return this.env.pos.config.iface_print_auto && !this.currentOrder._printed; + } + _shouldCloseImmediately() { + var invoiced_finalized = this.currentOrder.is_to_invoice() + ? this.currentOrder.finalized + : true; + return ( + this.env.proxy.printer && + this.env.pos.config.iface_print_skip_screen && + invoiced_finalized + ); + } + async _sendReceiptToCustomer() { + const printer = new Printer(null, this.env.pos); + const receiptString = this.orderReceipt.el.innerHTML; + const ticketImage = await printer.htmlToImg(receiptString); + const order = this.currentOrder; + const partner = order.get_partner(); + const orderName = order.get_name(); + const orderPartner = { + email: this.orderUiState.inputEmail, + name: partner ? partner.name : this.orderUiState.inputEmail, + }; + const order_server_id = this.env.pos.validated_orders_name_server_id_map[orderName]; + await this.rpc({ + model: "pos.order", + method: "action_receipt_to_customer", + args: [[order_server_id], orderName, orderPartner, ticketImage], + }); + } +} -Registries.Component.addByExtending(ReceiptScreen, AbstractReceiptScreen); - -export default ReceiptScreen; +registry.category("pos_screens").add("ReceiptScreen", ReceiptScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/WrappedProductNameLines.js b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/WrappedProductNameLines.js index 282954aefd0..f41d09c9d9e 100644 --- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/WrappedProductNameLines.js +++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/WrappedProductNameLines.js @@ -1,11 +1,7 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; -class WrappedProductNameLines extends PosComponent {} -WrappedProductNameLines.template = "WrappedProductNameLines"; - -Registries.Component.add(WrappedProductNameLines); - -export default WrappedProductNameLines; +export class WrappedProductNameLines extends PosComponent { + static template = "WrappedProductNameLines"; +} 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 78b57cb11f4..4404522ecb0 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,12 +1,14 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { round_precision as round_pr } from "web.utils"; -import Registries from "@point_of_sale/js/Registries"; +import { registry } from "@web/core/registry"; const { onMounted, onWillUnmount, useExternalListener, useState } = owl; -class ScaleScreen extends PosComponent { +export class ScaleScreen extends PosComponent { + static template = "ScaleScreen"; + /** * @param {Object} props * @param {Object} props.product The product to weight. @@ -95,8 +97,5 @@ class ScaleScreen extends PosComponent { : ""; } } -ScaleScreen.template = "ScaleScreen"; -Registries.Component.add(ScaleScreen); - -export default ScaleScreen; +registry.category("pos_screens").add("ScaleScreen", ScaleScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js index 0fa138f85a0..611cdb78e5d 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js @@ -1,10 +1,13 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; + +export class InvoiceButton extends PosComponent { + static template = "InvoiceButton"; -class InvoiceButton extends PosComponent { setup() { super.setup(); useListener("click", this._onClick); @@ -42,7 +45,7 @@ class InvoiceButton extends PosComponent { throw error; } else { // NOTE: error here is most probably undefined - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Network Error"), body: this.env._t("Unable to download invoice."), }); @@ -66,7 +69,7 @@ class InvoiceButton extends PosComponent { // Part 1: Handle missing partner. // Write to pos.order the selected partner. if (!order.get_partner()) { - const { confirmed: confirmedPopup } = await this.showPopup("ConfirmPopup", { + const { confirmed: confirmedPopup } = await this.showPopup(ConfirmPopup, { title: this.env._t("Need customer to invoice"), body: this.env._t("Do you want to open the customer list to select customer?"), }); @@ -115,7 +118,3 @@ class InvoiceButton extends PosComponent { } } } -InvoiceButton.template = "InvoiceButton"; -Registries.Component.add(InvoiceButton); - -export default InvoiceButton; diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/ReprintReceiptButton.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/ReprintReceiptButton.js index 6eb5d8bc97c..b5f07fa523c 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/ReprintReceiptButton.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/ReprintReceiptButton.js @@ -1,10 +1,11 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class ReprintReceiptButton extends PosComponent { + static template = "ReprintReceiptButton"; -class ReprintReceiptButton extends PosComponent { setup() { super.setup(); useListener("click", this._onClick); @@ -16,7 +17,3 @@ class ReprintReceiptButton extends PosComponent { this.showScreen("ReprintReceiptScreen", { order: this.props.order }); } } -ReprintReceiptButton.template = "ReprintReceiptButton"; -Registries.Component.add(ReprintReceiptButton); - -export default ReprintReceiptButton; diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderDetails.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderDetails.js index ffabdb098c8..be6e3b219d7 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderDetails.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderDetails.js @@ -1,12 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { OrderlineDetails } from "./OrderlineDetails"; /** * @props {models.Order} order */ -class OrderDetails extends PosComponent { +export class OrderDetails extends PosComponent { + static components = { OrderlineDetails }; + static template = "OrderDetails"; + get order() { return this.props.order; } @@ -20,8 +23,3 @@ class OrderDetails extends PosComponent { return this.env.pos.format_currency(this.order ? this.order.get_total_tax() : 0); } } -OrderDetails.template = "OrderDetails"; - -Registries.Component.add(OrderDetails); - -export default OrderDetails; 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 b4de4db9c77..9db44a405ff 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 @@ -1,14 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { format } from "web.field_utils"; import { round_precision as round_pr } from "web.utils"; /** * @props {pos.order.line} line */ -class OrderlineDetails extends PosComponent { +export class OrderlineDetails extends PosComponent { + static template = "OrderlineDetails"; + get line() { const line = this.props.line; const formatQty = (line) => { @@ -72,8 +73,3 @@ class OrderlineDetails extends PosComponent { return _.str.sprintf(this.env._t("To Refund: %s"), this.getFormattedToRefundQty()); } } -OrderlineDetails.template = "OrderlineDetails"; - -Registries.Component.add(OrderlineDetails); - -export default OrderlineDetails; diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js index 22b5050b08d..98639465775 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js @@ -1,35 +1,33 @@ /** @odoo-module */ -import AbstractReceiptScreen from "@point_of_sale/js/Misc/AbstractReceiptScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractReceiptScreen } from "@point_of_sale/js/Misc/AbstractReceiptScreen"; +import { registry } from "@web/core/registry"; +import { OrderReceipt } from "../ReceiptScreen/OrderReceipt"; -const ReprintReceiptScreen = (AbstractReceiptScreen) => { - class ReprintReceiptScreen extends AbstractReceiptScreen { - setup() { - super.setup(); - owl.onMounted(this.onMounted); - } - onMounted() { - this.printReceipt(); - } - confirm() { - this.showScreen("TicketScreen", { reuseSavedUIState: true }); - } - async printReceipt() { - if (this.env.proxy.printer && this.env.pos.config.iface_print_skip_screen) { - const result = await this._printReceipt(); - if (result) { - this.showScreen("TicketScreen", { reuseSavedUIState: true }); - } +export class ReprintReceiptScreen extends AbstractReceiptScreen { + static template = "ReprintReceiptScreen"; + static components = { OrderReceipt }; + setup() { + super.setup(); + owl.onMounted(this.onMounted); + } + onMounted() { + this.printReceipt(); + } + confirm() { + this.showScreen("TicketScreen", { reuseSavedUIState: true }); + } + async printReceipt() { + if (this.env.proxy.printer && this.env.pos.config.iface_print_skip_screen) { + const result = await this._printReceipt(); + if (result) { + this.showScreen("TicketScreen", { reuseSavedUIState: true }); } } - async tryReprint() { - await this._printReceipt(); - } } - ReprintReceiptScreen.template = "ReprintReceiptScreen"; - return ReprintReceiptScreen; -}; -Registries.Component.addByExtending(ReprintReceiptScreen, AbstractReceiptScreen); + async tryReprint() { + await this._printReceipt(); + } +} -export default ReprintReceiptScreen; +registry.category("pos_screens").add("ReprintReceiptScreen", ReprintReceiptScreen); 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 cbec4b7221d..3374a488dda 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 @@ -1,15 +1,44 @@ /** @odoo-module */ import { Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; -import IndependentToOrderScreen from "@point_of_sale/js/Misc/IndependentToOrderScreen"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { IndependentToOrderScreen } from "@point_of_sale/js/Misc/IndependentToOrderScreen"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { registry } from "@web/core/registry"; import { useListener } from "@web/core/utils/hooks"; import { parse } from "web.field_utils"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; + +import { ActionpadWidget } from "../ProductScreen/ActionpadWidget"; +import { InvoiceButton } from "../TicketScreen/ControlButtons/InvoiceButton"; +import { NumpadWidget } from "../ProductScreen/NumpadWidget"; +import { OrderDetails } from "../TicketScreen/OrderDetails"; +import { ReprintReceiptButton } from "./ControlButtons/ReprintReceiptButton"; +import { SearchBar } from "../../Misc/SearchBar"; + const { onMounted, onWillUnmount, useState } = owl; -class TicketScreen extends IndependentToOrderScreen { +export class TicketScreen extends IndependentToOrderScreen { + static template = "TicketScreen"; + static components = { + ActionpadWidget, + InvoiceButton, + NumpadWidget, + OrderDetails, + ReprintReceiptButton, + SearchBar, + }; + static defaultProps = { + destinationOrder: null, + // When passed as true, it will use the saved _state.ui as default + // value when this component is reinstantiated. + // After setting the default value, the _state.ui will be overridden + // by the passed props.ui if there is any. + reuseSavedUIState: false, + ui: {}, + }; + setup() { super.setup(); useListener("close-screen", this._onCloseScreen); @@ -25,7 +54,7 @@ class TicketScreen extends IndependentToOrderScreen { useListener("click-refund-order-uid", this._onClickRefundOrderUid); useListener("update-selected-orderline", this._onUpdateSelectedOrderline); useListener("do-refund", this._onDoRefund); - NumberBuffer.use({ + numberBuffer.use({ nonKeyboardInputEvent: "numpad-click-input", triggerAtInput: "update-selected-orderline", }); @@ -89,7 +118,7 @@ class TicketScreen extends IndependentToOrderScreen { this._state.ui.selectedOrderlineIds[clickedOrder.backendId] = firstLine.id; } } - NumberBuffer.reset(); + numberBuffer.reset(); } else { this._setOrder(clickedOrder); } @@ -111,7 +140,7 @@ class TicketScreen extends IndependentToOrderScreen { ["ProductScreen", "PaymentScreen"].includes(screen.name) && order.get_orderlines().length > 0 ) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Existing orderlines"), body: _.str.sprintf( this.env._t( @@ -151,7 +180,7 @@ class TicketScreen extends IndependentToOrderScreen { _onClickOrderline({ detail: orderline }) { const order = this.getSelectedSyncedOrder(); this._state.ui.selectedOrderlineIds[order.backendId] = orderline.id; - NumberBuffer.reset(); + numberBuffer.reset(); } _onClickRefundOrderUid({ detail: orderUid }) { // Open the refund order. @@ -164,24 +193,24 @@ class TicketScreen extends IndependentToOrderScreen { const buffer = detail.buffer; const order = this.getSelectedSyncedOrder(); if (!order) { - return NumberBuffer.reset(); + return numberBuffer.reset(); } const selectedOrderlineId = this.getSelectedOrderlineId(); const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); if (!orderline) { - return NumberBuffer.reset(); + return numberBuffer.reset(); } const toRefundDetail = this._getToRefundDetail(orderline); // When already linked to an order, do not modify the to refund quantity. if (toRefundDetail.destinationOrderUid) { - return NumberBuffer.reset(); + return numberBuffer.reset(); } const refundableQty = toRefundDetail.orderline.qty - toRefundDetail.orderline.refundedQty; if (refundableQty <= 0) { - return NumberBuffer.reset(); + return numberBuffer.reset(); } if (buffer == null || buffer == "") { @@ -189,8 +218,8 @@ class TicketScreen extends IndependentToOrderScreen { } else { const quantity = Math.abs(parse.float(buffer)); if (quantity > refundableQty) { - NumberBuffer.reset(); - this.showPopup("ErrorPopup", { + numberBuffer.reset(); + this.showPopup(ErrorPopup, { title: this.env._t("Maximum Exceeded"), body: _.str.sprintf( this.env._t( @@ -665,7 +694,7 @@ class TicketScreen extends IndependentToOrderScreen { // Cache these fetched orders so that next time, no need to fetch // them again, unless invalidated. See `_onInvoiceOrder`. fetchedOrders.forEach((order) => { - this._state.syncedOrders.cache[order.id] = Order.create( + this._state.syncedOrders.cache[order.id] = new Order( {}, { pos: this.env.pos, json: order } ); @@ -687,17 +716,5 @@ class TicketScreen extends IndependentToOrderScreen { //#endregion //#endregion } -TicketScreen.template = "TicketScreen"; -TicketScreen.defaultProps = { - destinationOrder: null, - // When passed as true, it will use the saved _state.ui as default - // value when this component is reinstantiated. - // After setting the default value, the _state.ui will be overridden - // by the passed props.ui if there is any. - reuseSavedUIState: false, - ui: {}, -}; -Registries.Component.add(TicketScreen); - -export default TicketScreen; +registry.category("pos_screens").add("TicketScreen", TicketScreen); diff --git a/addons/point_of_sale/static/src/js/barcode_reader.js b/addons/point_of_sale/static/src/js/barcode_reader.js index 702473b3e05..46d6f4fa231 100644 --- a/addons/point_of_sale/static/src/js/barcode_reader.js +++ b/addons/point_of_sale/static/src/js/barcode_reader.js @@ -8,7 +8,7 @@ var Mutex = concurrency.Mutex; // is set-up to act like a keyboard. Use connect() and disconnect() to activate // and deactivate the barcode reader. Use set_action_callbacks to tell it // what to do when it reads a barcode. -var BarcodeReader = core.Class.extend({ +export const BarcodeReader = core.Class.extend({ actions: ["product", "cashier", "client"], init: function (attributes) { @@ -163,5 +163,3 @@ var BarcodeReader = core.Class.extend({ this.remote_scanning = false; }, }); - -export default BarcodeReader; diff --git a/addons/point_of_sale/static/src/js/custom_hooks.js b/addons/point_of_sale/static/src/js/custom_hooks.js index a62686e291a..f054e8ee61f 100644 --- a/addons/point_of_sale/static/src/js/custom_hooks.js +++ b/addons/point_of_sale/static/src/js/custom_hooks.js @@ -1,4 +1,9 @@ /** @odoo-module */ +import { OfflineErrorPopup } from "./Popups/OfflineErrorPopup"; +import { ConfirmPopup } from "./Popups/ConfirmPopup"; +import { ErrorTracebackPopup } from "./Popups/ErrorTracebackPopup"; +import { ErrorPopup } from "./Popups/ErrorPopup"; + const { onMounted, onPatched, onWillUnmount, useComponent } = owl; /** @@ -13,7 +18,7 @@ export function useErrorHandlers() { component._handlePushOrderError = async function (error) { // This error handler receives `error` equivalent to `error.message` of the rpc error. if (error.message === "Backend Invoice") { - await this.showPopup("ConfirmPopup", { + await this.showPopup(ConfirmPopup, { title: this.env._t("Please print the invoice from the backend"), body: this.env._t( @@ -26,10 +31,10 @@ export function useErrorHandlers() { const body = this.env._t( "Check the internet connection then try to sync again by clicking on the red wifi button (upper right of the screen)." ); - await this.showPopup("OfflineErrorPopup", { title, body }); + await this.showPopup(OfflineErrorPopup, { title, body }); } else if (error.code === 200) { // OpenERP Server Errors - await this.showPopup("ErrorTracebackPopup", { + await this.showPopup(ErrorTracebackPopup, { title: error.data.message || this.env._t("Server Error"), body: error.data.debug || @@ -37,7 +42,7 @@ export function useErrorHandlers() { }); } else if (error.code === 700) { // Sweden Fiscal module errors - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Fiscal data module error"), body: error.data.error.status || @@ -53,13 +58,13 @@ export function useErrorHandlers() { } else { bodyMessage = "Fiscal data module is not on."; } - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Fiscal data module error"), body: bodyMessage, }); } else { // ??? - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Unknown Error"), body: this.env._t( "The order could not be sent to the server due to an unknown error" diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index e685715283f..16b78c46ef1 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -18,7 +18,7 @@ import utils from "web.utils"; */ const CACHE = {}; -var PosDB = core.Class.extend({ +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) { @@ -583,5 +583,3 @@ var PosDB = core.Class.extend({ this.save("unpaid_orders_to_remove", to_remove); }, }); - -export default PosDB; diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 857db75e6a7..377df34c6b4 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1,7 +1,7 @@ /** @odoo-module */ /* global waitForWebfonts */ -import PosDB from "@point_of_sale/js/db"; +import { PosDB } from "@point_of_sale/js/db"; import config from "web.config"; import core from "web.core"; import field_utils from "web.field_utils"; @@ -9,13 +9,13 @@ import time from "web.time"; import utils from "web.utils"; import { Gui } from "@point_of_sale/js/Gui"; import { batched, uuidv4 } from "@point_of_sale/js/utils"; +import { ErrorPopup } from "./Popups/ErrorPopup"; var QWeb = core.qweb; var _t = core._t; var round_di = utils.round_decimals; var round_pr = utils.round_precision; -import Registries from "@point_of_sale/js/Registries"; const { markRaw, reactive } = owl; // Container of the product images fetched during rendering @@ -60,7 +60,11 @@ class PosModel { * don't want this default calculation of cid. * @param {Object?} defaultObj its props copied to this instance. */ - constructor(defaultObj) { + constructor() { + this.setup(...arguments); + } + // To be used by Model patches to patch constructor + setup(defaultObj) { defaultObj = defaultObj || {}; if (!defaultObj.cid) { defaultObj.cid = this._getCID(defaultObj); @@ -84,8 +88,8 @@ class PosModel { } export class PosGlobalState extends PosModel { - constructor(obj) { - super(obj); + setup() { + 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 @@ -240,7 +244,7 @@ export class PosGlobalState extends PosModel { productTemplateMap[product.product_tmpl_id[0]] = ( productTemplateMap[product.product_tmpl_id[0]] || [] ).concat(product); - return Product.create(product); + return new Product(product); }); for (const pricelist of this.pricelists) { @@ -405,7 +409,7 @@ export class PosGlobalState extends PosModel { if (json) { options.json = json; } - return this.makeOrderReactive(Order.create({}, options)); + return this.makeOrderReactive(new Order({}, options)); } makeOrderReactive(order) { const batchedCallback = batched(() => { @@ -1425,7 +1429,6 @@ export class PosGlobalState extends PosModel { } } PosGlobalState.prototype.electronic_payment_interfaces = {}; -Registries.Model.add(PosGlobalState); /** * Call this function to map your PaymentInterface implementation to @@ -1497,7 +1500,7 @@ export class Product extends PosModel { var pricelist_items = []; if (pricelist) { - var pricelist_items = _.filter( + pricelist_items = _.filter( self.applicablePricelistItems[pricelist.id], function (item) { return ( @@ -1578,7 +1581,6 @@ export class Product extends PosModel { } } } -Registries.Model.add(Product); var orderline_id = 1; @@ -1586,8 +1588,8 @@ var orderline_id = 1; // An orderline contains a product, its quantity, its price, discount. etc. // An Order contains zero or more Orderlines. export class Orderline extends PosModel { - constructor(obj, options) { - super(obj); + setup(_defaultObj, options) { + super.setup(...arguments); this.pos = options.pos; this.order = options.order; this.price_manually_set = options.price_manually_set || false; @@ -1637,7 +1639,7 @@ export class Orderline extends PosModel { var pack_lot_lines = json.pack_lot_ids; for (var i = 0; i < pack_lot_lines.length; i++) { var packlotline = pack_lot_lines[i][2]; - var pack_lot_line = Packlotline.create( + var pack_lot_line = new Packlotline( {}, { json: _.extend({...packlotline}, { order_line: this }) } ); @@ -1649,7 +1651,7 @@ export class Orderline extends PosModel { this.refunded_orderline_id = json.refunded_orderline_id; } clone() { - var orderline = Orderline.create( + var orderline = new Orderline( {}, { pos: this.pos, @@ -1712,7 +1714,7 @@ export class Orderline extends PosModel { // Create new pack lot lines. let newPackLotLine; for (const newLotLine of newPackLotLines) { - newPackLotLine = Packlotline.create({}, { order_line: this }); + newPackLotLine = new Packlotline({}, { order_line: this }); newPackLotLine.lot_name = newLotLine.lot_name; this.pack_lot_lines.add(newPackLotLine); } @@ -1779,7 +1781,7 @@ export class Orderline extends PosModel { const maxQtyToRefund = toRefundDetail.orderline.qty - toRefundDetail.orderline.refundedQty; if (quant > 0) { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Positive quantity not allowed"), body: _t( "Only a negative quantity is allowed for this refund line. Click on +/- to modify the quantity to be refunded." @@ -1791,7 +1793,7 @@ export class Orderline extends PosModel { } else if (-quant <= maxQtyToRefund) { toRefundDetail.qty = -quant; } else { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Greater than allowed"), body: _.str.sprintf( _t( @@ -2308,11 +2310,10 @@ export class Orderline extends PosModel { return this.product.standard_price * this.quantity; } } -Registries.Model.add(Orderline); export class Packlotline extends PosModel { - constructor(obj, options) { - super(obj); + setup(_defaultObj, options) { + super.setup(...arguments); this.lot_name = null; this.order_line = options.order_line; if (options.json) { @@ -2340,12 +2341,11 @@ export class Packlotline extends PosModel { }; } } -Registries.Model.add(Packlotline); // Every Paymentline contains a cashregister and an amount of money. export class Payment extends PosModel { - constructor(obj, options) { - super(obj); + setup(obj, options) { + super.setup(...arguments); this.pos = options.pos; this.order = options.order; this.amount = 0; @@ -2475,15 +2475,14 @@ export class Payment extends PosModel { return Boolean(this.get_payment_status()); } } -Registries.Model.add(Payment); // An order more or less represents the content of a customer's shopping cart (the OrderLines) // plus the associated payment information (the Paymentlines) // there is always an active ('selected') order in the Pos, a new one is created // automaticaly once an order is completed and sent to the server. export class Order extends PosModel { - constructor(obj, options) { - super(obj); + setup(_defaultObj, options) { + super.setup(...arguments); var self = this; options = options || {}; @@ -2606,7 +2605,7 @@ export class Order extends PosModel { var orderline = orderlines[i][2]; if (this.pos.db.get_product_by_id(orderline.product_id)) { this.add_orderline( - Orderline.create({}, { pos: this.pos, order: this, json: orderline }) + new Orderline({}, { pos: this.pos, order: this, json: orderline }) ); } } @@ -2614,7 +2613,7 @@ export class Order extends PosModel { var paymentlines = json.statement_ids; for (i = 0; i < paymentlines.length; i++) { var paymentline = paymentlines[i][2]; - var newpaymentline = Payment.create( + var newpaymentline = new Payment( {}, { pos: this.pos, order: this, json: paymentline } ); @@ -2990,7 +2989,7 @@ export class Order extends PosModel { } this.assert_editable(); options = options || {}; - var line = Orderline.create({}, { pos: this.pos, order: this, product: product }); + var line = new Orderline({}, { pos: this.pos, order: this, product: product }); this.fix_tax_included_price(line); this.set_orderline_options(line, options); @@ -3097,7 +3096,7 @@ export class Order extends PosModel { if (this.electronic_payment_in_progress()) { return false; } else { - var newPaymentline = Payment.create( + var newPaymentline = new Payment( {}, { order: this, payment_method: payment_method, pos: this.pos } ); @@ -3617,4 +3616,3 @@ export class Order extends PosModel { } } } -Registries.Model.add(Order); diff --git a/addons/point_of_sale/static/src/js/payment.js b/addons/point_of_sale/static/src/js/payment.js index 8e5724184f7..7c4bee6da3d 100644 --- a/addons/point_of_sale/static/src/js/payment.js +++ b/addons/point_of_sale/static/src/js/payment.js @@ -5,7 +5,7 @@ import core from "web.core"; /** * Implement this interface to support a new payment method in the POS: * - import var PaymentInterface from "@point_of_sale/js/payment"; + import const PaymentInterface from "@point_of_sale/js/payment"; * var MyPayment = PaymentInterface.extend({ * ... * }) @@ -22,7 +22,7 @@ import core from "web.core"; * by overriding the loader_params of the models in the back end * in the `pos.session` model */ -var PaymentInterface = core.Class.extend({ +export const PaymentInterface = core.Class.extend({ init: function (pos, payment_method) { this.pos = pos; this.payment_method = payment_method; @@ -89,5 +89,3 @@ var PaymentInterface = core.Class.extend({ */ close: function () {}, }); - -export default PaymentInterface; diff --git a/addons/point_of_sale/static/src/js/pos_env.js b/addons/point_of_sale/static/src/js/pos_env.js index da033984cf4..87a3e34ea11 100644 --- a/addons/point_of_sale/static/src/js/pos_env.js +++ b/addons/point_of_sale/static/src/js/pos_env.js @@ -6,16 +6,14 @@ import env from "web.env"; import concurrency from "web.concurrency"; import { JobQueue, ProxyDevice } from "@point_of_sale/js/devices"; -import BarcodeReader from "@point_of_sale/js/barcode_reader"; +import { BarcodeReader } from "@point_of_sale/js/barcode_reader"; // Create new env object base on web.env. // Mutating this new object won't affect the original object. -const pos_env = Object.create(env); +export const pos_env = Object.create(env); pos_env.proxy_queue = new JobQueue(); // used to prevent parallels communications to the proxy pos_env.proxy = new ProxyDevice({ env: pos_env }); // used to communicate to the hardware devices via a local proxy pos_env.barcode_reader = new BarcodeReader({ env: pos_env, proxy: pos_env.proxy }); pos_env.posbus = new owl.EventBus(); pos_env.posMutex = new concurrency.Mutex(); - -export default pos_env; diff --git a/addons/point_of_sale/static/src/js/printers.js b/addons/point_of_sale/static/src/js/printers.js index 7eb0eb6ff69..cc1ae86fddf 100644 --- a/addons/point_of_sale/static/src/js/printers.js +++ b/addons/point_of_sale/static/src/js/printers.js @@ -4,6 +4,7 @@ import Session from "web.Session"; import core from "web.core"; import { Gui } from "@point_of_sale/js/Gui"; +import { ErrorPopup } from "./Popups/ErrorPopup"; var _t = core._t; // IMPROVEMENT: This is too much. We can get away from this class. @@ -114,7 +115,7 @@ export const PrinterMixin = { _onIoTActionResult: function (data) { if (this.pos && (data === false || data.result === false)) { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Connection to the printer failed"), body: _t("Please check if the printer is still connected."), }); @@ -123,7 +124,7 @@ export const PrinterMixin = { _onIoTActionFail: function () { if (this.pos) { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Connection to IoT Box failed"), body: _t("Please check if the IoT Box is still connected."), }); diff --git a/addons/point_of_sale/static/src/xml/Popups/PosPopupController.xml b/addons/point_of_sale/static/src/xml/Popups/PosPopupController.xml index 78392b7ef8f..acd752853ff 100644 --- a/addons/point_of_sale/static/src/xml/Popups/PosPopupController.xml +++ b/addons/point_of_sale/static/src/xml/Popups/PosPopupController.xml @@ -3,9 +3,9 @@
- +
diff --git a/addons/point_of_sale/static/tests/unit/helpers/test_env.js b/addons/point_of_sale/static/tests/unit/helpers/test_env.js index 58520aa13f2..da172119eaa 100644 --- a/addons/point_of_sale/static/tests/unit/helpers/test_env.js +++ b/addons/point_of_sale/static/tests/unit/helpers/test_env.js @@ -8,7 +8,7 @@ */ import makeTestEnvironment from "web.test_env"; -import env from "@point_of_sale/js/pos_env"; +import { pos_env as env} from "@point_of_sale/js/pos_env"; import { PosGlobalState } from "@point_of_sale/js/models"; // eslint-disable-next-line no-undef @@ -18,9 +18,9 @@ const cleanup = require("@web/../tests/helpers/cleanup"); // sensitive to load order. STOP DOING THIS ASAP. cleanup.registerCleanup = () => {}; -const makePosTestEnv = env.session.is_bound +export const makePosTestEnv = env.session.is_bound .then(() => { - const pos = PosGlobalState.create({ env }); + const pos = new PosGlobalState({ env }); return pos.load_server_data().then(() => pos); }) .then((pos) => { @@ -34,5 +34,3 @@ const makePosTestEnv = env.session.is_bound return makeTestEnvironment(env, providedRPC); }; }); - -export default makePosTestEnv; diff --git a/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js b/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js deleted file mode 100644 index 33029faceee..00000000000 --- a/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js +++ /dev/null @@ -1,412 +0,0 @@ -/** @odoo-module */ - -import Registries from "@point_of_sale/js/Registries"; - -QUnit.module("unit tests for ComponentRegistry", { - before() {}, -}); - -QUnit.test("basic extend", async function (assert) { - assert.expect(5); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - Registries.Component.freeze(); - - const RegA = Registries.Component.get(A); - const a = new RegA(); - assert.verifySteps(["A", "A1"]); - assert.ok(a instanceof RegA); - assert.ok(RegA.name === "A"); -}); - -QUnit.test("addByExtending", async function (assert) { - assert.expect(8); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const B = (x) => - class extends x { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - const A2 = (x) => - class extends x { - constructor() { - super(); - assert.step("A2"); - } - }; - Registries.Component.extend(A, A2); - - Registries.Component.freeze(); - - const RegA = Registries.Component.get(A); - const RegB = Registries.Component.get(B); - const b = new RegB(); - assert.verifySteps(["A", "A1", "A2", "B"]); - assert.ok(b instanceof RegA); - assert.ok(b instanceof RegB); - assert.ok(RegB.name === "B"); -}); - -QUnit.test("extend the one that is added by extending", async function (assert) { - assert.expect(6); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const B = (x) => - class extends x { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const B1 = (x) => - class extends x { - constructor() { - super(); - assert.step("B1"); - } - }; - Registries.Component.extend(B, B1); - - const B2 = (x) => - class extends x { - constructor() { - super(); - assert.step("B2"); - } - }; - Registries.Component.extend(B, B2); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - Registries.Component.freeze(); - - const RegB = Registries.Component.get(B); - new RegB(); - assert.verifySteps(["A", "A1", "B", "B1", "B2"]); -}); - -QUnit.test("addByExtending based on added by extending", async function (assert) { - assert.expect(10); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const B = (x) => - class extends x { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - const C = (x) => - class extends x { - constructor() { - super(); - assert.step("C"); - } - }; - Registries.Component.addByExtending(C, B); - - const B7 = (x) => - class extends x { - constructor() { - super(); - assert.step("B7"); - } - }; - Registries.Component.extend(B, B7); - - Registries.Component.freeze(); - - const RegA = Registries.Component.get(A); - const RegB = Registries.Component.get(B); - const RegC = Registries.Component.get(C); - const c = new RegC(); - assert.verifySteps(["A", "A1", "B", "B7", "C"]); - assert.ok(c instanceof RegA); - assert.ok(c instanceof RegB); - assert.ok(c instanceof RegC); - assert.ok(RegC.name === "C"); -}); - -QUnit.test("deeper inheritance", async function (assert) { - assert.expect(9); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const B = (x) => - class extends x { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - const C = (x) => - class extends x { - constructor() { - super(); - assert.step("C"); - } - }; - Registries.Component.addByExtending(C, B); - - const B2 = (x) => - class extends x { - constructor() { - super(); - assert.step("B2"); - } - }; - Registries.Component.extend(B, B2); - - const B3 = (x) => - class extends x { - constructor() { - super(); - assert.step("B3"); - } - }; - Registries.Component.extend(B, B3); - - const A9 = (x) => - class extends x { - constructor() { - super(); - assert.step("A9"); - } - }; - Registries.Component.extend(A, A9); - - const E = (x) => - class extends x { - constructor() { - super(); - assert.step("E"); - } - }; - Registries.Component.addByExtending(E, C); - - Registries.Component.freeze(); - - // |A| => A9 -> A1 -> A - // |B| => B3 -> B2 -> B -> |A| - // |C| => C -> |B| - // |E| => E -> |C| - - new (Registries.Component.get(E))(); - assert.verifySteps(["A", "A1", "A9", "B", "B2", "B3", "C", "E"]); -}); - -QUnit.test("mixins?", async function (assert) { - assert.expect(12); - - let A = class A { - constructor() { - assert.step("A"); - } - }; - Registries.Component.add(A); - - const Mixin = (x) => - class extends x { - constructor() { - super(); - assert.step("Mixin"); - } - mixinMethod() { - return "mixinMethod"; - } - get mixinGetter() { - return "mixinGetter"; - } - }; - - // use the mixin when declaring B. - let B = (x) => - class extends Mixin(x) { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - Registries.Component.freeze(); - - B = Registries.Component.get(B); - const b = new B(); - assert.verifySteps(["A", "A1", "Mixin", "B"]); - // instance of B should have the mixin properties - assert.strictEqual(b.mixinMethod(), "mixinMethod"); - assert.strictEqual(b.mixinGetter, "mixinGetter"); - - // instance of A should not have the mixin properties - A = Registries.Component.get(A); - const a = new A(); - assert.verifySteps(["A", "A1"]); - assert.notOk(a.mixinMethod); - assert.notOk(a.mixinGetter); -}); - -QUnit.test("extending methods", async function (assert) { - assert.expect(16); - - let A = class A { - foo() { - assert.step("A foo"); - } - }; - Registries.Component.add(A); - - let B = (x) => - class extends x { - bar() { - assert.step("B bar"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - bar() { - assert.step("A1 bar"); - // should only be for A. - } - }; - Registries.Component.extend(A, A1); - - const B1 = (x) => - class extends x { - foo() { - super.foo(); - assert.step("B1 foo"); - } - }; - Registries.Component.extend(B, B1); - - let C = (x) => - class extends x { - foo() { - super.foo(); - assert.step("C foo"); - } - bar() { - super.bar(); - assert.step("C bar"); - } - }; - Registries.Component.addByExtending(C, B); - - Registries.Component.freeze(); - - A = Registries.Component.get(A); - B = Registries.Component.get(B); - C = Registries.Component.get(C); - const a = new A(); - const b = new B(); - const c = new C(); - - a.foo(); - assert.verifySteps(["A foo"]); - b.foo(); - assert.verifySteps(["A foo", "B1 foo"]); - c.foo(); - assert.verifySteps(["A foo", "B1 foo", "C foo"]); - - a.bar(); - assert.verifySteps(["A1 bar"]); - b.bar(); - assert.verifySteps(["B bar"]); - c.bar(); - assert.verifySteps(["B bar", "C bar"]); -}); diff --git a/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js b/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js index 07a2957b64b..7b137f2c019 100644 --- a/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js +++ b/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; import makeTestEnvironment from "web.test_env"; import testUtils from "web.test_utils"; import { mount } from "@web/../tests/helpers/utils"; @@ -20,15 +20,15 @@ QUnit.test("simple fast inputs with capture in between", async function (assert) class Root extends LegacyComponent { setup() { this.state = useState({ buffer: "" }); - NumberBuffer.activate(); - NumberBuffer.use({ + numberBuffer.activate(); + numberBuffer.use({ nonKeyboardInputEvent: "numpad-click-input", state: this.state, }); } resetBuffer() { - NumberBuffer.capture(); - NumberBuffer.reset(); + numberBuffer.capture(); + numberBuffer.reset(); } onClickOne() { this.trigger("numpad-click-input", { key: "1" }); diff --git a/addons/point_of_sale/static/tests/unit/test_PosPopupController.js b/addons/point_of_sale/static/tests/unit/test_PosPopupController.js index 9b9a34f069a..18ac4601d23 100644 --- a/addons/point_of_sale/static/tests/unit/test_PosPopupController.js +++ b/addons/point_of_sale/static/tests/unit/test_PosPopupController.js @@ -1,53 +1,47 @@ /** @odoo-module */ -import PosPopupController from "@point_of_sale/js/Popups/PosPopupController"; -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosPopupController } from "@point_of_sale/js/Popups/PosPopupController"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import makeTestEnvironment from "web.test_env"; import testUtils from "web.test_utils"; -import Registries from "@point_of_sale/js/Registries"; import { mount } from "@web/../tests/helpers/utils"; const { EventBus, useSubEnv, xml } = owl; -QUnit.module("unit tests for PosPopupController", { - before() { - Registries.Component.freeze(); +// Note that we are creating new popups here to decouple this test from the pos app. +class CustomPopup1 extends AbstractAwaitablePopup {} +CustomPopup1.template = xml/* html */ ` + +`; - // Note that we are creating new popups here to decouple this test from the pos app. - class CustomPopup1 extends AbstractAwaitablePopup {} - CustomPopup1.template = xml/* html */ ` - - `; +class CustomPopup2 extends AbstractAwaitablePopup {} +CustomPopup2.template = xml/* html */ ` + +`; - class CustomPopup2 extends AbstractAwaitablePopup {} - CustomPopup2.template = xml/* html */ ` - - `; - - PosPopupController.components = { CustomPopup1, CustomPopup2 }; - }, -}); +QUnit.module("unit tests for PosPopupController"); QUnit.test("allow multiple popups at the same time", async function (assert) { assert.expect(12); class Root extends PosComponent { + static components = { PosPopupController }; setup() { super.setup(); useSubEnv({ @@ -66,7 +60,7 @@ QUnit.test("allow multiple popups at the same time", async function (assert) { const root = await mount(Root, testUtils.prepareTarget()); // Check 1 popup - let popup1Promise = root.showPopup("CustomPopup1", {}); + let popup1Promise = root.showPopup(CustomPopup1, {}); await testUtils.nextTick(); assert.strictEqual(root.el.querySelectorAll(".popup").length, 1); testUtils.dom.click(root.el.querySelector(".modal-dialog .custom-popup-1 .confirm")); @@ -76,13 +70,13 @@ QUnit.test("allow multiple popups at the same time", async function (assert) { assert.strictEqual(root.el.querySelectorAll(".popup").length, 0); // Check multiple popups - popup1Promise = root.showPopup("CustomPopup1", {}); + popup1Promise = root.showPopup(CustomPopup1, {}); await testUtils.nextTick(); // Check if the first popup is shown. assert.strictEqual(root.el.querySelectorAll(".popup").length, 1); - const popup2Promise = root.showPopup("CustomPopup2", {}); + const popup2Promise = root.showPopup(CustomPopup2, {}); await testUtils.nextTick(); // Check for the second popup. @@ -100,8 +94,8 @@ QUnit.test("allow multiple popups at the same time", async function (assert) { assert.strictEqual(root.el.querySelectorAll(".modal-dialog .custom-popup-2").length, 0); // popup 1 should not be hidden - const CustomPopup1 = root.el.querySelector(".modal-dialog"); - assert.strictEqual(![...CustomPopup1.classList].includes("oe_hidden"), true); + const customPopup1 = root.el.querySelector(".modal-dialog"); + assert.strictEqual(![...customPopup1.classList].includes("oe_hidden"), true); testUtils.dom.click(root.el.querySelector(".modal-dialog .custom-popup-1 .cancel")); await testUtils.nextTick(); @@ -118,6 +112,7 @@ QUnit.test("pressing cancel/confirm key should only close the top popup", async assert.expect(6); class Root extends PosComponent { + static components = { PosPopupController }; setup() { super.setup(); useSubEnv({ @@ -135,14 +130,14 @@ QUnit.test("pressing cancel/confirm key should only close the top popup", async const root = await mount(Root, testUtils.prepareTarget()); - const popup1Promise = root.showPopup("CustomPopup1", { + const popup1Promise = root.showPopup(CustomPopup1, { confirmKey: "Enter", cancelKey: "Escape", }); await testUtils.nextTick(); assert.strictEqual(root.el.querySelectorAll(".popup").length, 1); - const popup2Promise = root.showPopup("CustomPopup2", { + const popup2Promise = root.showPopup(CustomPopup2, { confirmKey: "Enter", cancelKey: "Escape", }); diff --git a/addons/pos_adyen/static/src/js/PaymentScreen.js b/addons/pos_adyen/static/src/js/PaymentScreen.js index b9bd2beed20..77bdd0ddb93 100644 --- a/addons/pos_adyen/static/src/js/PaymentScreen.js +++ b/addons/pos_adyen/static/src/js/PaymentScreen.js @@ -1,39 +1,34 @@ /** @odoo-module */ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; const { onMounted } = owl; -const PosAdyenPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - setup() { - super.setup(); - onMounted(() => { - const pendingPaymentLine = this.currentOrder.paymentlines.find( - (paymentLine) => - paymentLine.payment_method.use_payment_terminal === "adyen" && - !paymentLine.is_done() && - paymentLine.get_payment_status() !== "pending" +patch(PaymentScreen.prototype, "pos_adyen.PaymentScreen", { + setup() { + this._super(...arguments); + onMounted(() => { + const pendingPaymentLine = this.currentOrder.paymentlines.find( + (paymentLine) => + paymentLine.payment_method.use_payment_terminal === "adyen" && + !paymentLine.is_done() && + paymentLine.get_payment_status() !== "pending" + ); + if (pendingPaymentLine) { + const paymentTerminal = pendingPaymentLine.payment_method.payment_terminal; + paymentTerminal.set_most_recent_service_id( + pendingPaymentLine.terminalServiceId ); - if (pendingPaymentLine) { - const paymentTerminal = pendingPaymentLine.payment_method.payment_terminal; - paymentTerminal.set_most_recent_service_id( - pendingPaymentLine.terminalServiceId - ); - pendingPaymentLine.set_payment_status("waiting"); - paymentTerminal.start_get_status_polling().then((isPaymentSuccessful) => { - if (isPaymentSuccessful) { - pendingPaymentLine.set_payment_status("done"); - pendingPaymentLine.can_be_reversed = paymentTerminal.supports_reversals; - } else { - pendingPaymentLine.set_payment_status("retry"); - } - }); - } - }); - } - }; - -Registries.Component.extend(PaymentScreen, PosAdyenPaymentScreen); - -export default PaymentScreen; + pendingPaymentLine.set_payment_status("waiting"); + paymentTerminal.start_get_status_polling().then((isPaymentSuccessful) => { + if (isPaymentSuccessful) { + pendingPaymentLine.set_payment_status("done"); + pendingPaymentLine.can_be_reversed = paymentTerminal.supports_reversals; + } else { + pendingPaymentLine.set_payment_status("retry"); + } + }); + } + }); + }, +}); diff --git a/addons/pos_adyen/static/src/js/models.js b/addons/pos_adyen/static/src/js/models.js index 002a556af9c..f625742d560 100644 --- a/addons/pos_adyen/static/src/js/models.js +++ b/addons/pos_adyen/static/src/js/models.js @@ -1,29 +1,27 @@ /** @odoo-module */ import { register_payment_method, Payment } from "@point_of_sale/js/models"; -import PaymentAdyen from "@pos_adyen/js/payment_adyen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentAdyen } from "@pos_adyen/js/payment_adyen"; +import { patch } from "@web/core/utils/patch"; register_payment_method("adyen", PaymentAdyen); -const PosAdyenPayment = (Payment) => - class PosAdyenPayment extends Payment { - constructor(obj, options) { - super(...arguments); - this.terminalServiceId = this.terminalServiceId || null; - } - //@override - export_as_JSON() { - const json = super.export_as_JSON(...arguments); - json.terminal_service_id = this.terminalServiceId; - return json; - } - //@override - init_from_JSON(json) { - super.init_from_JSON(...arguments); - this.terminalServiceId = json.terminal_service_id; - } - setTerminalServiceId(id) { - this.terminalServiceId = id; - } - }; -Registries.Model.extend(Payment, PosAdyenPayment); +patch(Payment.prototype, "pos_adyen.Payment", { + setup() { + this._super(...arguments); + this.terminalServiceId = this.terminalServiceId || null; + }, + //@override + export_as_JSON() { + const json = this._super(...arguments); + json.terminal_service_id = this.terminalServiceId; + return json; + }, + //@override + init_from_JSON(json) { + this._super(...arguments); + this.terminalServiceId = json.terminal_service_id; + }, + setTerminalServiceId(id) { + this.terminalServiceId = id; + }, +}); diff --git a/addons/pos_adyen/static/src/js/payment_adyen.js b/addons/pos_adyen/static/src/js/payment_adyen.js index 2ff8fe53cbc..60f01d40691 100644 --- a/addons/pos_adyen/static/src/js/payment_adyen.js +++ b/addons/pos_adyen/static/src/js/payment_adyen.js @@ -2,12 +2,13 @@ import core from "web.core"; import rpc from "web.rpc"; -import PaymentInterface from "@point_of_sale/js/payment"; +import { PaymentInterface } from "@point_of_sale/js/payment"; import { Gui } from "@point_of_sale/js/Gui"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; var _t = core._t; -var PaymentAdyen = PaymentInterface.extend({ +export const PaymentAdyen = PaymentInterface.extend({ send_payment_request: function (cid) { this._super.apply(this, arguments); this._reset_state(); @@ -365,11 +366,9 @@ var PaymentAdyen = PaymentInterface.extend({ if (!title) { title = _t("Adyen Error"); } - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: title, body: msg, }); }, }); - -export default PaymentAdyen; diff --git a/addons/pos_discount/static/src/js/DiscountButton.js b/addons/pos_discount/static/src/js/DiscountButton.js index 6ed6aac3abd..ae8859bc6dd 100644 --- a/addons/pos_discount/static/src/js/DiscountButton.js +++ b/addons/pos_discount/static/src/js/DiscountButton.js @@ -1,18 +1,21 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; + +export class DiscountButton extends PosComponent { + static template = "DiscountButton"; -class DiscountButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); } async onClick() { var self = this; - const { confirmed, payload } = await this.showPopup("NumberPopup", { + const { confirmed, payload } = await this.showPopup(NumberPopup, { title: this.env._t("Discount Percentage"), startingValue: this.env.pos.config.discount_pc, isInputSelected: true, @@ -28,7 +31,7 @@ class DiscountButton extends PosComponent { var lines = order.get_orderlines(); var product = this.env.pos.db.get_product_by_id(this.env.pos.config.discount_product_id[0]); if (product === undefined) { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("No discount product found"), body: this.env._t( "The discount product seems misconfigured. Make sure it is flagged as 'Can be Sold' and 'Available in Point of Sale'." @@ -80,7 +83,6 @@ class DiscountButton extends PosComponent { } } } -DiscountButton.template = "DiscountButton"; ProductScreen.addControlButton({ component: DiscountButton, @@ -88,7 +90,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.module_pos_discount && this.env.pos.config.discount_product_id; }, }); - -Registries.Component.add(DiscountButton); - -export default DiscountButton; diff --git a/addons/pos_epson_printer/static/src/js/pos_epson_printer.js b/addons/pos_epson_printer/static/src/js/pos_epson_printer.js index 49a3ff2ad9f..aa916a9e3d4 100644 --- a/addons/pos_epson_printer/static/src/js/pos_epson_printer.js +++ b/addons/pos_epson_printer/static/src/js/pos_epson_printer.js @@ -1,18 +1,16 @@ /** @odoo-module */ import { PosGlobalState } from "@point_of_sale/js/models"; -import EpsonPrinter from "@pos_epson_printer/js/printers"; -import Registries from "@point_of_sale/js/Registries"; +import { EpsonPrinter } from "@pos_epson_printer/js/printers"; +import { patch } from "@web/core/utils/patch"; -const PosEpsonPosGlobalState = (PosGlobalState) => - class PosEpsonPosGlobalState extends PosGlobalState { - after_load_server_data() { - var self = this; - return super.after_load_server_data(...arguments).then(function () { - if (self.config.other_devices && self.config.epson_printer_ip) { - self.env.proxy.printer = new EpsonPrinter(self.config.epson_printer_ip, self); - } - }); - } - }; -Registries.Model.extend(PosGlobalState, PosEpsonPosGlobalState); +patch(PosGlobalState.prototype, "pos_epson_printer.PosGlobalState", { + after_load_server_data() { + var self = this; + return this._super(...arguments).then(function () { + if (self.config.other_devices && self.config.epson_printer_ip) { + self.env.proxy.printer = new EpsonPrinter(self.config.epson_printer_ip, self); + } + }); + }, +}); diff --git a/addons/pos_epson_printer/static/src/js/printers.js b/addons/pos_epson_printer/static/src/js/printers.js index b6da88b5982..43c86186874 100644 --- a/addons/pos_epson_printer/static/src/js/printers.js +++ b/addons/pos_epson_printer/static/src/js/printers.js @@ -49,7 +49,7 @@ class EpsonPrintResultGenerator extends PrintResultGenerator { } } -var EpsonPrinter = core.Class.extend(PrinterMixin, { +export const EpsonPrinter = core.Class.extend(PrinterMixin, { init(ip, pos) { PrinterMixin.init.call(this, pos); var url = window.location.protocol + "//" + ip; @@ -167,5 +167,3 @@ var EpsonPrinter = core.Class.extend(PrinterMixin, { return $(res).find("response").attr("success") === "true"; }, }); - -export default EpsonPrinter; diff --git a/addons/pos_epson_printer_restaurant/static/src/js/models.js b/addons/pos_epson_printer_restaurant/static/src/js/models.js index 911e03ddfdd..8e52d4ebdf2 100644 --- a/addons/pos_epson_printer_restaurant/static/src/js/models.js +++ b/addons/pos_epson_printer_restaurant/static/src/js/models.js @@ -1,22 +1,20 @@ /** @odoo-module */ import { PosGlobalState } from "@point_of_sale/js/models"; -import EpsonPrinter from "@pos_epson_printer/js/printers"; -import Registries from "@point_of_sale/js/Registries"; +import { EpsonPrinter } from "@pos_epson_printer/js/printers"; +import { patch } from "@web/core/utils/patch"; // The override of create_printer needs to happen after its declaration in // pos_restaurant. We need to make sure that this code is executed after the // models file in pos_restaurant. import "@pos_restaurant/js/models"; -const PosEpsonResPosGlobalState = (PosGlobalState) => - class PosEpsonResPosGlobalState extends PosGlobalState { - create_printer(config) { - if (config.printer_type === "epson_epos") { - return new EpsonPrinter(config.epson_printer_ip, this); - } else { - return super.create_printer(...arguments); - } +patch(PosGlobalState.prototype, "pos_epson_printer_restaurant.PosGlobalState", { + create_printer(config) { + if (config.printer_type === "epson_epos") { + return new EpsonPrinter(config.epson_printer_ip, this); + } else { + return this._super(...arguments); } - }; -Registries.Model.extend(PosGlobalState, PosEpsonResPosGlobalState); + }, +}); diff --git a/addons/pos_hr/static/src/app/navbar.js b/addons/pos_hr/static/src/app/navbar.js new file mode 100644 index 00000000000..045c5ce7535 --- /dev/null +++ b/addons/pos_hr/static/src/app/navbar.js @@ -0,0 +1,9 @@ +/** @odoo-module */ + +import { Navbar } from "@point_of_sale/app/navbar/navbar"; +import { HeaderLockButton } from "../js/HeaderLockButton"; +import { patch } from "@web/core/utils/patch"; + +patch(Navbar, "pos_hr.Navbar", { + components: { ...Navbar.components, HeaderLockButton }, +}); diff --git a/addons/pos_hr/static/src/js/CashierName.js b/addons/pos_hr/static/src/js/CashierName.js index 4b81d0b30a2..aa3a3f335b9 100644 --- a/addons/pos_hr/static/src/js/CashierName.js +++ b/addons/pos_hr/static/src/js/CashierName.js @@ -1,33 +1,29 @@ /** @odoo-module */ -import CashierName from "@point_of_sale/js/ChromeWidgets/CashierName"; -import Registries from "@point_of_sale/js/Registries"; -import SelectCashierMixin from "@pos_hr/js/SelectCashierMixin"; +import { CashierName } from "@point_of_sale/js/ChromeWidgets/CashierName"; +import { patch } from "@web/core/utils/patch"; +import { SelectCashierMixin } from "@pos_hr/js/SelectCashierMixin"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; -const PosHrCashierName = (CashierName) => - class extends SelectCashierMixin(CashierName) { - setup() { - super.setup(); - useBarcodeReader({ cashier: this.barcodeCashierAction }); +patch(CashierName.prototype, "pos_hr.CashierName", { + ...SelectCashierMixin, + setup() { + this._super(...arguments); + useBarcodeReader({ cashier: this.barcodeCashierAction }); + }, + //@Override + get avatar() { + if (this.env.pos.config.module_pos_hr) { + const cashier = this.env.pos.get_cashier(); + return `/web/image/hr.employee/${cashier.id}/avatar_128`; } - //@Override - get avatar() { - if (this.env.pos.config.module_pos_hr) { - const cashier = this.env.pos.get_cashier(); - return `/web/image/hr.employee/${cashier.id}/avatar_128`; - } - return super.avatar; + return this._super(); + }, + //@Override + get cssClass() { + if (this.env.pos.config.module_pos_hr) { + return { oe_status: true }; } - //@Override - get cssClass() { - if (this.env.pos.config.module_pos_hr) { - return { oe_status: true }; - } - return super.cssClass; - } - }; - -Registries.Component.extend(CashierName, PosHrCashierName); - -export default CashierName; + return this._super(); + }, +}); diff --git a/addons/pos_hr/static/src/js/Chrome.js b/addons/pos_hr/static/src/js/Chrome.js index 2fc07716279..05cc98c9d4d 100644 --- a/addons/pos_hr/static/src/js/Chrome.js +++ b/addons/pos_hr/static/src/js/Chrome.js @@ -1,30 +1,25 @@ /** @odoo-module */ -import Chrome from "@point_of_sale/js/Chrome"; -import Registries from "@point_of_sale/js/Registries"; +import { Chrome } from "@point_of_sale/js/Chrome"; +import { patch } from "@web/core/utils/patch"; -const PosHrChrome = (Chrome) => - class extends Chrome { - async start() { - await super.start(); - if (this.env.pos.config.module_pos_hr) { - this.showTempScreen("LoginScreen"); - } +patch(Chrome.prototype, "pos_hr.Chrome", { + async start() { + await this._super(...arguments); + if (this.env.pos.config.module_pos_hr) { + this.showTempScreen("LoginScreen"); } - get showCashMoveButton() { - return ( - super.showCashMoveButton && - (!this.env.pos.cashier || this.env.pos.cashier.role == "manager") - ); + }, + get showCashMoveButton() { + return ( + this._super() && + (!this.env.pos.cashier || this.env.pos.cashier.role == "manager") + ); + }, + shouldShowCashControl() { + if (this.env.pos.config.module_pos_hr) { + return this._super() && this.env.pos.hasLoggedIn; } - shouldShowCashControl() { - if (this.env.pos.config.module_pos_hr) { - return super.shouldShowCashControl() && this.env.pos.hasLoggedIn; - } - return super.shouldShowCashControl(); - } - }; - -Registries.Component.extend(Chrome, PosHrChrome); - -export default Chrome; + return this._super(); + }, +}); diff --git a/addons/pos_hr/static/src/js/HeaderLockButton.js b/addons/pos_hr/static/src/js/HeaderLockButton.js index 608dc155a72..3c50d19a5de 100644 --- a/addons/pos_hr/static/src/js/HeaderLockButton.js +++ b/addons/pos_hr/static/src/js/HeaderLockButton.js @@ -1,11 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { useState } = owl; -class HeaderLockButton extends PosComponent { +export class HeaderLockButton extends PosComponent { + static template = "HeaderLockButton"; + setup() { super.setup(); this.state = useState({ isUnlockIcon: true, title: "Unlocked" }); @@ -19,8 +20,3 @@ class HeaderLockButton extends PosComponent { this.state.title = isMouseOver ? "Lock" : "Unlocked"; } } -HeaderLockButton.template = "HeaderLockButton"; - -Registries.Component.add(HeaderLockButton); - -export default HeaderLockButton; diff --git a/addons/pos_hr/static/src/js/LoginScreen.js b/addons/pos_hr/static/src/js/LoginScreen.js index 4dd9dda43b5..08f6e694a31 100644 --- a/addons/pos_hr/static/src/js/LoginScreen.js +++ b/addons/pos_hr/static/src/js/LoginScreen.js @@ -1,41 +1,44 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; -import SelectCashierMixin from "@pos_hr/js/SelectCashierMixin"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { SelectCashierMixin } from "@pos_hr/js/SelectCashierMixin"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; +import { registry } from "@web/core/registry"; +import { patch } from "@web/core/utils/patch"; -class LoginScreen extends SelectCashierMixin(PosComponent) { +export class LoginScreen extends PosComponent { + static template = "LoginScreen"; +} +// FIXME stop this double patch once the mixin is converted to a hook. +patch(LoginScreen.prototype, "pos_hr.LoginScreen SelectCashierMixin", SelectCashierMixin); +patch(LoginScreen.prototype, "pos_hr.LoginScreen methods", { setup() { - super.setup(); + this._super(...arguments); useBarcodeReader({ cashier: this.barcodeCashierAction }, true); - } + }, async selectCashier() { - if (await super.selectCashier()) { + if (await this._super()) { this.back(); } - } + }, async barcodeCashierAction(code) { - if (await super.barcodeCashierAction(code)) { + if (await this._super(code)) { this.back(); } - } + }, back() { this.props.resolve({ confirmed: false, payload: false }); this.trigger("close-temp-screen"); this.env.pos.hasLoggedIn = true; this.env.posbus.trigger("start-cash-control"); - } + }, confirm() { this.props.resolve({ confirmed: true, payload: true }); this.trigger("close-temp-screen"); - } + }, get shopName() { return this.env.pos.config.name; - } -} -LoginScreen.template = "LoginScreen"; + }, +}); -Registries.Component.add(LoginScreen); - -export default LoginScreen; +registry.category("pos_screens").add("LoginScreen", LoginScreen); diff --git a/addons/pos_hr/static/src/js/PaymentScreen.js b/addons/pos_hr/static/src/js/PaymentScreen.js index ae458e96a88..c2da288386e 100644 --- a/addons/pos_hr/static/src/js/PaymentScreen.js +++ b/addons/pos_hr/static/src/js/PaymentScreen.js @@ -1,16 +1,11 @@ /** @odoo-module */ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; -const PosHrPaymentScreen = (PaymentScreen_) => - class extends PaymentScreen_ { - async _finalizeValidation() { - this.currentOrder.employee = this.env.pos.get_cashier(); - await super._finalizeValidation(); - } - }; - -Registries.Component.extend(PaymentScreen, PosHrPaymentScreen); - -export default PaymentScreen; +patch(PaymentScreen.prototype, "pos_hr.PaymentScreen", { + async _finalizeValidation() { + this.currentOrder.employee = this.env.pos.get_cashier(); + await this._super(); + }, +}); diff --git a/addons/pos_hr/static/src/js/SelectCashierMixin.js b/addons/pos_hr/static/src/js/SelectCashierMixin.js index 7deb6cef0ab..087b0012617 100644 --- a/addons/pos_hr/static/src/js/SelectCashierMixin.js +++ b/addons/pos_hr/static/src/js/SelectCashierMixin.js @@ -1,76 +1,76 @@ /** @odoo-module */ /* global Sha1 */ -const SelectCashierMixin = (PosComponent) => - class ComponentWithSelectCashierMixin extends PosComponent { - async askPin(employee) { - const { confirmed, payload: inputPin } = await this.showPopup("NumberPopup", { - isPassword: true, - title: this.env._t("Password ?"), - startingValue: null, +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; + +// FIXME POSREF make this into a hook or something +export const SelectCashierMixin = { + async askPin(employee) { + const { confirmed, payload: inputPin } = await this.showPopup(NumberPopup, { + isPassword: true, + title: this.env._t("Password ?"), + startingValue: null, + }); + + if (!confirmed) { + return; + } + + if (employee.pin === Sha1.hash(inputPin)) { + return employee; + } else { + await this.showPopup(ErrorPopup, { + title: this.env._t("Incorrect Password"), + }); + return; + } + }, + + /** + * Select a cashier, the returning value will either be an object or nothing (undefined) + */ + async selectCashier() { + if (this.env.pos.config.module_pos_hr) { + const employeesList = this.env.pos.employees + .filter((employee) => employee.id !== this.env.pos.get_cashier().id) + .map((employee) => { + return { + id: employee.id, + item: employee, + label: employee.name, + isSelected: false, + }; + }); + let { confirmed, payload: employee } = await this.showPopup(SelectionPopup, { + title: this.env._t("Change Cashier"), + list: employeesList, }); if (!confirmed) { return; } - if (employee.pin === Sha1.hash(inputPin)) { - return employee; - } else { - await this.showPopup("ErrorPopup", { - title: this.env._t("Incorrect Password"), - }); - return; + if (employee && employee.pin) { + employee = await this.askPin(employee); } - } - - /** - * Select a cashier, the returning value will either be an object or nothing (undefined) - */ - async selectCashier() { - if (this.env.pos.config.module_pos_hr) { - const employeesList = this.env.pos.employees - .filter((employee) => employee.id !== this.env.pos.get_cashier().id) - .map((employee) => { - return { - id: employee.id, - item: employee, - label: employee.name, - isSelected: false, - }; - }); - let { confirmed, payload: employee } = await this.showPopup("SelectionPopup", { - title: this.env._t("Change Cashier"), - list: employeesList, - }); - - if (!confirmed) { - return; - } - - if (employee && employee.pin) { - employee = await this.askPin(employee); - } - if (employee) { - this.env.pos.set_cashier(employee); - } - return employee; - } - } - - async barcodeCashierAction(code) { - const employee = this.env.pos.employees.find( - (emp) => emp.barcode === Sha1.hash(code.code) - ); - if ( - employee && - employee !== this.env.pos.get_cashier() && - (!employee.pin || (await this.askPin(employee))) - ) { + if (employee) { this.env.pos.set_cashier(employee); } return employee; } - }; + }, -export default SelectCashierMixin; + async barcodeCashierAction(code) { + const employee = this.env.pos.employees.find((emp) => emp.barcode === Sha1.hash(code.code)); + if ( + employee && + employee !== this.env.pos.get_cashier() && + (!employee.pin || (await this.askPin(employee))) + ) { + this.env.pos.set_cashier(employee); + } + return employee; + }, +}; diff --git a/addons/pos_hr/static/src/js/models.js b/addons/pos_hr/static/src/js/models.js index 9680bf4d976..7d5990fbd53 100644 --- a/addons/pos_hr/static/src/js/models.js +++ b/addons/pos_hr/static/src/js/models.js @@ -1,86 +1,81 @@ /** @odoo-module */ import { PosGlobalState, Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const PosHrPosGlobalState = (PosGlobalState) => - class PosHrPosGlobalState extends PosGlobalState { - async _processData(loadedData) { - await super._processData(...arguments); - if (this.config.module_pos_hr) { - this.employees = loadedData["hr.employee"]; - this.employee_by_id = loadedData["employee_by_id"]; - this.reset_cashier(); - } +patch(PosGlobalState.prototype, "pos_hr.PosGlobalState", { + async _processData(loadedData) { + await this._super(...arguments); + if (this.config.module_pos_hr) { + this.employees = loadedData["hr.employee"]; + this.employee_by_id = loadedData["employee_by_id"]; + this.reset_cashier(); } - async after_load_server_data() { - await super.after_load_server_data(...arguments); - if (this.config.module_pos_hr) { - this.hasLoggedIn = !this.config.module_pos_hr; - } + }, + async after_load_server_data() { + await this._super(...arguments); + if (this.config.module_pos_hr) { + this.hasLoggedIn = !this.config.module_pos_hr; } - reset_cashier() { - this.cashier = { - name: null, - id: null, - barcode: null, - user_id: null, - pin: null, - role: null, - }; + }, + reset_cashier() { + this.cashier = { + name: null, + id: null, + barcode: null, + user_id: null, + pin: null, + role: null, + }; + }, + set_cashier(employee) { + this.cashier = employee; + const selectedOrder = this.get_order(); + if (selectedOrder && !selectedOrder.get_orderlines().length) { + // Order without lines can be considered to be un-owned by any employee. + // We set the cashier on that order to the currently set employee. + selectedOrder.cashier = employee; } - set_cashier(employee) { - this.cashier = employee; - const selectedOrder = this.get_order(); - if (selectedOrder && !selectedOrder.get_orderlines().length) { - // Order without lines can be considered to be un-owned by any employee. - // We set the cashier on that order to the currently set employee. - selectedOrder.cashier = employee; - } - if (!this.cashierHasPriceControlRights() && this.numpadMode === "price") { - this.numpadMode = "quantity"; - } + if (!this.cashierHasPriceControlRights() && this.numpadMode === "price") { + this.numpadMode = "quantity"; } + }, + /**{name: null, id: null, barcode: null, user_id:null, pin:null} + * If pos_hr is activated, return {name: string, id: int, barcode: string, pin: string, user_id: int} + * @returns {null|*} + */ + get_cashier() { + if (this.config.module_pos_hr) { + return this.cashier; + } + return this._super(); + }, + get_cashier_user_id() { + if (this.config.module_pos_hr) { + return this.cashier.user_id ? this.cashier.user_id : null; + } + return this._super(); + }, +}); - /**{name: null, id: null, barcode: null, user_id:null, pin:null} - * If pos_hr is activated, return {name: string, id: int, barcode: string, pin: string, user_id: int} - * @returns {null|*} - */ - get_cashier() { - if (this.config.module_pos_hr) { - return this.cashier; - } - return super.get_cashier(); +patch(Order.prototype, "pos_hr.Order", { + setup(options) { + this._super(...arguments); + if (!options.json && this.pos.config.module_pos_hr) { + this.cashier = this.pos.get_cashier(); } - get_cashier_user_id() { - if (this.config.module_pos_hr) { - return this.cashier.user_id ? this.cashier.user_id : null; - } - return super.get_cashier_user_id(); + }, + init_from_JSON(json) { + this._super(...arguments); + if (this.pos.config.module_pos_hr && json.employee_id) { + this.cashier = this.pos.employee_by_id[json.employee_id]; } - }; -Registries.Model.extend(PosGlobalState, PosHrPosGlobalState); - -const PosHrOrder = (Order) => - class PosHrOrder extends Order { - constructor(obj, options) { - super(...arguments); - if (!options.json && this.pos.config.module_pos_hr) { - this.cashier = this.pos.get_cashier(); - } + }, + export_as_JSON() { + const json = this._super(...arguments); + if (this.pos.config.module_pos_hr) { + json.employee_id = this.cashier ? this.cashier.id : false; } - init_from_JSON(json) { - super.init_from_JSON(...arguments); - if (this.pos.config.module_pos_hr && json.employee_id) { - this.cashier = this.pos.employee_by_id[json.employee_id]; - } - } - export_as_JSON() { - const json = super.export_as_JSON(...arguments); - if (this.pos.config.module_pos_hr) { - json.employee_id = this.cashier ? this.cashier.id : false; - } - return json; - } - }; -Registries.Model.extend(Order, PosHrOrder); + return json; + }, +}); diff --git a/addons/pos_hr_restaurant/static/src/js/Chrome.js b/addons/pos_hr_restaurant/static/src/js/Chrome.js index 09f5e6b7cdd..2382a2fab15 100644 --- a/addons/pos_hr_restaurant/static/src/js/Chrome.js +++ b/addons/pos_hr_restaurant/static/src/js/Chrome.js @@ -1,14 +1,11 @@ /* @odoo-module alias=pos_restaurant_hr.chrome */ -import Chrome from '@point_of_sale/js/Chrome'; -import Registries from '@point_of_sale/js/Registries'; +import { Chrome } from '@point_of_sale/js/Chrome'; +import { patch } from "@web/core/utils/patch"; - -export const PosHrRestaurantChrome = (Chrome) => class extends Chrome { +patch(Chrome.prototype, "pos_hr_restaurant.Chrome", { //@override _shouldResetIdleTimer() { - return super._shouldResetIdleTimer() && this.state.tempScreen?.name !== 'LoginScreen'; + return this._super() && this.state.tempScreen?.name !== 'LoginScreen'; } -} - -Registries.Component.extend(Chrome, PosHrRestaurantChrome); +}); diff --git a/addons/pos_loyalty/static/src/js/ControlButtons/PromoCodeButton.js b/addons/pos_loyalty/static/src/js/ControlButtons/PromoCodeButton.js index 31c87fa7f61..5868320b13b 100644 --- a/addons/pos_loyalty/static/src/js/ControlButtons/PromoCodeButton.js +++ b/addons/pos_loyalty/static/src/js/ControlButtons/PromoCodeButton.js @@ -1,18 +1,20 @@ /** @odoo-module **/ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; +import { TextInputPopup } from "@point_of_sale/js/Popups/TextInputPopup"; export class PromoCodeButton extends PosComponent { + static template = "PromoCodeButton"; + setup() { super.setup(); useListener("click", this.onClick); } async onClick() { - let { confirmed, payload: code } = await this.showPopup("TextInputPopup", { + let { confirmed, payload: code } = await this.showPopup(TextInputPopup, { title: this.env._t("Enter Code"), startingValue: "", placeholder: this.env._t("Gift card or Discount code"), @@ -26,8 +28,6 @@ export class PromoCodeButton extends PosComponent { } } -PromoCodeButton.template = "PromoCodeButton"; - ProductScreen.addControlButton({ component: PromoCodeButton, condition: function () { @@ -36,5 +36,3 @@ ProductScreen.addControlButton({ ); }, }); - -Registries.Component.add(PromoCodeButton); diff --git a/addons/pos_loyalty/static/src/js/ControlButtons/ResetProgramsButton.js b/addons/pos_loyalty/static/src/js/ControlButtons/ResetProgramsButton.js index bd06a58cf15..5ec2742c6e8 100644 --- a/addons/pos_loyalty/static/src/js/ControlButtons/ResetProgramsButton.js +++ b/addons/pos_loyalty/static/src/js/ControlButtons/ResetProgramsButton.js @@ -1,11 +1,12 @@ /** @odoo-module **/ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; export class ResetProgramsButton extends PosComponent { + static template = "ResetProgramsButton"; + setup() { super.setup(); useListener("click", this.onClick); @@ -18,13 +19,9 @@ export class ResetProgramsButton extends PosComponent { } } -ResetProgramsButton.template = "ResetProgramsButton"; - ProductScreen.addControlButton({ component: ResetProgramsButton, condition: function () { return this.env.pos.programs.some((p) => ["coupons", "promotion"].includes(p.program_type)); }, }); - -Registries.Component.add(ResetProgramsButton); diff --git a/addons/pos_loyalty/static/src/js/ControlButtons/RewardButton.js b/addons/pos_loyalty/static/src/js/ControlButtons/RewardButton.js index 87dcf63e61c..5167644d62b 100644 --- a/addons/pos_loyalty/static/src/js/ControlButtons/RewardButton.js +++ b/addons/pos_loyalty/static/src/js/ControlButtons/RewardButton.js @@ -1,12 +1,14 @@ /** @odoo-module **/ import { Gui } from "@point_of_sale/js/Gui"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; export class RewardButton extends PosComponent { + static template = "RewardButton"; + setup() { super.setup(); useListener("click", this.onClick); @@ -68,7 +70,7 @@ export class RewardButton extends PosComponent { label: this.env.pos.db.get_product_by_id(product_id).display_name, item: product_id, })); - const { confirmed, payload: selectedProduct } = await this.showPopup("SelectionPopup", { + const { confirmed, payload: selectedProduct } = await this.showPopup(SelectionPopup, { title: this.env._t("Please select a product for this reward"), list: productsList, }); @@ -112,7 +114,7 @@ export class RewardButton extends PosComponent { label: reward.reward.description, item: reward, })); - const { confirmed, payload: selectedReward } = await this.showPopup("SelectionPopup", { + const { confirmed, payload: selectedReward } = await this.showPopup(SelectionPopup, { title: this.env._t("Please select a reward"), list: rewardsList, }); @@ -128,13 +130,9 @@ export class RewardButton extends PosComponent { } } -RewardButton.template = "RewardButton"; - ProductScreen.addControlButton({ component: RewardButton, condition: function () { return this.env.pos.programs.length > 0; }, }); - -Registries.Component.add(RewardButton); diff --git a/addons/pos_loyalty/static/src/js/ControlButtons/eWalletButton.js b/addons/pos_loyalty/static/src/js/ControlButtons/eWalletButton.js index fa655d5415b..cbab3248efa 100644 --- a/addons/pos_loyalty/static/src/js/ControlButtons/eWalletButton.js +++ b/addons/pos_loyalty/static/src/js/ControlButtons/eWalletButton.js @@ -1,10 +1,13 @@ /** @odoo-module **/ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; export class eWalletButton extends PosComponent { + static template = "point_of_sale.eWalletButton"; + _getEWalletRewards(order) { const claimableRewards = order.getClaimableRewards(); const eWalletRewards = claimableRewards.filter( @@ -25,7 +28,7 @@ export class eWalletButton extends PosComponent { if (eWalletPrograms.length == 1) { selectedProgram = eWalletPrograms[0]; } else { - const { confirmed, payload } = await this.showPopup("SelectionPopup", { + const { confirmed, payload } = await this.showPopup(SelectionPopup, { title: this.env._t("Refund with eWallet"), list: eWalletPrograms.map((program) => ({ id: program.id, @@ -52,7 +55,7 @@ export class eWalletButton extends PosComponent { if (eWalletRewards.length == 1) { eWalletReward = eWalletRewards[0]; } else { - const { confirmed, payload } = await this.showPopup("SelectionPopup", { + const { confirmed, payload } = await this.showPopup(SelectionPopup, { title: this.env._t("Use eWallet to pay"), list: eWalletRewards.map(({ reward, coupon_id }) => ({ id: reward.id, @@ -72,7 +75,7 @@ export class eWalletButton extends PosComponent { ); if (result !== true) { // Returned an error - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Error"), body: result, }); @@ -94,7 +97,6 @@ export class eWalletButton extends PosComponent { } } } -eWalletButton.template = "point_of_sale.eWalletButton"; ProductScreen.addControlButton({ component: eWalletButton, @@ -102,5 +104,3 @@ ProductScreen.addControlButton({ return this.env.pos.programs.filter((p) => p.program_type == "ewallet").length > 0; }, }); - -Registries.Component.add(eWalletButton); diff --git a/addons/pos_loyalty/static/src/js/Loyalty.js b/addons/pos_loyalty/static/src/js/Loyalty.js index 9b6bce62000..0319ecc4461 100644 --- a/addons/pos_loyalty/static/src/js/Loyalty.js +++ b/addons/pos_loyalty/static/src/js/Loyalty.js @@ -1,12 +1,13 @@ /** @odoo-module **/ import { Order, Orderline, PosGlobalState } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; import session from "web.session"; import concurrency from "web.concurrency"; import { Gui } from "@point_of_sale/js/Gui"; import { round_decimals, round_precision } from "web.utils"; import core from "web.core"; +import { patch } from "@web/core/utils/patch"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; const _t = core._t; const dropPrevious = new concurrency.MutexedDropPrevious(); // Used for queuing reward updates @@ -75,1658 +76,1607 @@ function computeFreeQuantity(numberItems, n, m) { return Math.floor(free + adjustment); } -const PosLoyaltyGlobalState = (PosGlobalState) => - class PosLoyaltyGlobalState extends PosGlobalState { - //@override - async _processData(loadedData) { +patch(PosGlobalState.prototype, "pos_loyalty.PosGlobalState", { + //@override + async _processData(loadedData) { + this.couponCache = {}; + await this._super(loadedData); + this.productId2ProgramIds = loadedData["product_id_to_program_ids"]; + this.programs = loadedData["loyalty.program"] || []; //TODO: rename to `loyaltyPrograms` etc + this.rules = loadedData["loyalty.rule"] || []; + this.rewards = loadedData["loyalty.reward"] || []; + this._loadLoyaltyData(); + }, + _loadLoyaltyData() { + this.program_by_id = {}; + this.reward_by_id = {}; + + for (const program of this.programs) { + this.program_by_id[program.id] = program; + if (program.date_to) { + program.date_to = new Date(program.date_to); + } + program.rules = []; + program.rewards = []; + } + for (const rule of this.rules) { + rule.valid_product_ids = new Set(rule.valid_product_ids); + rule.program_id = this.program_by_id[rule.program_id[0]]; + rule.program_id.rules.push(rule); + } + for (const reward of this.rewards) { + this.reward_by_id[reward.id] = reward; + reward.program_id = this.program_by_id[reward.program_id[0]]; + reward.discount_line_product_id = this.db.get_product_by_id( + reward.discount_line_product_id[0] + ); + reward.all_discount_product_ids = new Set(reward.all_discount_product_ids); + reward.program_id.rewards.push(reward); + } + }, + async load_server_data() { + await this._super(...arguments); + if (this.selectedOrder) { + this.selectedOrder._updateRewards(); + } + }, + set_order(order) { + const result = this._super(...arguments); + // FIXME - JCB: This is a temporary fix. + // When an order is selected, it doesn't always contain the reward lines. + // And the list of active programs are not always correct. This is because + // of the use of DropPrevious in _updateRewards. + if (order) { + order._updateRewards(); + } + return result; + }, + /** + * Fetches `loyalty.card` records from the server and adds/updates them in our cache. + * + * @param {domain} domain For the search + * @param {int} limit Default to 1 + */ + async fetchCoupons(domain, limit = 1) { + const result = await this.env.services.rpc({ + model: "loyalty.card", + method: "search_read", + kwargs: { + domain: domain, + fields: ["id", "points", "code", "partner_id", "program_id"], + limit: limit, + context: session.user_context, + }, + }); + if (Object.keys(this.couponCache).length + result.length > COUPON_CACHE_MAX_SIZE) { this.couponCache = {}; - await super._processData(loadedData); - this.productId2ProgramIds = loadedData["product_id_to_program_ids"]; - this.programs = loadedData["loyalty.program"] || []; //TODO: rename to `loyaltyPrograms` etc - this.rules = loadedData["loyalty.rule"] || []; - this.rewards = loadedData["loyalty.reward"] || []; - this._loadLoyaltyData(); - } - _loadLoyaltyData() { - this.program_by_id = {}; - this.reward_by_id = {}; - - for (const program of this.programs) { - this.program_by_id[program.id] = program; - if (program.date_to) { - program.date_to = new Date(program.date_to); - } - program.rules = []; - program.rewards = []; - } - for (const rule of this.rules) { - rule.valid_product_ids = new Set(rule.valid_product_ids); - rule.program_id = this.program_by_id[rule.program_id[0]]; - rule.program_id.rules.push(rule); - } - for (const reward of this.rewards) { - this.reward_by_id[reward.id] = reward; - reward.program_id = this.program_by_id[reward.program_id[0]]; - reward.discount_line_product_id = this.db.get_product_by_id( - reward.discount_line_product_id[0] - ); - reward.all_discount_product_ids = new Set(reward.all_discount_product_ids); - reward.program_id.rewards.push(reward); - } - } - async load_server_data() { - await super.load_server_data(...arguments); + // Make sure that the current order has no invalid data. if (this.selectedOrder) { - this.selectedOrder._updateRewards(); + this.selectedOrder.invalidCoupons = true; } } - set_order(order) { - const result = super.set_order(...arguments); - // FIXME - JCB: This is a temporary fix. - // When an order is selected, it doesn't always contain the reward lines. - // And the list of active programs are not always correct. This is because - // of the use of DropPrevious in _updateRewards. - if (order) { - order._updateRewards(); - } - return result; + const couponList = []; + for (const dbCoupon of result) { + const coupon = new PosLoyaltyCard( + dbCoupon.code, + dbCoupon.id, + dbCoupon.program_id[0], + dbCoupon.partner_id[0], + dbCoupon.points + ); + this.couponCache[coupon.id] = coupon; + couponList.push(coupon); } - /** - * Fetches `loyalty.card` records from the server and adds/updates them in our cache. - * - * @param {domain} domain For the search - * @param {int} limit Default to 1 - */ - async fetchCoupons(domain, limit = 1) { - const result = await this.env.services.rpc({ - model: "loyalty.card", - method: "search_read", - kwargs: { - domain: domain, - fields: ["id", "points", "code", "partner_id", "program_id"], - limit: limit, - context: session.user_context, - }, + return couponList; + }, + /** + * Fetches a loyalty card for the given program and partner, put in cache afterwards + * if a matching card is found in the cache, that one is used instead. + * If no card is found a local only card will be created until the order is validated. + * + * @param {int} programId + * @param {int} partnerId + */ + async fetchLoyaltyCard(programId, partnerId) { + for (const coupon of Object.values(this.couponCache)) { + if (coupon.partner_id === partnerId && coupon.program_id === programId) { + return coupon; + } + } + const fetchedCoupons = await this.fetchCoupons([ + ["partner_id", "=", partnerId], + ["program_id", "=", programId], + ]); + const dbCoupon = fetchedCoupons.length > 0 ? fetchedCoupons[0] : null; + return dbCoupon || new PosLoyaltyCard(null, null, programId, partnerId, 0); + }, + getLoyaltyCards(partner) { + const loyaltyCards = []; + for (const coupon of Object.values(this.couponCache)) { + if (coupon.partner_id === partner.id) { + loyaltyCards.push(coupon); + } + } + return loyaltyCards; + }, + addPartners(partners) { + const result = this._super(partners); + // cache the loyalty cards of the partners + for (const partner of partners) { + for (const [couponId, { code, program_id, points }] of Object.entries( + partner.loyalty_cards || {} + )) { + this.couponCache[couponId] = new PosLoyaltyCard( + code, + parseInt(couponId, 10), + program_id, + partner.id, + points + ); + } + } + return result; + }, +}); + +patch(Orderline.prototype, "pos_loyalty.Orderline", { + export_as_JSON() { + const result = this._super(...arguments); + result.is_reward_line = this.is_reward_line; + result.reward_id = this.reward_id; + result.reward_product_id = this.reward_product_id; + result.coupon_id = this.coupon_id; + result.reward_identifier_code = this.reward_identifier_code; + result.points_cost = this.points_cost; + result.giftBarcode = this.giftBarcode; + result.giftCardId = this.giftCardId; + result.eWalletGiftCardProgramId = this.eWalletGiftCardProgram + ? this.eWalletGiftCardProgram.id + : null; + return result; + }, + init_from_JSON(json) { + if (json.is_reward_line) { + this.is_reward_line = json.is_reward_line; + this.reward_id = json.reward_id; + this.reward_product_id = json.reward_product_id; + // Since non existing coupon have a negative id, of which the counter is lost upon reloading + // we make sure that they are kept the same between after a reload between the order and the lines. + this.coupon_id = this.order.oldCouponMapping[json.coupon_id] || json.coupon_id; + this.reward_identifier_code = json.reward_identifier_code; + this.points_cost = json.points_cost; + } + this.giftBarcode = json.giftBarcode; + this.giftCardId = json.giftCardId; + this.eWalletGiftCardProgram = this.pos.program_by_id[json.eWalletGiftCardProgramId]; + this._super(...arguments); + }, + set_quantity(quantity, keep_price) { + if (quantity === "remove" && this.is_reward_line) { + // Remove any line that is part of that same reward aswell. + const linesToRemove = []; + this.order.get_orderlines().forEach((line) => { + if ( + line != this && + line.reward_id === this.reward_id && + line.coupon_id === this.coupon_id && + line.reward_identifier_code === this.reward_identifier_code + ) { + linesToRemove.push(line); + } }); - if (Object.keys(this.couponCache).length + result.length > COUPON_CACHE_MAX_SIZE) { - this.couponCache = {}; - // Make sure that the current order has no invalid data. - if (this.selectedOrder) { - this.selectedOrder.invalidCoupons = true; - } + for (const line of linesToRemove) { + this.order.orderlines.remove(line); } - const couponList = []; - for (const dbCoupon of result) { - const coupon = new PosLoyaltyCard( - dbCoupon.code, - dbCoupon.id, - dbCoupon.program_id[0], - dbCoupon.partner_id[0], - dbCoupon.points - ); - this.couponCache[coupon.id] = coupon; - couponList.push(coupon); - } - return couponList; } - /** - * Fetches a loyalty card for the given program and partner, put in cache afterwards - * if a matching card is found in the cache, that one is used instead. - * If no card is found a local only card will be created until the order is validated. - * - * @param {int} programId - * @param {int} partnerId - */ - async fetchLoyaltyCard(programId, partnerId) { - for (const coupon of Object.values(this.couponCache)) { - if (coupon.partner_id === partnerId && coupon.program_id === programId) { - return coupon; - } - } - const fetchedCoupons = await this.fetchCoupons([ - ["partner_id", "=", partnerId], - ["program_id", "=", programId], - ]); - const dbCoupon = fetchedCoupons.length > 0 ? fetchedCoupons[0] : null; - return dbCoupon || new PosLoyaltyCard(null, null, programId, partnerId, 0); - } - getLoyaltyCards(partner) { - const loyaltyCards = []; - for (const coupon of Object.values(this.couponCache)) { - if (coupon.partner_id === partner.id) { - loyaltyCards.push(coupon); - } - } - return loyaltyCards; - } - addPartners(partners) { - const result = super.addPartners(partners); - // cache the loyalty cards of the partners - for (const partner of partners) { - for (const [couponId, { code, program_id, points }] of Object.entries( - partner.loyalty_cards || {} - )) { - this.couponCache[couponId] = new PosLoyaltyCard( - code, - parseInt(couponId, 10), - program_id, - partner.id, - points - ); - } - } - return result; - } - }; -Registries.Model.extend(PosGlobalState, PosLoyaltyGlobalState); + return this._super(...arguments); + }, + getEWalletGiftCardProgramType() { + return this.eWalletGiftCardProgram && this.eWalletGiftCardProgram.program_type; + }, + ignoreLoyaltyPoints({ program }) { + return ( + ["gift_card", "ewallet"].includes(program.program_type) && + this.eWalletGiftCardProgram && + this.eWalletGiftCardProgram.id !== program.id + ); + }, +}); -const PosLoyaltyOrderline = (Orderline) => - class PosLoyaltyOrderline extends Orderline { - export_as_JSON() { - const result = super.export_as_JSON(...arguments); - result.is_reward_line = this.is_reward_line; - result.reward_id = this.reward_id; - result.reward_product_id = this.reward_product_id; - result.coupon_id = this.coupon_id; - result.reward_identifier_code = this.reward_identifier_code; - result.points_cost = this.points_cost; - result.giftBarcode = this.giftBarcode; - result.giftCardId = this.giftCardId; - result.eWalletGiftCardProgramId = this.eWalletGiftCardProgram - ? this.eWalletGiftCardProgram.id - : null; - return result; - } - init_from_JSON(json) { - if (json.is_reward_line) { - this.is_reward_line = json.is_reward_line; - this.reward_id = json.reward_id; - this.reward_product_id = json.reward_product_id; - // Since non existing coupon have a negative id, of which the counter is lost upon reloading - // we make sure that they are kept the same between after a reload between the order and the lines. - this.coupon_id = this.order.oldCouponMapping[json.coupon_id] || json.coupon_id; - this.reward_identifier_code = json.reward_identifier_code; - this.points_cost = json.points_cost; - } - this.giftBarcode = json.giftBarcode; - this.giftCardId = json.giftCardId; - this.eWalletGiftCardProgram = this.pos.program_by_id[json.eWalletGiftCardProgramId]; - super.init_from_JSON(...arguments); - } - set_quantity(quantity, keep_price) { - if (quantity === "remove" && this.is_reward_line) { - // Remove any line that is part of that same reward aswell. - const linesToRemove = []; - this.order.get_orderlines().forEach((line) => { - if ( - line != this && - line.reward_id === this.reward_id && - line.coupon_id === this.coupon_id && - line.reward_identifier_code === this.reward_identifier_code - ) { - linesToRemove.push(line); - } - }); - for (const line of linesToRemove) { - this.order.orderlines.remove(line); +patch(Order.prototype, "pos_loyalty.Order", { + setup() { + this._super(...arguments); + this._initializePrograms({}); + // Always start with invalid coupons so that coupon for this + // order is properly assigned. @see _checkMissingCoupons + this.invalidCoupons = true; + }, + export_as_JSON() { + const json = this._super(...arguments); + json.disabledRewards = [...this.disabledRewards]; + json.codeActivatedProgramRules = this.codeActivatedProgramRules; + json.codeActivatedCoupons = this.codeActivatedCoupons; + json.couponPointChanges = this.couponPointChanges; + return json; + }, + init_from_JSON(json) { + this.couponPointChanges = json.couponPointChanges; + // Remapping of coupon_id for both couponPointChanges and Orderline.coupon_id + this.oldCouponMapping = {}; + if (this.couponPointChanges) { + for (const [key, pe] of Object.entries(this.couponPointChanges)) { + if (!this.pos.program_by_id[pe.program_id]) { + // Remove points changes for programs that are not available anymore. + delete this.couponPointChanges[key]; + continue; } + if (pe.coupon_id > 0) { + continue; + } + const newId = nextId--; + delete this.oldCouponMapping[pe.coupon_id]; + pe.coupon_id = newId; + this.couponPointChanges[newId] = pe; } - return super.set_quantity(...arguments); } - getEWalletGiftCardProgramType() { - return this.eWalletGiftCardProgram && this.eWalletGiftCardProgram.program_type; - } - ignoreLoyaltyPoints({ program }) { - return ( - ["gift_card", "ewallet"].includes(program.program_type) && - this.eWalletGiftCardProgram && - this.eWalletGiftCardProgram.id !== program.id + this._super(...arguments); + delete this.oldCouponMapping; + this.disabledRewards = new Set(json.disabledRewards); + this.codeActivatedProgramRules = json.codeActivatedProgramRules; + this.codeActivatedCoupons = json.codeActivatedCoupons; + }, + /** + * We need to update the rewards upon changing the partner as it may impact the points available + * for rewards. + * + * @override + */ + set_partner(partner) { + const oldPartner = this.get_partner(); + this._super(partner); + if (this.couponPointChanges && oldPartner !== this.get_partner()) { + // Remove couponPointChanges for cards in is_nominative programs. + // This makes sure that counting of points on loyalty and ewallet programs is updated after partner changes. + const loyaltyProgramIds = new Set( + this.pos.programs + .filter((program) => program.is_nominative) + .map((program) => program.id) ); - } - }; -Registries.Model.extend(Orderline, PosLoyaltyOrderline); - -const PosLoyaltyOrder = (Order) => - class PosLoyaltyOrder extends Order { - constructor() { - super(...arguments); - this._initializePrograms({}); - // Always start with invalid coupons so that coupon for this - // order is properly assigned. @see _checkMissingCoupons - this.invalidCoupons = true; - } - export_as_JSON() { - const json = super.export_as_JSON(...arguments); - json.disabledRewards = [...this.disabledRewards]; - json.codeActivatedProgramRules = this.codeActivatedProgramRules; - json.codeActivatedCoupons = this.codeActivatedCoupons; - json.couponPointChanges = this.couponPointChanges; - return json; - } - init_from_JSON(json) { - this.couponPointChanges = json.couponPointChanges; - // Remapping of coupon_id for both couponPointChanges and Orderline.coupon_id - this.oldCouponMapping = {}; - if (this.couponPointChanges) { - for (const [key, pe] of Object.entries(this.couponPointChanges)) { - if (!this.pos.program_by_id[pe.program_id]) { - // Remove points changes for programs that are not available anymore. - delete this.couponPointChanges[key]; - continue; - } - if (pe.coupon_id > 0) { - continue; - } - const newId = nextId--; - delete this.oldCouponMapping[pe.coupon_id]; - pe.coupon_id = newId; - this.couponPointChanges[newId] = pe; + for (const [key, pointChange] of Object.entries(this.couponPointChanges)) { + if (loyaltyProgramIds.has(pointChange.program_id)) { + delete this.couponPointChanges[key]; } } - super.init_from_JSON(...arguments); - delete this.oldCouponMapping; - this.disabledRewards = new Set(json.disabledRewards); - this.codeActivatedProgramRules = json.codeActivatedProgramRules; - this.codeActivatedCoupons = json.codeActivatedCoupons; - } - /** - * We need to update the rewards upon changing the partner as it may impact the points available - * for rewards. - * - * @override - */ - set_partner(partner) { - const oldPartner = this.get_partner(); - super.set_partner(partner); - if (this.couponPointChanges && oldPartner !== this.get_partner()) { - // Remove couponPointChanges for cards in is_nominative programs. - // This makes sure that counting of points on loyalty and ewallet programs is updated after partner changes. - const loyaltyProgramIds = new Set( - this.pos.programs - .filter((program) => program.is_nominative) - .map((program) => program.id) - ); - for (const [key, pointChange] of Object.entries(this.couponPointChanges)) { - if (loyaltyProgramIds.has(pointChange.program_id)) { - delete this.couponPointChanges[key]; - } - } - this._updateRewards(); - } - } - wait_for_push_order() { - return ( - !_.isEmpty(this.couponPointChanges) || - this._get_reward_lines().length || - super.wait_for_push_order(...arguments) - ); - } - /** - * Add additional information for our ticket, such as new coupons and loyalty point gains. - * - * @override - */ - export_for_printing() { - const result = super.export_for_printing(...arguments); - if (this.get_partner()) { - result.loyaltyStats = this.getLoyaltyPoints(); - } - result.new_coupon_info = this.new_coupon_info; - return result; - } - //@override - _get_ignored_product_ids_total_discount() { - const productIds = super._get_ignored_product_ids_total_discount(...arguments); - const giftCardPrograms = this.pos.programs.filter( - (p) => p.program_type === "gift_card" - ); - for (const program of giftCardPrograms) { - const giftCardProductId = [...program.rules[0].valid_product_ids][0]; - if (giftCardProductId) { - productIds.push(giftCardProductId); - } - } - return productIds; - } - get_orderlines() { - const orderlines = super.get_orderlines(this, arguments); - const rewardLines = []; - const nonRewardLines = []; - for (const line of orderlines) { - if (line.is_reward_line) { - rewardLines.push(line); - } else { - nonRewardLines.push(line); - } - } - return [...nonRewardLines, ...rewardLines]; - } - _get_reward_lines() { - const orderLines = super.get_orderlines(...arguments); - if (orderLines) { - return orderLines.filter((line) => line.is_reward_line); - } - return orderLines; - } - _get_regular_order_lines() { - const orderLines = super.get_orderlines(...arguments); - if (orderLines) { - return orderLines.filter( - (line) => !line.is_reward_line && !line.refunded_orderline_id - ); - } - return orderLines; - } - get_last_orderline() { - const orderLines = super - .get_orderlines(...arguments) - .filter((line) => !line.is_reward_line); - return orderLines[orderLines.length - 1]; - } - set_pricelist(pricelist) { - super.set_pricelist(...arguments); this._updateRewards(); } - set_orderline_options(line, options) { - super.set_orderline_options(...arguments); - if (options && options.is_reward_line) { - line.is_reward_line = options.is_reward_line; - line.reward_id = options.reward_id; - line.reward_product_id = options.reward_product_id; - line.coupon_id = options.coupon_id; - line.reward_identifier_code = options.reward_identifier_code; - line.points_cost = options.points_cost; - line.price_manually_set = true; - } - line.giftBarcode = options.giftBarcode; - line.giftCardId = options.giftCardId; - line.eWalletGiftCardProgram = options.eWalletGiftCardProgram; + }, + wait_for_push_order() { + return ( + !_.isEmpty(this.couponPointChanges) || + this._get_reward_lines().length || + this._super(...arguments) + ); + }, + /** + * Add additional information for our ticket, such as new coupons and loyalty point gains. + * + * @override + */ + export_for_printing() { + const result = this._super(...arguments); + if (this.get_partner()) { + result.loyaltyStats = this.getLoyaltyPoints(); } - add_product(product, options) { - super.add_product(...arguments); - this._updateRewards(); - } - - async _initializePrograms() { - // When deleting a reward line, a popup will be displayed if the reward was automatic, - // if confirmed the reward is added to this list and will not be claimed automatically again. - if (!this.disabledRewards) { - this.disabledRewards = new Set(); - } - // List of programs that require a code that are activated. - if (!this.codeActivatedProgramRules) { - this.codeActivatedProgramRules = []; - } - // List of coupons activated manually - if (!this.codeActivatedCoupons) { - this.codeActivatedCoupons = []; - } - // This field will hold the added points for each coupon. - // Points lost are directly linked to the order lines. - if (!this.couponPointChanges) { - this.couponPointChanges = {}; + result.new_coupon_info = this.new_coupon_info; + return result; + }, + //@override + _get_ignored_product_ids_total_discount() { + const productIds = this._super(...arguments); + const giftCardPrograms = this.pos.programs.filter((p) => p.program_type === "gift_card"); + for (const program of giftCardPrograms) { + const giftCardProductId = [...program.rules[0].valid_product_ids][0]; + if (giftCardProductId) { + productIds.push(giftCardProductId); } } - _resetPrograms() { + return productIds; + }, + get_orderlines() { + const orderlines = this._super(this, arguments); + const rewardLines = []; + const nonRewardLines = []; + for (const line of orderlines) { + if (line.is_reward_line) { + rewardLines.push(line); + } else { + nonRewardLines.push(line); + } + } + return [...nonRewardLines, ...rewardLines]; + }, + _get_reward_lines() { + if (this.orderlines) { + return this.orderlines.filter((line) => line.is_reward_line); + } + return this.orderlines; + }, + _get_regular_order_lines() { + if (this.orderlines) { + return this.orderlines.filter( + (line) => !line.is_reward_line && !line.refunded_orderline_id + ); + } + return this.orderlines; + }, + get_last_orderline() { + const orderLines = this.orderlines.filter((line) => !line.is_reward_line); + return orderLines[orderLines.length - 1]; + }, + set_pricelist(pricelist) { + this._super(...arguments); + this._updateRewards(); + }, + set_orderline_options(line, options) { + this._super(...arguments); + if (options && options.is_reward_line) { + line.is_reward_line = options.is_reward_line; + line.reward_id = options.reward_id; + line.reward_product_id = options.reward_product_id; + line.coupon_id = options.coupon_id; + line.reward_identifier_code = options.reward_identifier_code; + line.points_cost = options.points_cost; + line.price_manually_set = true; + } + line.giftBarcode = options.giftBarcode; + line.giftCardId = options.giftCardId; + line.eWalletGiftCardProgram = options.eWalletGiftCardProgram; + }, + add_product(product, options) { + this._super(...arguments); + this._updateRewards(); + }, + async _initializePrograms() { + // When deleting a reward line, a popup will be displayed if the reward was automatic, + // if confirmed the reward is added to this list and will not be claimed automatically again. + if (!this.disabledRewards) { this.disabledRewards = new Set(); - this.codeActivatedProgramRules = []; - this.codeActivatedCoupons = []; - this.couponPointChanges = {}; - this.orderlines.remove(this._get_reward_lines()); - this._updateRewards(); } - _updateRewards() { - // Calls are not expected to take some time besides on the first load + when loyalty programs are made applicable - if (this.pos.programs.length === 0) { + // List of programs that require a code that are activated. + if (!this.codeActivatedProgramRules) { + this.codeActivatedProgramRules = []; + } + // List of coupons activated manually + if (!this.codeActivatedCoupons) { + this.codeActivatedCoupons = []; + } + // This field will hold the added points for each coupon. + // Points lost are directly linked to the order lines. + if (!this.couponPointChanges) { + this.couponPointChanges = {}; + } + }, + _resetPrograms() { + this.disabledRewards = new Set(); + this.codeActivatedProgramRules = []; + this.codeActivatedCoupons = []; + this.couponPointChanges = {}; + this.orderlines.remove(this._get_reward_lines()); + this._updateRewards(); + }, + _updateRewards() { + // Calls are not expected to take some time besides on the first load + when loyalty programs are made applicable + if (this.pos.programs.length === 0) { + return; + } + dropPrevious + .exec(() => { + return this._updateLoyaltyPrograms().then(async () => { + // Try auto claiming rewards + const claimableRewards = this.getClaimableRewards(false, false, true); + let changed = false; + for (const { coupon_id, reward } of claimableRewards) { + if ( + reward.program_id.rewards.length === 1 && + !reward.program_id.is_nominative && + (reward.reward_type !== "product" || + (reward.reward_type == "product" && !reward.multi_product)) + ) { + this._applyReward(reward, coupon_id); + changed = true; + } + } + // Rewards may impact the number of points gained + if (changed) { + await this._updateLoyaltyPrograms(); + } + this._updateRewardLines(); + }); + }) + .catch(() => { + /* catch the reject of dp when calling `add` to avoid unhandledrejection */ + }); + }, + async _updateLoyaltyPrograms() { + await this._checkMissingCoupons(); + await this._updatePrograms(); + }, + /** + * Checks that all 'existing' coupons are in our cache, and if not load/update them. + */ + async _checkMissingCoupons() { + // This function must stay sequential to avoid potential concurrency errors. + await mutex.exec(async () => { + if (!this.invalidCoupons) { return; } - dropPrevious - .exec(() => { - return this._updateLoyaltyPrograms().then(async () => { - // Try auto claiming rewards - const claimableRewards = this.getClaimableRewards(false, false, true); - let changed = false; - for (const { coupon_id, reward } of claimableRewards) { - if ( - reward.program_id.rewards.length === 1 && - !reward.program_id.is_nominative && - (reward.reward_type !== "product" || - (reward.reward_type == "product" && !reward.multi_product)) - ) { - this._applyReward(reward, coupon_id); - changed = true; + this.invalidCoupons = false; + const allCoupons = []; + for (const pe of Object.values(this.couponPointChanges)) { + if (pe.coupon_id > 0) { + allCoupons.push(pe.coupon_id); + } + } + allCoupons.push(...this.codeActivatedCoupons.map((coupon) => coupon.id)); + const couponsToFetch = allCoupons.filter((elem) => !this.pos.couponCache[elem]); + if (couponsToFetch.length) { + await this.pos.fetchCoupons([["id", "in", couponsToFetch]], couponsToFetch.length); + // Remove coupons that could not be loaded from the db + this.codeActivatedCoupons = this.codeActivatedCoupons.filter( + (coupon) => this.pos.couponCache[coupon.id] + ); + this.couponPointChanges = Object.fromEntries( + Object.entries(this.couponPointChanges).filter( + ([k, pe]) => this.pos.couponCache[pe.coupon_id] + ) + ); + } + }); + }, + /** + * Refreshes the currently applied rewards, if they are not applicable anymore they are removed. + */ + _updateRewardLines() { + if (!this.orderlines.length) { + return; + } + const rewardLines = this._get_reward_lines(); + if (!rewardLines.length) { + return; + } + const productRewards = []; + const otherRewards = []; + const paymentRewards = []; // Gift card and ewallet rewards are considered payments and must stay at the end + for (const line of rewardLines) { + const claimedReward = { + reward: this.pos.reward_by_id[line.reward_id], + coupon_id: line.coupon_id, + args: { + product: line.reward_product_id, + }, + }; + if ( + claimedReward.reward.program_id.program_type === "gift_card" || + claimedReward.reward.program_id.program_type === "ewallet" + ) { + paymentRewards.push(claimedReward); + } else if (claimedReward.reward.reward_type === "product") { + productRewards.push(claimedReward); + } else { + otherRewards.push(claimedReward); + } + this.orderlines.remove(line); + } + for (const claimedReward of productRewards.concat(otherRewards).concat(paymentRewards)) { + // For existing coupons check that they are still claimed, they can exist in either `couponPointChanges` or `codeActivatedCoupons` + if ( + !this.codeActivatedCoupons.find( + (coupon) => coupon.id === claimedReward.coupon_id + ) && + !this.couponPointChanges[claimedReward.coupon_id] + ) { + continue; + } + this._applyReward(claimedReward.reward, claimedReward.coupon_id, claimedReward.args); + } + }, + /** + * Update our couponPointChanges, meaning the points/coupons each program give etc. + */ + async _updatePrograms() { + const changesPerProgram = {}; + const programsToCheck = new Set(); + // By default include all programs that are considered 'applicable' + for (const program of this.pos.programs) { + if (this._programIsApplicable(program)) { + programsToCheck.add(program.id); + } + } + for (const pe of Object.values(this.couponPointChanges)) { + if (!changesPerProgram[pe.program_id]) { + changesPerProgram[pe.program_id] = []; + programsToCheck.add(pe.program_id); + } + changesPerProgram[pe.program_id].push(pe); + } + for (const coupon of this.codeActivatedCoupons) { + programsToCheck.add(coupon.program_id); + } + const programs = [...programsToCheck].map((programId) => this.pos.program_by_id[programId]); + const pointsAddedPerProgram = this.pointsForPrograms(programs); + for (const program of this.pos.programs) { + // Future programs may split their points per unit paid (gift cards for example), consider a non applicable program to give no points + const pointsAdded = this._programIsApplicable(program) + ? pointsAddedPerProgram[program.id] + : []; + // For programs that apply to both (loyalty) we always add a change of 0 points, if there is none, since it makes it easier to + // track for claimable rewards, and makes sure to load the partner's loyalty card. + if (program.is_nominative && !pointsAdded.length && this.get_partner()) { + pointsAdded.push({ points: 0 }); + } + const oldChanges = changesPerProgram[program.id] || []; + // Update point changes for those that exist + for (let idx = 0; idx < Math.min(pointsAdded.length, oldChanges.length); idx++) { + Object.assign(oldChanges[idx], pointsAdded[idx]); + } + if (pointsAdded.length < oldChanges.length) { + const removedIds = oldChanges.map((pe) => pe.coupon_id); + this.couponPointChanges = Object.fromEntries( + Object.entries(this.couponPointChanges).filter(([k, pe]) => { + return !removedIds.includes(pe.coupon_id); + }) + ); + } else if (pointsAdded.length > oldChanges.length) { + for (const pa of pointsAdded.splice(oldChanges.length)) { + const coupon = await this._couponForProgram(program); + this.couponPointChanges[coupon.id] = { + points: pa.points, + program_id: program.id, + coupon_id: coupon.id, + barcode: pa.barcode, + }; + } + } + } + // Also remove coupons from codeActivatedCoupons if their program applies_on current orders and the program does not give any points + this.codeActivatedCoupons = this.codeActivatedCoupons.filter((coupon) => { + const program = this.pos.program_by_id[coupon.program_id]; + if ( + program.applies_on === "current" && + pointsAddedPerProgram[program.id].length === 0 + ) { + return false; + } + return true; + }); + }, + /** + * @typedef {{ won: number, spend: number, total: number, balance: number, name: string}} LoyaltyPoints + * @typedef {{ couponId: number, program: object, points: LoyaltyPoints}} LoyaltyStat + * @returns {Array} + */ + getLoyaltyPoints() { + // map: couponId -> LoyaltyPoints + const loyaltyPoints = {}; + for (const pointChange of Object.values(this.couponPointChanges)) { + const { coupon_id, points, program_id } = pointChange; + const program = this.pos.program_by_id[program_id]; + if (program.program_type !== "loyalty") { + // Not a loyalty program, skip + continue; + } + const loyaltyCard = this.pos.couponCache[coupon_id] || /* or new card */ { + id: coupon_id, + balance: 0, + }; + let [won, spent, total] = [0, 0, 0]; + const balance = loyaltyCard.balance; + won += points - this._getPointsCorrection(program); + if (coupon_id !== 0) { + for (const line of this._get_reward_lines()) { + if (line.coupon_id === coupon_id) { + spent += line.points_cost; + } + } + } + total = balance + won - spent; + const name = program.portal_visible ? program.portal_point_name : _t("Points"); + loyaltyPoints[coupon_id] = { + won: parseFloat(won.toFixed(2)), + spent: parseFloat(spent.toFixed(2)), + // Display total when order is ongoing. + total: parseFloat(total.toFixed(2)), + // Display balance when order is done. + balance: parseFloat(balance.toFixed(2)), + name, + program, + }; + } + return Object.entries(loyaltyPoints).map(([couponId, points]) => ({ + couponId, + points, + program: points.program, + })); + }, + /** + * The points in the couponPointChanges for free product reward is not correct. + * It doesn't take into account the points from the `free` product. Use this method + * to compute the necessary correction. + * @param {*} program + * @returns {number} + */ + _getPointsCorrection(program) { + const rewardLines = this.orderlines.filter((line) => line.is_reward_line); + let res = 0; + for (const rule of program.rules) { + for (const line of rewardLines) { + const reward = this.pos.reward_by_id[line.reward_id]; + if (reward.reward_type !== "product") { + continue; + } + if (rule.reward_point_mode === "order") { + res += rule.reward_point_amount; + } else if (rule.reward_point_mode === "money") { + res -= round_precision( + rule.reward_point_amount * line.get_price_with_tax(), + 0.01 + ); + } else if (rule.reward_point_mode === "unit") { + res += rule.reward_point_amount * line.get_quantity(); + } + } + } + return res; + }, + /** + * @returns {number} The points that are left for the given coupon for this order. + */ + _getRealCouponPoints(coupon_id) { + let points = 0; + const dbCoupon = this.pos.couponCache[coupon_id]; + if (dbCoupon) { + points += dbCoupon.balance; + } + Object.values(this.couponPointChanges).some((pe) => { + if (pe.coupon_id === coupon_id) { + if (this.pos.program_by_id[pe.program_id].applies_on !== "future") { + points += pe.points; + } + // couponPointChanges is not supposed to have a coupon multiple times + return true; + } + return false; + }); + for (const line of this.get_orderlines()) { + if (line.is_reward_line && line.coupon_id === coupon_id) { + points -= line.points_cost; + } + } + return points; + }, + /** + * Depending on the program type returns a new (local) instance of coupon or tries to retrieve the coupon in case of loyalty cards. + * Existing coupons are put in a cache which is also used to fetch the coupons. + * + * @param {object} program + */ + async _couponForProgram(program) { + if (program.is_nominative) { + return this.pos.fetchLoyaltyCard(program.id, this.get_partner().id); + } + // This type of coupons don't need to really exist up until validating the order, so no need to cache + return new PosLoyaltyCard(null, null, program.id, (this.get_partner() || { id: -1 }).id, 0); + }, + _programIsApplicable(program) { + if ( + program.trigger === "auto" && + !program.rules.find( + (rule) => rule.mode === "auto" || this.codeActivatedProgramRules.includes(rule.id) + ) + ) { + return false; + } + if ( + program.trigger === "with_code" && + !program.rules.find((rule) => this.codeActivatedProgramRules.includes(rule.id)) + ) { + return false; + } + if (program.is_nominative && !this.get_partner()) { + return false; + } + if (program.date_to && program.date_to <= new Date()) { + return false; + } + if (program.limit_usage && program.total_order_count >= program.max_usage) { + return false; + } + return true; + }, + /** + * Computes how much points each program gives. + * + * @param {Array} programs list of loyalty.program + * @returns {Object} Containing the points gained per program + */ + pointsForPrograms(programs) { + const totalTaxed = this.get_total_with_tax(); + const totalUntaxed = this.get_total_without_tax(); + const totalsPerProgram = Object.fromEntries( + programs.map((program) => [program.id, { untaxed: totalUntaxed, taxed: totalTaxed }]) + ); + const orderLines = this.get_orderlines(); + for (const line of orderLines) { + if (!line.reward_id) { + continue; + } + const reward = this.pos.reward_by_id[line.reward_id]; + if (reward.reward_type !== "discount") { + continue; + } + const rewardProgram = reward.program_id; + for (const program of programs) { + // Remove automatic discount and this program's discounts from the totals. + if (program.id === rewardProgram.id || rewardProgram.trigger === "auto") { + totalsPerProgram[program.id]["taxed"] -= line.get_price_with_tax(); + totalsPerProgram[program.id]["untaxed"] -= line.get_price_without_tax(); + } + } + } + const result = {}; + for (const program of programs) { + let points = 0; + const splitPoints = []; + for (const rule of program.rules) { + if ( + rule.mode === "with_code" && + !this.codeActivatedProgramRules.includes(rule.id) + ) { + continue; + } + const amountCheck = + (rule.minimum_amount_tax_mode === "incl" && + totalsPerProgram[program.id]["taxed"]) || + totalsPerProgram[program.id]["untaxed"]; + if (rule.minimum_amount > amountCheck) { + // NOTE: big doutes par rapport au fait de compter tous les produits + continue; + } + let totalProductQty = 0; + // Only count points for paid lines. + const qtyPerProduct = {}; + let orderedProductPaid = 0; + for (const line of orderLines) { + if ( + ((!line.reward_product_id && + (rule.any_product || + rule.valid_product_ids.has(line.get_product().id))) || + (line.reward_product_id && + (rule.any_product || + rule.valid_product_ids.has(line.reward_product_id)))) && + !line.ignoreLoyaltyPoints({ program }) + ) { + // We only count reward products from the same program to avoid unwanted feedback loops + if (line.reward_product_id) { + const reward = this.pos.reward_by_id[line.reward_id]; + if (program.id !== reward.program_id) { + continue; } } - // Rewards may impact the number of points gained - if (changed) { - await this._updateLoyaltyPrograms(); + const lineQty = line.reward_product_id + ? -line.get_quantity() + : line.get_quantity(); + totalProductQty += lineQty; + if (qtyPerProduct[line.reward_product_id || line.get_product().id]) { + qtyPerProduct[line.reward_product_id || line.get_product().id] += + lineQty; + } else { + qtyPerProduct[line.reward_product_id || line.get_product().id] = + lineQty; } - this._updateRewardLines(); - }); - }) - .catch(() => { - /* catch the reject of dp when calling `add` to avoid unhandledrejection */ - }); - } - async _updateLoyaltyPrograms() { - await this._checkMissingCoupons(); - await this._updatePrograms(); - } - /** - * Checks that all 'existing' coupons are in our cache, and if not load/update them. - */ - async _checkMissingCoupons() { - // This function must stay sequential to avoid potential concurrency errors. - await mutex.exec(async () => { - if (!this.invalidCoupons) { - return; - } - this.invalidCoupons = false; - const allCoupons = []; - for (const pe of Object.values(this.couponPointChanges)) { - if (pe.coupon_id > 0) { - allCoupons.push(pe.coupon_id); + orderedProductPaid += line.get_price_with_tax(); } } - allCoupons.push(...this.codeActivatedCoupons.map((coupon) => coupon.id)); - const couponsToFetch = allCoupons.filter((elem) => !this.pos.couponCache[elem]); - if (couponsToFetch.length) { - await this.pos.fetchCoupons( - [["id", "in", couponsToFetch]], - couponsToFetch.length - ); - // Remove coupons that could not be loaded from the db - this.codeActivatedCoupons = this.codeActivatedCoupons.filter( - (coupon) => this.pos.couponCache[coupon.id] - ); - this.couponPointChanges = Object.fromEntries( - Object.entries(this.couponPointChanges).filter( - ([k, pe]) => this.pos.couponCache[pe.coupon_id] - ) - ); - } - }); - } - /** - * Refreshes the currently applied rewards, if they are not applicable anymore they are removed. - */ - _updateRewardLines() { - if (!this.orderlines.length) { - return; - } - const rewardLines = this._get_reward_lines(); - if (!rewardLines.length) { - return; - } - const productRewards = []; - const otherRewards = []; - const paymentRewards = []; // Gift card and ewallet rewards are considered payments and must stay at the end - for (const line of rewardLines) { - const claimedReward = { - reward: this.pos.reward_by_id[line.reward_id], - coupon_id: line.coupon_id, - args: { - product: line.reward_product_id, - }, - }; - if ( - claimedReward.reward.program_id.program_type === "gift_card" || - claimedReward.reward.program_id.program_type === "ewallet" - ) { - paymentRewards.push(claimedReward); - } else if (claimedReward.reward.reward_type === "product") { - productRewards.push(claimedReward); - } else { - otherRewards.push(claimedReward); - } - this.orderlines.remove(line); - } - for (const claimedReward of productRewards - .concat(otherRewards) - .concat(paymentRewards)) { - // For existing coupons check that they are still claimed, they can exist in either `couponPointChanges` or `codeActivatedCoupons` - if ( - !this.codeActivatedCoupons.find( - (coupon) => coupon.id === claimedReward.coupon_id - ) && - !this.couponPointChanges[claimedReward.coupon_id] - ) { + if (totalProductQty < rule.minimum_qty) { + // Should also count the points from negative quantities. + // For example, when refunding an ewallet payment. See TicketScreen override in this addon. continue; } - this._applyReward( - claimedReward.reward, - claimedReward.coupon_id, - claimedReward.args - ); - } - } - /** - * Update our couponPointChanges, meaning the points/coupons each program give etc. - */ - async _updatePrograms() { - const changesPerProgram = {}; - const programsToCheck = new Set(); - // By default include all programs that are considered 'applicable' - for (const program of this.pos.programs) { - if (this._programIsApplicable(program)) { - programsToCheck.add(program.id); - } - } - for (const pe of Object.values(this.couponPointChanges)) { - if (!changesPerProgram[pe.program_id]) { - changesPerProgram[pe.program_id] = []; - programsToCheck.add(pe.program_id); - } - changesPerProgram[pe.program_id].push(pe); - } - for (const coupon of this.codeActivatedCoupons) { - programsToCheck.add(coupon.program_id); - } - const programs = [...programsToCheck].map( - (programId) => this.pos.program_by_id[programId] - ); - const pointsAddedPerProgram = this.pointsForPrograms(programs); - for (const program of this.pos.programs) { - // Future programs may split their points per unit paid (gift cards for example), consider a non applicable program to give no points - const pointsAdded = this._programIsApplicable(program) - ? pointsAddedPerProgram[program.id] - : []; - // For programs that apply to both (loyalty) we always add a change of 0 points, if there is none, since it makes it easier to - // track for claimable rewards, and makes sure to load the partner's loyalty card. - if (program.is_nominative && !pointsAdded.length && this.get_partner()) { - pointsAdded.push({ points: 0 }); - } - const oldChanges = changesPerProgram[program.id] || []; - // Update point changes for those that exist - for (let idx = 0; idx < Math.min(pointsAdded.length, oldChanges.length); idx++) { - Object.assign(oldChanges[idx], pointsAdded[idx]); - } - if (pointsAdded.length < oldChanges.length) { - const removedIds = oldChanges.map((pe) => pe.coupon_id); - this.couponPointChanges = Object.fromEntries( - Object.entries(this.couponPointChanges).filter(([k, pe]) => { - return !removedIds.includes(pe.coupon_id); - }) - ); - } else if (pointsAdded.length > oldChanges.length) { - for (const pa of pointsAdded.splice(oldChanges.length)) { - const coupon = await this._couponForProgram(program); - this.couponPointChanges[coupon.id] = { - points: pa.points, - program_id: program.id, - coupon_id: coupon.id, - barcode: pa.barcode, - }; - } - } - } - // Also remove coupons from codeActivatedCoupons if their program applies_on current orders and the program does not give any points - this.codeActivatedCoupons = this.codeActivatedCoupons.filter((coupon) => { - const program = this.pos.program_by_id[coupon.program_id]; if ( - program.applies_on === "current" && - pointsAddedPerProgram[program.id].length === 0 + program.applies_on === "future" && + rule.reward_point_split && + rule.reward_point_mode !== "order" ) { - return false; - } - return true; - }); - } - /** - * @typedef {{ won: number, spend: number, total: number, balance: number, name: string}} LoyaltyPoints - * @typedef {{ couponId: number, program: object, points: LoyaltyPoints}} LoyaltyStat - * @returns {Array} - */ - getLoyaltyPoints() { - // map: couponId -> LoyaltyPoints - const loyaltyPoints = {}; - for (const pointChange of Object.values(this.couponPointChanges)) { - const { coupon_id, points, program_id } = pointChange; - const program = this.pos.program_by_id[program_id]; - if (program.program_type !== "loyalty") { - // Not a loyalty program, skip - continue; - } - const loyaltyCard = this.pos.couponCache[coupon_id] || /* or new card */ { - id: coupon_id, - balance: 0, - }; - let [won, spent, total] = [0, 0, 0]; - const balance = loyaltyCard.balance; - won += points - this._getPointsCorrection(program); - if (coupon_id !== 0) { - for (const line of this._get_reward_lines()) { - if (line.coupon_id === coupon_id) { - spent += line.points_cost; - } - } - } - total = balance + won - spent; - const name = program.portal_visible ? program.portal_point_name : _t("Points"); - loyaltyPoints[coupon_id] = { - won: parseFloat(won.toFixed(2)), - spent: parseFloat(spent.toFixed(2)), - // Display total when order is ongoing. - total: parseFloat(total.toFixed(2)), - // Display balance when order is done. - balance: parseFloat(balance.toFixed(2)), - name, - program, - }; - } - return Object.entries(loyaltyPoints).map(([couponId, points]) => ({ - couponId, - points, - program: points.program, - })); - } - /** - * The points in the couponPointChanges for free product reward is not correct. - * It doesn't take into account the points from the `free` product. Use this method - * to compute the necessary correction. - * @param {*} program - * @returns {number} - */ - _getPointsCorrection(program) { - const rewardLines = this.orderlines.filter((line) => line.is_reward_line); - let res = 0; - for (const rule of program.rules) { - for (const line of rewardLines) { - const reward = this.pos.reward_by_id[line.reward_id]; - if (reward.reward_type !== "product") { - continue; - } - if (rule.reward_point_mode === "order") { - res += rule.reward_point_amount; + // In this case we count the points per rule + if (rule.reward_point_mode === "unit") { + splitPoints.push( + ...Array.apply(null, Array(totalProductQty)).map((_) => { + return { points: rule.reward_point_amount }; + }) + ); } else if (rule.reward_point_mode === "money") { - res -= round_precision( - rule.reward_point_amount * line.get_price_with_tax(), + for (const line of orderLines) { + if ( + line.is_reward_line || + !rule.valid_product_ids.has(line.get_product().id) || + line.get_quantity() <= 0 || + line.ignoreLoyaltyPoints({ program }) + ) { + continue; + } + const pointsPerUnit = round_precision( + (rule.reward_point_amount * line.get_price_with_tax()) / + line.get_quantity(), + 0.01 + ); + if (pointsPerUnit > 0) { + splitPoints.push( + ...Array.apply(null, Array(line.get_quantity())).map(() => { + if (line.giftBarcode && line.get_quantity() == 1) { + return { + points: pointsPerUnit, + barcode: line.giftBarcode, + giftCardId: line.giftCardId, + }; + } + return { points: pointsPerUnit }; + }) + ); + } + } + } + } else { + // In this case we add on to the global point count + if (rule.reward_point_mode === "order") { + points += rule.reward_point_amount; + } else if (rule.reward_point_mode === "money") { + // NOTE: unlike in sale_loyalty this performs a round half-up instead of round down + points += round_precision( + rule.reward_point_amount * orderedProductPaid, 0.01 ); } else if (rule.reward_point_mode === "unit") { - res += rule.reward_point_amount * line.get_quantity(); + points += rule.reward_point_amount * totalProductQty; } } } - return res; - } - /** - * @returns {number} The points that are left for the given coupon for this order. - */ - _getRealCouponPoints(coupon_id) { - let points = 0; - const dbCoupon = this.pos.couponCache[coupon_id]; - if (dbCoupon) { - points += dbCoupon.balance; + const res = points ? [{ points }] : []; + if (splitPoints.length) { + res.push(...splitPoints); } - Object.values(this.couponPointChanges).some((pe) => { - if (pe.coupon_id === coupon_id) { - if (this.pos.program_by_id[pe.program_id].applies_on !== "future") { - points += pe.points; - } - // couponPointChanges is not supposed to have a coupon multiple times - return true; - } - return false; - }); - for (const line of this.get_orderlines()) { - if (line.is_reward_line && line.coupon_id === coupon_id) { - points -= line.points_cost; - } - } - return points; + result[program.id] = res; } - /** - * Depending on the program type returns a new (local) instance of coupon or tries to retrieve the coupon in case of loyalty cards. - * Existing coupons are put in a cache which is also used to fetch the coupons. - * - * @param {object} program - */ - async _couponForProgram(program) { - if (program.is_nominative) { - return this.pos.fetchLoyaltyCard(program.id, this.get_partner().id); + return result; + }, + /** + * @returns {Array} List of lines composing the global discount + */ + _getGlobalDiscountLines() { + return this.get_orderlines().filter( + (line) => line.reward_id && this.pos.reward_by_id[line.reward_id].is_global_discount + ); + }, + /** + * Returns the number of product items in the order based on the given rule. + * @param {*} rule + */ + _computeNItems(rule) { + return this._get_regular_order_lines().reduce((nItems, line) => { + let increment = 0; + if (rule.any_product || rule.valid_product_ids.has(line.product.id)) { + increment = line.get_quantity(); } - // This type of coupons don't need to really exist up until validating the order, so no need to cache - return new PosLoyaltyCard( - null, - null, - program.id, - (this.get_partner() || { id: -1 }).id, - 0 - ); - } - _programIsApplicable(program) { - if ( - program.trigger === "auto" && - !program.rules.find( - (rule) => - rule.mode === "auto" || this.codeActivatedProgramRules.includes(rule.id) - ) - ) { + return nItems + increment; + }, 0); + }, + /** + * Checks whether this order is allowed to generate rewards + * from the given coupon program. + * @param {*} couponProgram + */ + _canGenerateRewards(couponProgram, orderTotalWithTax, orderTotalWithoutTax) { + for (const rule of couponProgram.rules) { + const amountToCompare = + rule.minimum_amount_tax_mode == "incl" ? orderTotalWithTax : orderTotalWithoutTax; + if (rule.minimum_amount > amountToCompare) { return false; } - if ( - program.trigger === "with_code" && - !program.rules.find((rule) => this.codeActivatedProgramRules.includes(rule.id)) - ) { + const nItems = this._computeNItems(rule); + if (rule.minimum_qty > nItems) { return false; } - if (program.is_nominative && !this.get_partner()) { - return false; - } - if (program.date_to && program.date_to <= new Date()) { - return false; - } - if (program.limit_usage && program.total_order_count >= program.max_usage) { - return false; - } - return true; } - /** - * Computes how much points each program gives. - * - * @param {Array} programs list of loyalty.program - * @returns {Object} Containing the points gained per program - */ - pointsForPrograms(programs) { - const totalTaxed = this.get_total_with_tax(); - const totalUntaxed = this.get_total_without_tax(); - const totalsPerProgram = Object.fromEntries( - programs.map((program) => [ - program.id, - { untaxed: totalUntaxed, taxed: totalTaxed }, - ]) - ); - const orderLines = this.get_orderlines(); - for (const line of orderLines) { - if (!line.reward_id) { - continue; - } - const reward = this.pos.reward_by_id[line.reward_id]; - if (reward.reward_type !== "discount") { - continue; - } - const rewardProgram = reward.program_id; - for (const program of programs) { - // Remove automatic discount and this program's discounts from the totals. - if (program.id === rewardProgram.id || rewardProgram.trigger === "auto") { - totalsPerProgram[program.id]["taxed"] -= line.get_price_with_tax(); - totalsPerProgram[program.id]["untaxed"] -= line.get_price_without_tax(); - } - } - } - const result = {}; - for (const program of programs) { - let points = 0; - const splitPoints = []; - for (const rule of program.rules) { - if ( - rule.mode === "with_code" && - !this.codeActivatedProgramRules.includes(rule.id) - ) { - continue; - } - const amountCheck = - (rule.minimum_amount_tax_mode === "incl" && - totalsPerProgram[program.id]["taxed"]) || - totalsPerProgram[program.id]["untaxed"]; - if (rule.minimum_amount > amountCheck) { - // NOTE: big doutes par rapport au fait de compter tous les produits - continue; - } - let totalProductQty = 0; - // Only count points for paid lines. - const qtyPerProduct = {}; - let orderedProductPaid = 0; - for (const line of orderLines) { - if ( - ((!line.reward_product_id && - (rule.any_product || - rule.valid_product_ids.has(line.get_product().id))) || - (line.reward_product_id && - (rule.any_product || - rule.valid_product_ids.has(line.reward_product_id)))) && - !line.ignoreLoyaltyPoints({ program }) - ) { - // We only count reward products from the same program to avoid unwanted feedback loops - if (line.reward_product_id) { - const reward = this.pos.reward_by_id[line.reward_id]; - if (program.id !== reward.program_id) { - continue; - } - } - const lineQty = line.reward_product_id - ? -line.get_quantity() - : line.get_quantity(); - totalProductQty += lineQty; - if (qtyPerProduct[line.reward_product_id || line.get_product().id]) { - qtyPerProduct[line.reward_product_id || line.get_product().id] += - lineQty; - } else { - qtyPerProduct[line.reward_product_id || line.get_product().id] = - lineQty; - } - orderedProductPaid += line.get_price_with_tax(); - } - } - if (totalProductQty < rule.minimum_qty) { - // Should also count the points from negative quantities. - // For example, when refunding an ewallet payment. See TicketScreen override in this addon. - continue; - } - if ( - program.applies_on === "future" && - rule.reward_point_split && - rule.reward_point_mode !== "order" - ) { - // In this case we count the points per rule - if (rule.reward_point_mode === "unit") { - splitPoints.push( - ...Array.apply(null, Array(totalProductQty)).map((_) => { - return { points: rule.reward_point_amount }; - }) - ); - } else if (rule.reward_point_mode === "money") { - for (const line of orderLines) { - if ( - line.is_reward_line || - !rule.valid_product_ids.has(line.get_product().id) || - line.get_quantity() <= 0 || - line.ignoreLoyaltyPoints({ program }) - ) { - continue; - } - const pointsPerUnit = round_precision( - (rule.reward_point_amount * line.get_price_with_tax()) / - line.get_quantity(), - 0.01 - ); - if (pointsPerUnit > 0) { - splitPoints.push( - ...Array.apply(null, Array(line.get_quantity())).map(() => { - if (line.giftBarcode && line.get_quantity() == 1) { - return { - points: pointsPerUnit, - barcode: line.giftBarcode, - giftCardId: line.giftCardId, - }; - } - return { points: pointsPerUnit }; - }) - ); - } - } - } - } else { - // In this case we add on to the global point count - if (rule.reward_point_mode === "order") { - points += rule.reward_point_amount; - } else if (rule.reward_point_mode === "money") { - // NOTE: unlike in sale_loyalty this performs a round half-up instead of round down - points += round_precision( - rule.reward_point_amount * orderedProductPaid, - 0.01 - ); - } else if (rule.reward_point_mode === "unit") { - points += rule.reward_point_amount * totalProductQty; - } - } - } - const res = points ? [{ points }] : []; - if (splitPoints.length) { - res.push(...splitPoints); - } - result[program.id] = res; - } - return result; - } - /** - * @returns {Array} List of lines composing the global discount - */ - _getGlobalDiscountLines() { - return this.get_orderlines().filter( - (line) => line.reward_id && this.pos.reward_by_id[line.reward_id].is_global_discount - ); - } - /** - * Returns the number of product items in the order based on the given rule. - * @param {*} rule - */ - _computeNItems(rule) { - return this._get_regular_order_lines().reduce((nItems, line) => { - let increment = 0; - if (rule.any_product || rule.valid_product_ids.has(line.product.id)) { - increment = line.get_quantity(); - } - return nItems + increment; - }, 0); - } - /** - * Checks whether this order is allowed to generate rewards - * from the given coupon program. - * @param {*} couponProgram - */ - _canGenerateRewards(couponProgram, orderTotalWithTax, orderTotalWithoutTax) { - for (const rule of couponProgram.rules) { - const amountToCompare = - rule.minimum_amount_tax_mode == "incl" - ? orderTotalWithTax - : orderTotalWithoutTax; - if (rule.minimum_amount > amountToCompare) { - return false; - } - const nItems = this._computeNItems(rule); - if (rule.minimum_qty > nItems) { - return false; - } - } - return true; - } - /** - * @param {Integer} coupon_id (optional) Coupon id - * @param {Integer} program_id (optional) Program id - * @returns {Array} List of {Object} containing the coupon_id and reward keys - */ - getClaimableRewards(coupon_id = false, program_id = false, auto = false) { - const allCouponPrograms = Object.values(this.couponPointChanges) - .map((pe) => { + return true; + }, + /** + * @param {Integer} coupon_id (optional) Coupon id + * @param {Integer} program_id (optional) Program id + * @returns {Array} List of {Object} containing the coupon_id and reward keys + */ + getClaimableRewards(coupon_id = false, program_id = false, auto = false) { + const allCouponPrograms = Object.values(this.couponPointChanges) + .map((pe) => { + return { + program_id: pe.program_id, + coupon_id: pe.coupon_id, + }; + }) + .concat( + this.codeActivatedCoupons.map((coupon) => { return { - program_id: pe.program_id, - coupon_id: pe.coupon_id, + program_id: coupon.program_id, + coupon_id: coupon.id, }; }) - .concat( - this.codeActivatedCoupons.map((coupon) => { - return { - program_id: coupon.program_id, - coupon_id: coupon.id, - }; - }) - ); - const result = []; - const totalWithTax = this.get_total_with_tax(); - const totalWithoutTax = this.get_total_without_tax(); - const totalIsZero = totalWithTax === 0; - const globalDiscountLines = this._getGlobalDiscountLines(); - const globalDiscountPercent = globalDiscountLines.length - ? this.pos.reward_by_id[globalDiscountLines[0].reward_id].discount - : 0; - for (const couponProgram of allCouponPrograms) { - const program = this.pos.program_by_id[couponProgram.program_id]; - if (program.trigger == "with_code") { - // For coupon programs, the rules become conditions. - // Points to purchase rewards will only come from the scanned coupon. - if (!this._canGenerateRewards(program, totalWithTax, totalWithoutTax)) { - continue; - } - } - if ( - (coupon_id && couponProgram.coupon_id !== coupon_id) || - (program_id && couponProgram.program_id !== program_id) - ) { + ); + const result = []; + const totalWithTax = this.get_total_with_tax(); + const totalWithoutTax = this.get_total_without_tax(); + const totalIsZero = totalWithTax === 0; + const globalDiscountLines = this._getGlobalDiscountLines(); + const globalDiscountPercent = globalDiscountLines.length + ? this.pos.reward_by_id[globalDiscountLines[0].reward_id].discount + : 0; + for (const couponProgram of allCouponPrograms) { + const program = this.pos.program_by_id[couponProgram.program_id]; + if (program.trigger == "with_code") { + // For coupon programs, the rules become conditions. + // Points to purchase rewards will only come from the scanned coupon. + if (!this._canGenerateRewards(program, totalWithTax, totalWithoutTax)) { continue; } - const points = this._getRealCouponPoints(couponProgram.coupon_id); - for (const reward of program.rewards) { - if (points < reward.required_points) { + } + if ( + (coupon_id && couponProgram.coupon_id !== coupon_id) || + (program_id && couponProgram.program_id !== program_id) + ) { + continue; + } + const points = this._getRealCouponPoints(couponProgram.coupon_id); + for (const reward of program.rewards) { + if (points < reward.required_points) { + continue; + } + if (auto && this.disabledRewards.has(reward.id)) { + continue; + } + // Try to filter out rewards that will not be claimable anyway. + if (reward.is_global_discount && reward.discount <= globalDiscountPercent) { + continue; + } + if (reward.reward_type === "discount" && totalIsZero) { + continue; + } + if (reward.reward_type === "product" && !reward.multi_product) { + const product = this.pos.db.get_product_by_id(reward.reward_product_ids[0]); + const unclaimedQty = this._computeUnclaimedFreeProductQty( + reward, + couponProgram.coupon_id, + product, + points + ); + if (unclaimedQty <= 0) { continue; } - if (auto && this.disabledRewards.has(reward.id)) { + } + result.push({ + coupon_id: couponProgram.coupon_id, + reward: reward, + }); + } + } + return result; + }, + /** + * Returns the reward such that when its reward product is added + * in the order, it will be added as free. That is, when added, + * it comes with the corresponding reward product line. + */ + getPotentialFreeProductRewards() { + const allCouponPrograms = Object.values(this.couponPointChanges) + .map((pe) => { + return { + program_id: pe.program_id, + coupon_id: pe.coupon_id, + }; + }) + .concat( + this.codeActivatedCoupons.map((coupon) => { + return { + program_id: coupon.program_id, + coupon_id: coupon.id, + }; + }) + ); + const result = []; + for (const couponProgram of allCouponPrograms) { + const program = this.pos.program_by_id[couponProgram.program_id]; + const points = this._getRealCouponPoints(couponProgram.coupon_id); + const hasLine = this.orderlines.filter((line) => !line.is_reward_line).length > 0; + for (const reward of program.rewards.filter( + (reward) => reward.reward_type == "product" + )) { + if (points < reward.required_points) { + continue; + } + // Loyalty program (applies_on == 'both') should needs an orderline before it can apply a reward. + const considerTheReward = + program.applies_on !== "both" || (program.applies_on == "both" && hasLine); + if (reward.reward_type === "product" && considerTheReward) { + const product = this.pos.db.get_product_by_id(reward.reward_product_ids[0]); + const potentialQty = this._computePotentialFreeProductQty( + reward, + product, + points + ); + if (potentialQty <= 0) { continue; } - // Try to filter out rewards that will not be claimable anyway. - if (reward.is_global_discount && reward.discount <= globalDiscountPercent) { - continue; - } - if (reward.reward_type === "discount" && totalIsZero) { - continue; - } - if (reward.reward_type === "product" && !reward.multi_product) { - const product = this.pos.db.get_product_by_id(reward.reward_product_ids[0]); - const unclaimedQty = this._computeUnclaimedFreeProductQty( - reward, - couponProgram.coupon_id, - product, - points - ); - if (unclaimedQty <= 0) { - continue; - } - } result.push({ coupon_id: couponProgram.coupon_id, reward: reward, + potentialQty, }); } } - return result; } - /** - * Returns the reward such that when its reward product is added - * in the order, it will be added as free. That is, when added, - * it comes with the corresponding reward product line. - */ - getPotentialFreeProductRewards() { - const allCouponPrograms = Object.values(this.couponPointChanges) - .map((pe) => { - return { - program_id: pe.program_id, - coupon_id: pe.coupon_id, - }; - }) - .concat( - this.codeActivatedCoupons.map((coupon) => { - return { - program_id: coupon.program_id, - coupon_id: coupon.id, - }; - }) - ); - const result = []; - for (const couponProgram of allCouponPrograms) { - const program = this.pos.program_by_id[couponProgram.program_id]; - const points = this._getRealCouponPoints(couponProgram.coupon_id); - const hasLine = this.orderlines.filter((line) => !line.is_reward_line).length > 0; - for (const reward of program.rewards.filter( - (reward) => reward.reward_type == "product" - )) { - if (points < reward.required_points) { + return result; + }, + /** + * Applies a reward to the order, `_updateRewards` is expected to be called right after. + * + * @param {loyalty.reward} reward + * @param {Integer} coupon_id + * @param {Object} args Reward options + * @returns True if everything went right or an error message + */ + _applyReward(reward, coupon_id, args) { + if (this._getRealCouponPoints(coupon_id) < reward.required_points) { + return _t("There are not enough points on the coupon to claim this reward."); + } + if (reward.is_global_discount) { + const globalDiscountLines = this._getGlobalDiscountLines(); + if (globalDiscountLines.length) { + const rewardId = globalDiscountLines[0].reward_id; + if ( + rewardId != reward.id && + this.pos.reward_by_id[rewardId].discount >= reward.discount + ) { + return _t("A better global discount is already applied."); + } else if (rewardId != rewardId.id) { + for (const line of globalDiscountLines) { + this.orderlines.remove(line); + } + } + } + } + args = args || {}; + const rewardLines = this._getRewardLineValues({ + reward: reward, + coupon_id: coupon_id, + product: args["product"] || null, + }); + if (!Array.isArray(rewardLines)) { + return rewardLines; // Returned an error. + } + if (!rewardLines.length) { + return _t("The reward could not be applied."); + } + for (const rewardLine of rewardLines) { + this.orderlines.add(this._createLineFromVals(rewardLine)); + } + return true; + }, + _createLineFromVals(vals) { + vals["lst_price"] = vals["price"]; + const line = new Orderline({}, { pos: this.pos, order: this, product: vals["product"] }); + this.fix_tax_included_price(line); + this.set_orderline_options(line, vals); + return line; + }, + /** + * @param {loyalty.reward} reward + * @returns the discountable and discountable per tax for this discount on order reward. + */ + _getDiscountableOnOrder(reward) { + let discountable = 0; + const discountablePerTax = {}; + for (const line of this.get_orderlines()) { + if (!line.get_quantity()) { + continue; + } + const taxKey = line.get_taxes().map((t) => t.id); + discountable += line.get_price_with_tax(); + if (!discountablePerTax[taxKey]) { + discountablePerTax[taxKey] = 0; + } + discountablePerTax[taxKey] += line.get_base_price(); + } + return { discountable, discountablePerTax }; + }, + /** + * @returns the order's cheapest line + */ + _getCheapestLine() { + let cheapestLine; + for (const line of this.get_orderlines()) { + if (line.reward_id || !line.get_quantity()) { + continue; + } + if (!cheapestLine || cheapestLine.price > line.price) { + cheapestLine = line; + } + } + return cheapestLine; + }, + /** + * @param {loyalty.reward} reward + * @returns the discountable and discountable per tax for this discount on cheapest reward. + */ + _getDiscountableOnCheapest(reward) { + const cheapestLine = this._getCheapestLine(); + if (!cheapestLine) { + return { discountable: 0, discountablePerTax: {} }; + } + const taxKey = cheapestLine.get_taxes().map((t) => t.id); + return { + discountable: cheapestLine.price, + discountablePerTax: Object.fromEntries([[taxKey, cheapestLine.price]]), + }; + }, + /** + * @param {loyalty.reward} reward + * @returns all lines to which the reward applies. + */ + _getSpecificDiscountableLines(reward) { + const discountableLines = []; + const applicableProducts = reward.all_discount_product_ids; + for (const line of this.get_orderlines()) { + if (!line.get_quantity()) { + continue; + } + if ( + applicableProducts.has(line.get_product().id) || + applicableProducts.has(line.reward_product_id) + ) { + discountableLines.push(line); + } + } + return discountableLines; + }, + /** + * For a 'specific' type of discount it is more complicated as we have to make sure that we never + * discount more than what is available on a per line basis. + * @param {loyalty.reward} reward + * @returns the discountable and discountable per tax for this discount on specific reward. + */ + _getDiscountableOnSpecific(reward) { + const applicableProducts = reward.all_discount_product_ids; + const linesToDiscount = []; + const discountLinesPerReward = {}; + const orderLines = this.get_orderlines(); + const remainingAmountPerLine = {}; + for (const line of orderLines) { + if (!line.get_quantity() || !line.price) { + continue; + } + remainingAmountPerLine[line.cid] = line.get_price_with_tax(); + if ( + applicableProducts.has(line.get_product().id) || + (line.reward_product_id && applicableProducts.has(line.reward_product_id)) + ) { + linesToDiscount.push(line); + } else if (line.reward_id) { + const lineReward = this.pos.reward_by_id[line.reward_id]; + if (lineReward.id === reward.id) { + continue; + } + if (!discountLinesPerReward[line.reward_identifier_code]) { + discountLinesPerReward[line.reward_identifier_code] = []; + } + discountLinesPerReward[line.reward_identifier_code].push(line); + } + } + + let cheapestLine = false; + for (const lines of Object.values(discountLinesPerReward)) { + const lineReward = this.pos.reward_by_id[lines[0].reward_id]; + let discountedLines = orderLines; + if (lineReward.discount_applicability === "cheapest") { + cheapestLine = cheapestLine || this._getCheapestLine(); + discountedLines = [cheapestLine]; + } else if (lineReward.discount_applicability === "specific") { + discountedLines = this._getSpecificDiscountableLines(lineReward); + } + if (!discountedLines.length) { + continue; + } + const commonLines = linesToDiscount.filter((line) => discountedLines.includes(line)); + if (lineReward.discount_mode === "percent") { + const discount = lineReward.discount / 100; + for (const line of discountedLines) { + if (line.reward_id) { continue; } - // Loyalty program (applies_on == 'both') should needs an orderline before it can apply a reward. - const considerTheReward = - program.applies_on !== "both" || (program.applies_on == "both" && hasLine); - if (reward.reward_type === "product" && considerTheReward) { - const product = this.pos.db.get_product_by_id(reward.reward_product_ids[0]); - const potentialQty = this._computePotentialFreeProductQty( - reward, - product, - points - ); - if (potentialQty <= 0) { - continue; - } - result.push({ - coupon_id: couponProgram.coupon_id, - reward: reward, - potentialQty, - }); + if (lineReward.discount_applicability === "cheapest") { + remainingAmountPerLine[line.cid] *= 1 - discount / line.get_quantity(); + } else { + remainingAmountPerLine[line.cid] *= 1 - discount; } } - } - return result; - } - /** - * Applies a reward to the order, `_updateRewards` is expected to be called right after. - * - * @param {loyalty.reward} reward - * @param {Integer} coupon_id - * @param {Object} args Reward options - * @returns True if everything went right or an error message - */ - _applyReward(reward, coupon_id, args) { - if (this._getRealCouponPoints(coupon_id) < reward.required_points) { - return _t("There are not enough points on the coupon to claim this reward."); - } - if (reward.is_global_discount) { - const globalDiscountLines = this._getGlobalDiscountLines(); - if (globalDiscountLines.length) { - const rewardId = globalDiscountLines[0].reward_id; - if ( - rewardId != reward.id && - this.pos.reward_by_id[rewardId].discount >= reward.discount - ) { - return _t("A better global discount is already applied."); - } else if (rewardId != rewardId.id) { - for (const line of globalDiscountLines) { - this.orderlines.remove(line); - } - } - } - } - args = args || {}; - const rewardLines = this._getRewardLineValues({ - reward: reward, - coupon_id: coupon_id, - product: args["product"] || null, - }); - if (!Array.isArray(rewardLines)) { - return rewardLines; // Returned an error. - } - if (!rewardLines.length) { - return _t("The reward could not be applied."); - } - for (const rewardLine of rewardLines) { - this.orderlines.add(this._createLineFromVals(rewardLine)); - } - return true; - } - _createLineFromVals(vals) { - vals["lst_price"] = vals["price"]; - const line = Orderline.create( - {}, - { pos: this.pos, order: this, product: vals["product"] } - ); - this.fix_tax_included_price(line); - this.set_orderline_options(line, vals); - return line; - } - /** - * @param {loyalty.reward} reward - * @returns the discountable and discountable per tax for this discount on order reward. - */ - _getDiscountableOnOrder(reward) { - let discountable = 0; - const discountablePerTax = {}; - for (const line of this.get_orderlines()) { - if (!line.get_quantity()) { - continue; - } - const taxKey = line.get_taxes().map((t) => t.id); - discountable += line.get_price_with_tax(); - if (!discountablePerTax[taxKey]) { - discountablePerTax[taxKey] = 0; - } - discountablePerTax[taxKey] += line.get_base_price(); - } - return { discountable, discountablePerTax }; - } - /** - * @returns the order's cheapest line - */ - _getCheapestLine() { - let cheapestLine; - for (const line of this.get_orderlines()) { - if (line.reward_id || !line.get_quantity()) { - continue; - } - if (!cheapestLine || cheapestLine.price > line.price) { - cheapestLine = line; - } - } - return cheapestLine; - } - /** - * @param {loyalty.reward} reward - * @returns the discountable and discountable per tax for this discount on cheapest reward. - */ - _getDiscountableOnCheapest(reward) { - const cheapestLine = this._getCheapestLine(); - if (!cheapestLine) { - return { discountable: 0, discountablePerTax: {} }; - } - const taxKey = cheapestLine.get_taxes().map((t) => t.id); - return { - discountable: cheapestLine.price, - discountablePerTax: Object.fromEntries([[taxKey, cheapestLine.price]]), - }; - } - /** - * @param {loyalty.reward} reward - * @returns all lines to which the reward applies. - */ - _getSpecificDiscountableLines(reward) { - const discountableLines = []; - const applicableProducts = reward.all_discount_product_ids; - for (const line of this.get_orderlines()) { - if (!line.get_quantity()) { - continue; - } - if ( - applicableProducts.has(line.get_product().id) || - applicableProducts.has(line.reward_product_id) - ) { - discountableLines.push(line); - } - } - return discountableLines; - } - /** - * For a 'specific' type of discount it is more complicated as we have to make sure that we never - * discount more than what is available on a per line basis. - * @param {loyalty.reward} reward - * @returns the discountable and discountable per tax for this discount on specific reward. - */ - _getDiscountableOnSpecific(reward) { - const applicableProducts = reward.all_discount_product_ids; - const linesToDiscount = []; - const discountLinesPerReward = {}; - const orderLines = this.get_orderlines(); - const remainingAmountPerLine = {}; - for (const line of orderLines) { - if (!line.get_quantity() || !line.price) { - continue; - } - remainingAmountPerLine[line.cid] = line.get_price_with_tax(); - if ( - applicableProducts.has(line.get_product().id) || - (line.reward_product_id && applicableProducts.has(line.reward_product_id)) - ) { - linesToDiscount.push(line); - } else if (line.reward_id) { - const lineReward = this.pos.reward_by_id[line.reward_id]; - if (lineReward.id === reward.id) { - continue; - } - if (!discountLinesPerReward[line.reward_identifier_code]) { - discountLinesPerReward[line.reward_identifier_code] = []; - } - discountLinesPerReward[line.reward_identifier_code].push(line); - } - } - - let cheapestLine = false; - for (const lines of Object.values(discountLinesPerReward)) { - const lineReward = this.pos.reward_by_id[lines[0].reward_id]; - let discountedLines = orderLines; - if (lineReward.discount_applicability === "cheapest") { - cheapestLine = cheapestLine || this._getCheapestLine(); - discountedLines = [cheapestLine]; - } else if (lineReward.discount_applicability === "specific") { - discountedLines = this._getSpecificDiscountableLines(lineReward); - } - if (!discountedLines.length) { - continue; - } - const commonLines = linesToDiscount.filter((line) => - discountedLines.includes(line) + } else { + const nonCommonLines = discountedLines.filter( + (line) => !linesToDiscount.includes(line) ); - if (lineReward.discount_mode === "percent") { - const discount = lineReward.discount / 100; - for (const line of discountedLines) { - if (line.reward_id) { - continue; - } - if (lineReward.discount_applicability === "cheapest") { - remainingAmountPerLine[line.cid] *= 1 - discount / line.get_quantity(); - } else { - remainingAmountPerLine[line.cid] *= 1 - discount; - } + const discountedAmounts = lines.reduce((map, line) => { + map[line.get_taxes().map((t) => t.id)]; + return map; + }, {}); + const process = (line) => { + const key = line.get_taxes().map((t) => t.id); + if (!discountedAmounts[key] || line.reward_id) { + return; } - } else { - const nonCommonLines = discountedLines.filter( - (line) => !linesToDiscount.includes(line) - ); - const discountedAmounts = lines.reduce((map, line) => { - map[line.get_taxes().map((t) => t.id)]; - return map; - }, {}); - const process = (line) => { - const key = line.get_taxes().map((t) => t.id); - if (!discountedAmounts[key] || line.reward_id) { - return; - } - const remaining = remainingAmountPerLine[line.cid]; - const consumed = Math.min(remaining, discountedAmounts[key]); - discountedAmounts[key] -= consumed; - remainingAmountPerLine[line.cid] -= consumed; - }; - nonCommonLines.forEach(process); - commonLines.forEach(process); - } + const remaining = remainingAmountPerLine[line.cid]; + const consumed = Math.min(remaining, discountedAmounts[key]); + discountedAmounts[key] -= consumed; + remainingAmountPerLine[line.cid] -= consumed; + }; + nonCommonLines.forEach(process); + commonLines.forEach(process); } - - let discountable = 0; - const discountablePerTax = {}; - for (const line of linesToDiscount) { - discountable += remainingAmountPerLine[line.cid]; - const taxKey = line.get_taxes().map((t) => t.id); - if (!discountablePerTax[taxKey]) { - discountablePerTax[taxKey] = 0; - } - discountablePerTax[taxKey] += - line.get_base_price() * - (remainingAmountPerLine[line.cid] / line.get_price_with_tax()); - } - return { discountable, discountablePerTax }; } - /** - * @param {Object} args See `_applyReward` - * @returns {Array} List of values to create the reward lines - */ - _getRewardLineValues(args) { - const reward = args["reward"]; - if (reward.reward_type === "discount") { - return this._getRewardLineValuesDiscount(args); - } else if (reward.reward_type === "product") { - return this._getRewardLineValuesProduct(args); + + let discountable = 0; + const discountablePerTax = {}; + for (const line of linesToDiscount) { + discountable += remainingAmountPerLine[line.cid]; + const taxKey = line.get_taxes().map((t) => t.id); + if (!discountablePerTax[taxKey]) { + discountablePerTax[taxKey] = 0; } - // NOTE: we may reach this step if for some reason there is a free shipping reward + discountablePerTax[taxKey] += + line.get_base_price() * + (remainingAmountPerLine[line.cid] / line.get_price_with_tax()); + } + return { discountable, discountablePerTax }; + }, + /** + * @param {Object} args See `_applyReward` + * @returns {Array} List of values to create the reward lines + */ + _getRewardLineValues(args) { + const reward = args["reward"]; + if (reward.reward_type === "discount") { + return this._getRewardLineValuesDiscount(args); + } else if (reward.reward_type === "product") { + return this._getRewardLineValuesProduct(args); + } + // NOTE: we may reach this step if for some reason there is a free shipping reward + return []; + }, + /** + * @param {Object} args See `_applyReward` + * @returns {Array} List of values to create the discount lines + */ + _getRewardLineValuesDiscount(args) { + const reward = args["reward"]; + const coupon_id = args["coupon_id"]; + const rewardAppliesTo = reward.discount_applicability; + let getDiscountable; + if (rewardAppliesTo === "order") { + getDiscountable = this._getDiscountableOnOrder.bind(this); + } else if (rewardAppliesTo === "cheapest") { + getDiscountable = this._getDiscountableOnCheapest.bind(this); + } else if (rewardAppliesTo === "specific") { + getDiscountable = this._getDiscountableOnSpecific.bind(this); + } + if (!getDiscountable) { + return _t("Unknown discount type"); + } + let { discountable, discountablePerTax } = getDiscountable(reward); + discountable = Math.min(this.get_total_with_tax(), discountable); + if (!discountable) { return []; } - /** - * @param {Object} args See `_applyReward` - * @returns {Array} List of values to create the discount lines - */ - _getRewardLineValuesDiscount(args) { - const reward = args["reward"]; - const coupon_id = args["coupon_id"]; - const rewardAppliesTo = reward.discount_applicability; - let getDiscountable; - if (rewardAppliesTo === "order") { - getDiscountable = this._getDiscountableOnOrder.bind(this); - } else if (rewardAppliesTo === "cheapest") { - getDiscountable = this._getDiscountableOnCheapest.bind(this); - } else if (rewardAppliesTo === "specific") { - getDiscountable = this._getDiscountableOnSpecific.bind(this); - } - if (!getDiscountable) { - return _t("Unknown discount type"); - } - let { discountable, discountablePerTax } = getDiscountable(reward); - discountable = Math.min(this.get_total_with_tax(), discountable); - if (!discountable) { - return []; - } - let maxDiscount = reward.discount_max_amount || Infinity; - if (reward.discount_mode === "per_point") { - maxDiscount = Math.min( - maxDiscount, - reward.discount * this._getRealCouponPoints(coupon_id) - ); - } else if (reward.discount_mode === "per_order") { - maxDiscount = Math.min(maxDiscount, reward.discount); - } else if (reward.discount_mode === "percent") { - maxDiscount = Math.min(maxDiscount, discountable * (reward.discount / 100)); - } - const rewardCode = _newRandomRewardCode(); - let pointCost = reward.clear_wallet - ? this._getRealCouponPoints(coupon_id) - : reward.required_points; - if (reward.discount_mode === "per_point" && !reward.clear_wallet) { - pointCost = Math.min(maxDiscount, discountable) / reward.discount; - } - // These are considered payments and do not require to be either taxed or split by tax - const discountProduct = reward.discount_line_product_id; - if (["ewallet", "gift_card"].includes(reward.program_id.program_type)) { - return [ - { - product: discountProduct, - price: -Math.min(maxDiscount, discountable), - quantity: 1, - reward_id: reward.id, - is_reward_line: true, - coupon_id: coupon_id, - points_cost: pointCost, - reward_identifier_code: rewardCode, - merge: false, - }, - ]; - } - const discountFactor = discountable ? Math.min(1, maxDiscount / discountable) : 1; - const result = Object.entries(discountablePerTax).reduce((lst, entry) => { - // Ignore 0 price lines - if (!entry[1]) { - return; - } - const taxIds = - entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str)); - lst.push({ + let maxDiscount = reward.discount_max_amount || Infinity; + if (reward.discount_mode === "per_point") { + maxDiscount = Math.min( + maxDiscount, + reward.discount * this._getRealCouponPoints(coupon_id) + ); + } else if (reward.discount_mode === "per_order") { + maxDiscount = Math.min(maxDiscount, reward.discount); + } else if (reward.discount_mode === "percent") { + maxDiscount = Math.min(maxDiscount, discountable * (reward.discount / 100)); + } + const rewardCode = _newRandomRewardCode(); + let pointCost = reward.clear_wallet + ? this._getRealCouponPoints(coupon_id) + : reward.required_points; + if (reward.discount_mode === "per_point" && !reward.clear_wallet) { + pointCost = Math.min(maxDiscount, discountable) / reward.discount; + } + // These are considered payments and do not require to be either taxed or split by tax + const discountProduct = reward.discount_line_product_id; + if (["ewallet", "gift_card"].includes(reward.program_id.program_type)) { + return [ + { product: discountProduct, - price: -(entry[1] * discountFactor), + price: -Math.min(maxDiscount, discountable), quantity: 1, reward_id: reward.id, is_reward_line: true, coupon_id: coupon_id, - points_cost: 0, + points_cost: pointCost, reward_identifier_code: rewardCode, - tax_ids: taxIds, - merge: false, - }); - return lst; - }, []); - if (result.length) { - result[0]["points_cost"] = pointCost; - } - return result; - } - _isRewardProductPartOfRules(reward, product) { - return ( - reward.program_id.rules.filter( - (rule) => rule.any_product || rule.valid_product_ids.has(product.id) - ).length > 0 - ); - } - /** - * Tries to compute how many free product can be given out for the given product. - * Contrary to sale_loyalty, the product must be in the order lines in order to give it out - * (resulting in discount lines for the product's value). - * As such we need to approximate the effect of removing 1 quantity on the counting of points in order - * to avoid feedback loops between giving a product and it removing the required points for it. - * - * @param {loyalty.reward} reward - * @param {Integer} coupon_id - * @param {Product} product - * @returns {Integer} Available quantity to be given as reward for the given product - */ - _computeUnclaimedFreeProductQty(reward, coupon_id, product, remainingPoints) { - let claimed = 0; - let available = 0; - let shouldCorrectRemainingPoints = false; - for (const line of this.get_orderlines()) { - if (line.get_product().id === product.id) { - available += line.get_quantity(); - } else if (line.reward_product_id === product.id) { - if (line.reward_id == reward.id) { - claimed += line.get_quantity(); - } else { - shouldCorrectRemainingPoints = true; - } - } - } - let freeQty; - if (reward.program_id.trigger == "auto") { - if (this._isRewardProductPartOfRules(reward, product)) { - // OPTIMIZATION: Pre-calculate the factors for each reward-product combination during the loading. - // For points not based on quantity, need to normalize the points to compute free quantity. - const factor = reward.program_id.rules.reduce((result, rule) => { - if (rule.any_product || rule.valid_product_ids.has(product.id)) { - if (rule.reward_point_mode === "order") { - result += rule.reward_point_amount; - } else if (rule.reward_point_mode === "money") { - result += round_precision( - rule.reward_point_amount * product.lst_price, - 0.01 - ); - } else if (rule.reward_point_mode === "unit") { - result += rule.reward_point_amount; - } - } - return result; - }, 0); - const correction = shouldCorrectRemainingPoints - ? this._getPointsCorrection(reward.program_id) - : 0; - freeQty = computeFreeQuantity( - (remainingPoints - correction) / factor, - reward.required_points / factor, - reward.reward_product_qty - ); - } else { - freeQty = Math.floor( - (remainingPoints / reward.required_points) * reward.reward_product_qty - ); - } - } else if (reward.program_id.trigger == "with_code") { - freeQty = Math.floor( - (remainingPoints / reward.required_points) * reward.reward_product_qty - ); - } - return Math.min(available, freeQty) - claimed; - } - _computePotentialFreeProductQty(reward, product, remainingPoints) { - if (reward.program_id.trigger == "auto") { - if (this._isRewardProductPartOfRules(reward, product)) { - const line = this.get_orderlines().find( - (line) => line.reward_product_id === product.id - ); - // Compute the correction points once even if there are multiple reward lines. - // This is because _getPointsCorrection is taking into account all the lines already. - const claimedPoints = line ? this._getPointsCorrection(reward.program_id) : 0; - return Math.floor( - ((remainingPoints - claimedPoints) / reward.required_points) * - reward.reward_product_qty - ); - } else { - return Math.floor( - (remainingPoints / reward.required_points) * reward.reward_product_qty - ); - } - } else if (reward.program_id.trigger == "with_code") { - return Math.floor( - (remainingPoints / reward.required_points) * reward.reward_product_qty - ); - } - } - /** - * @param {Object} args See `_applyReward` - * @returns {Array} List of values to create the reward lines - */ - _getRewardLineValuesProduct(args) { - const reward = args["reward"]; - const product = this.pos.db.get_product_by_id( - args["product"] || reward.reward_product_ids[0] - ); - const points = this._getRealCouponPoints(args["coupon_id"]); - const unclaimedQty = this._computeUnclaimedFreeProductQty( - reward, - args["coupon_id"], - product, - points - ); - if (unclaimedQty <= 0) { - return _t("There are not enough products in the basket to claim this reward."); - } - const claimable_count = reward.clear_wallet - ? 1 - : Math.min( - Math.ceil(unclaimedQty / reward.reward_product_qty), - Math.floor(points / reward.required_points) - ); - const cost = reward.clear_wallet ? points : claimable_count * reward.required_points; - // In case the reward is the product multiple times, give it as many times as possible - const freeQuantity = Math.min( - unclaimedQty, - reward.reward_product_qty * claimable_count - ); - return [ - { - product: reward.discount_line_product_id, - price: -round_decimals( - product.get_price(this.pricelist, freeQuantity), - this.pos.currency.decimal_places - ), - tax_ids: product.taxes_id, - quantity: freeQuantity, - reward_id: reward.id, - is_reward_line: true, - reward_product_id: product.id, - coupon_id: args["coupon_id"], - points_cost: cost, - reward_identifier_code: _newRandomRewardCode(), merge: false, }, ]; } - /** - * Full routine for activating a code for the order. - * If only one more reward is claimable after activating the code, that reward is claimed - * directly, to avoid more steps than necessary. - * If more rewards are claimable, the employee will have to manually select the reward - * in the reward selection menu. - * - * @param {String} code - * @returns true if everything went right, error message if not. - */ - async _activateCode(code) { - const rule = this.pos.rules.find((rule) => { - return ( - rule.mode === "with_code" && (rule.promo_barcode === code || rule.code === code) - ); + const discountFactor = discountable ? Math.min(1, maxDiscount / discountable) : 1; + const result = Object.entries(discountablePerTax).reduce((lst, entry) => { + // Ignore 0 price lines + if (!entry[1]) { + return; + } + const taxIds = entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str)); + lst.push({ + product: discountProduct, + price: -(entry[1] * discountFactor), + quantity: 1, + reward_id: reward.id, + is_reward_line: true, + coupon_id: coupon_id, + points_cost: 0, + reward_identifier_code: rewardCode, + tax_ids: taxIds, + merge: false, }); - let claimableRewards = null; - if (rule) { - if (this.codeActivatedProgramRules.includes(rule.id)) { - return _t("That promo code program has already been activated."); + return lst; + }, []); + if (result.length) { + result[0]["points_cost"] = pointCost; + } + return result; + }, + _isRewardProductPartOfRules(reward, product) { + return ( + reward.program_id.rules.filter( + (rule) => rule.any_product || rule.valid_product_ids.has(product.id) + ).length > 0 + ); + }, + /** + * Tries to compute how many free product can be given out for the given product. + * Contrary to sale_loyalty, the product must be in the order lines in order to give it out + * (resulting in discount lines for the product's value). + * As such we need to approximate the effect of removing 1 quantity on the counting of points in order + * to avoid feedback loops between giving a product and it removing the required points for it. + * + * @param {loyalty.reward} reward + * @param {Integer} coupon_id + * @param {Product} product + * @returns {Integer} Available quantity to be given as reward for the given product + */ + _computeUnclaimedFreeProductQty(reward, coupon_id, product, remainingPoints) { + let claimed = 0; + let available = 0; + let shouldCorrectRemainingPoints = false; + for (const line of this.get_orderlines()) { + if (line.get_product().id === product.id) { + available += line.get_quantity(); + } else if (line.reward_product_id === product.id) { + if (line.reward_id == reward.id) { + claimed += line.get_quantity(); + } else { + shouldCorrectRemainingPoints = true; } - this.codeActivatedProgramRules.push(rule.id); - await this._updateLoyaltyPrograms(); - claimableRewards = this.getClaimableRewards(false, rule.program_id.id); - } else { - if (this.codeActivatedCoupons.find((coupon) => coupon.code === code)) { - return _t("That coupon code has already been scanned and activated."); - } - const customer = this.get_partner(); - const { successful, payload } = await this.pos.env.services.rpc({ - model: "pos.config", - method: "use_coupon_code", - args: [ - [this.pos.config.id], - code, - this.creation_date, - customer ? customer.id : false, - ], - kwargs: { context: session.user_context }, - }); - if (successful) { - // Allow rejecting a gift card that is not yet paid. - const program = this.pos.program_by_id[payload.program_id]; - if ( - program && - program.program_type === "gift_card" && - !payload.has_source_order - ) { - const { confirmed } = await Gui.showPopup("ConfirmPopup", { - title: _t("Unpaid gift card"), - body: _t( - "This gift card is not linked to any order. Do you really want to apply its reward?" - ), - }); - if (!confirmed) { - return _t("Unpaid gift card rejected."); + } + } + let freeQty; + if (reward.program_id.trigger == "auto") { + if (this._isRewardProductPartOfRules(reward, product)) { + // OPTIMIZATION: Pre-calculate the factors for each reward-product combination during the loading. + // For points not based on quantity, need to normalize the points to compute free quantity. + const factor = reward.program_id.rules.reduce((result, rule) => { + if (rule.any_product || rule.valid_product_ids.has(product.id)) { + if (rule.reward_point_mode === "order") { + result += rule.reward_point_amount; + } else if (rule.reward_point_mode === "money") { + result += round_precision( + rule.reward_point_amount * product.lst_price, + 0.01 + ); + } else if (rule.reward_point_mode === "unit") { + result += rule.reward_point_amount; } } - const coupon = new PosLoyaltyCard( - code, - payload.coupon_id, - payload.program_id, - payload.partner_id, - payload.points - ); - this.pos.couponCache[coupon.id] = coupon; - this.codeActivatedCoupons.push(coupon); - await this._updateLoyaltyPrograms(); - claimableRewards = this.getClaimableRewards(coupon.id); - } else { - return payload.error_message; + return result; + }, 0); + const correction = shouldCorrectRemainingPoints + ? this._getPointsCorrection(reward.program_id) + : 0; + freeQty = computeFreeQuantity( + (remainingPoints - correction) / factor, + reward.required_points / factor, + reward.reward_product_qty + ); + } else { + freeQty = Math.floor( + (remainingPoints / reward.required_points) * reward.reward_product_qty + ); + } + } else if (reward.program_id.trigger == "with_code") { + freeQty = Math.floor( + (remainingPoints / reward.required_points) * reward.reward_product_qty + ); + } + return Math.min(available, freeQty) - claimed; + }, + _computePotentialFreeProductQty(reward, product, remainingPoints) { + if (reward.program_id.trigger == "auto") { + if (this._isRewardProductPartOfRules(reward, product)) { + const line = this.get_orderlines().find( + (line) => line.reward_product_id === product.id + ); + // Compute the correction points once even if there are multiple reward lines. + // This is because _getPointsCorrection is taking into account all the lines already. + const claimedPoints = line ? this._getPointsCorrection(reward.program_id) : 0; + return Math.floor( + ((remainingPoints - claimedPoints) / reward.required_points) * + reward.reward_product_qty + ); + } else { + return Math.floor( + (remainingPoints / reward.required_points) * reward.reward_product_qty + ); + } + } else if (reward.program_id.trigger == "with_code") { + return Math.floor( + (remainingPoints / reward.required_points) * reward.reward_product_qty + ); + } + }, + /** + * @param {Object} args See `_applyReward` + * @returns {Array} List of values to create the reward lines + */ + _getRewardLineValuesProduct(args) { + const reward = args["reward"]; + const product = this.pos.db.get_product_by_id( + args["product"] || reward.reward_product_ids[0] + ); + const points = this._getRealCouponPoints(args["coupon_id"]); + const unclaimedQty = this._computeUnclaimedFreeProductQty( + reward, + args["coupon_id"], + product, + points + ); + if (unclaimedQty <= 0) { + return _t("There are not enough products in the basket to claim this reward."); + } + const claimable_count = reward.clear_wallet + ? 1 + : Math.min( + Math.ceil(unclaimedQty / reward.reward_product_qty), + Math.floor(points / reward.required_points) + ); + const cost = reward.clear_wallet ? points : claimable_count * reward.required_points; + // In case the reward is the product multiple times, give it as many times as possible + const freeQuantity = Math.min(unclaimedQty, reward.reward_product_qty * claimable_count); + return [ + { + product: reward.discount_line_product_id, + price: -round_decimals( + product.get_price(this.pricelist, freeQuantity), + this.pos.currency.decimal_places + ), + tax_ids: product.taxes_id, + quantity: freeQuantity, + reward_id: reward.id, + is_reward_line: true, + reward_product_id: product.id, + coupon_id: args["coupon_id"], + points_cost: cost, + reward_identifier_code: _newRandomRewardCode(), + merge: false, + }, + ]; + }, + /** + * Full routine for activating a code for the order. + * If only one more reward is claimable after activating the code, that reward is claimed + * directly, to avoid more steps than necessary. + * If more rewards are claimable, the employee will have to manually select the reward + * in the reward selection menu. + * + * @param {String} code + * @returns true if everything went right, error message if not. + */ + async _activateCode(code) { + const rule = this.pos.rules.find((rule) => { + return rule.mode === "with_code" && (rule.promo_barcode === code || rule.code === code); + }); + let claimableRewards = null; + if (rule) { + if (this.codeActivatedProgramRules.includes(rule.id)) { + return _t("That promo code program has already been activated."); + } + this.codeActivatedProgramRules.push(rule.id); + await this._updateLoyaltyPrograms(); + claimableRewards = this.getClaimableRewards(false, rule.program_id.id); + } else { + if (this.codeActivatedCoupons.find((coupon) => coupon.code === code)) { + return _t("That coupon code has already been scanned and activated."); + } + const customer = this.get_partner(); + const { successful, payload } = await this.pos.env.services.rpc({ + model: "pos.config", + method: "use_coupon_code", + args: [ + [this.pos.config.id], + code, + this.creation_date, + customer ? customer.id : false, + ], + kwargs: { context: session.user_context }, + }); + if (successful) { + // Allow rejecting a gift card that is not yet paid. + const program = this.pos.program_by_id[payload.program_id]; + if (program && program.program_type === "gift_card" && !payload.has_source_order) { + const { confirmed } = await Gui.showPopup(ConfirmPopup, { + title: _t("Unpaid gift card"), + body: _t( + "This gift card is not linked to any order. Do you really want to apply its reward?" + ), + }); + if (!confirmed) { + return _t("Unpaid gift card rejected."); + } } - } - if (claimableRewards && claimableRewards.length === 1) { - if ( - claimableRewards[0].reward.reward_type !== "product" || - !claimableRewards[0].reward.multi_product - ) { - this._applyReward(claimableRewards[0].reward, claimableRewards[0].coupon_id); - this._updateRewards(); - } - } - return true; - } - async activateCode(code) { - const res = await this._activateCode(code); - if (res !== true) { - Gui.showNotification(res); + const coupon = new PosLoyaltyCard( + code, + payload.coupon_id, + payload.program_id, + payload.partner_id, + payload.points + ); + this.pos.couponCache[coupon.id] = coupon; + this.codeActivatedCoupons.push(coupon); + await this._updateLoyaltyPrograms(); + claimableRewards = this.getClaimableRewards(coupon.id); + } else { + return payload.error_message; } } - isProgramsResettable() { - const array = [ - this.disabledRewards, - this.codeActivatedProgramRules, - this.codeActivatedCoupons, - this.couponPointChanges, - this._get_reward_lines(), - ]; - return array.some((elem) => elem.length > 0); + if (claimableRewards && claimableRewards.length === 1) { + if ( + claimableRewards[0].reward.reward_type !== "product" || + !claimableRewards[0].reward.multi_product + ) { + this._applyReward(claimableRewards[0].reward, claimableRewards[0].coupon_id); + this._updateRewards(); + } } - }; -Registries.Model.extend(Order, PosLoyaltyOrder); + return true; + }, + async activateCode(code) { + const res = await this._activateCode(code); + if (res !== true) { + Gui.showNotification(res); + } + }, + isProgramsResettable() { + const array = [ + this.disabledRewards, + this.codeActivatedProgramRules, + this.codeActivatedCoupons, + this.couponPointChanges, + this._get_reward_lines(), + ]; + return array.some((elem) => elem.length > 0); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/OrderSummary.js b/addons/pos_loyalty/static/src/js/OrderSummary.js index 7c3d423eae0..78084e72760 100644 --- a/addons/pos_loyalty/static/src/js/OrderSummary.js +++ b/addons/pos_loyalty/static/src/js/OrderSummary.js @@ -1,14 +1,11 @@ /** @odoo-module **/ -import OrderSummary from "@point_of_sale/js/Screens/ProductScreen/OrderSummary"; -import Registries from "@point_of_sale/js/Registries"; +import { OrderSummary } from "@point_of_sale/js/Screens/ProductScreen/OrderSummary"; +import { patch } from "@web/core/utils/patch"; -export const PosLoyaltyOrderSummary = (OrderSummary) => - class PosLoyaltyOrderSummary extends OrderSummary { - getLoyaltyPoints() { - const order = this.env.pos.get_order(); - return order.getLoyaltyPoints(); - } - }; - -Registries.Component.extend(OrderSummary, PosLoyaltyOrderSummary); +patch(OrderSummary.prototype, "pos_loyalty.OrderSummary", { + getLoyaltyPoints() { + const order = this.env.pos.get_order(); + return order.getLoyaltyPoints(); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/OrderWidget.js b/addons/pos_loyalty/static/src/js/OrderWidget.js index c41796991e0..aacc6979c16 100644 --- a/addons/pos_loyalty/static/src/js/OrderWidget.js +++ b/addons/pos_loyalty/static/src/js/OrderWidget.js @@ -1,45 +1,42 @@ /** @odoo-module **/ -import OrderWidget from "@point_of_sale/js/Screens/ProductScreen/OrderWidget"; -import Registries from "@point_of_sale/js/Registries"; +import { OrderWidget } from "@point_of_sale/js/Screens/ProductScreen/OrderWidget"; +import { patch } from "@web/core/utils/patch"; -export const PosLoyaltyOrderWidget = (OrderWidget) => - class PosLoyaltyOrderWidget extends OrderWidget { - getActiveProgramsAndRewards() { - const order = this.env.pos.get_order(); - const activePrograms = Object.values(order.couponPointChanges).map( - (pe) => this.env.pos.program_by_id[pe.program_id] - ); - const seenRewards = new Set(); - const activeRewards = []; - for (const line of order._get_reward_lines()) { - const key = - line.reward_id + "-" + line.coupon_id + "-" + line.reward_identifier_code; - if (seenRewards.has(key)) { - continue; - } - seenRewards.add(key); - const dbCoupon = this.env.pos.couponCache[line.coupon_id]; - const couponCode = dbCoupon ? dbCoupon.code : false; - activeRewards.push({ - id: line.cid, - name: line.get_product().display_name, - code: couponCode, - }); +patch(OrderWidget.prototype, "pos_loyalty.OrderWidget", { + getActiveProgramsAndRewards() { + const order = this.env.pos.get_order(); + const activePrograms = Object.values(order.couponPointChanges).map( + (pe) => this.env.pos.program_by_id[pe.program_id] + ); + const seenRewards = new Set(); + const activeRewards = []; + for (const line of order._get_reward_lines()) { + const key = + line.reward_id + "-" + line.coupon_id + "-" + line.reward_identifier_code; + if (seenRewards.has(key)) { + continue; } - const activeCoupons = order.codeActivatedCoupons.map((coupon) => { - const program = this.env.pos.program_by_id[coupon.program_id]; - return { - programName: program.name, - code: coupon.code, - }; + seenRewards.add(key); + const dbCoupon = this.env.pos.couponCache[line.coupon_id]; + const couponCode = dbCoupon ? dbCoupon.code : false; + activeRewards.push({ + id: line.cid, + name: line.get_product().display_name, + code: couponCode, }); - return { - activePrograms: [...new Set(activePrograms)], - activeCoupons, - activeRewards, - }; } - }; - -Registries.Component.extend(OrderWidget, PosLoyaltyOrderWidget); + const activeCoupons = order.codeActivatedCoupons.map((coupon) => { + const program = this.env.pos.program_by_id[coupon.program_id]; + return { + programName: program.name, + code: coupon.code, + }; + }); + return { + activePrograms: [...new Set(activePrograms)], + activeCoupons, + activeRewards, + }; + }, +}); diff --git a/addons/pos_loyalty/static/src/js/Orderline.js b/addons/pos_loyalty/static/src/js/Orderline.js index 33eef0c76e3..758ed774388 100644 --- a/addons/pos_loyalty/static/src/js/Orderline.js +++ b/addons/pos_loyalty/static/src/js/Orderline.js @@ -1,34 +1,31 @@ /** @odoo-module **/ -import Orderline from "@point_of_sale/js/Screens/ProductScreen/Orderline"; -import Registries from "@point_of_sale/js/Registries"; +import { Orderline } from "@point_of_sale/js/Screens/ProductScreen/Orderline"; +import { patch } from "@web/core/utils/patch"; -export const PosLoyaltyOrderline = (Orderline) => - class extends Orderline { - get addedClasses() { - return Object.assign( - { "program-reward": this.props.line.is_reward_line }, - super.addedClasses +patch(Orderline.prototype, "pos_loyalty.Orderline", { + get addedClasses() { + return Object.assign( + { "program-reward": this.props.line.is_reward_line }, + this._super() + ); + }, + _isGiftCardOrEWalletReward() { + const coupon = this.env.pos.couponCache[this.props.line.coupon_id]; + if (coupon) { + const program = this.env.pos.program_by_id[coupon.program_id]; + return ( + ["ewallet", "gift_card"].includes(program.program_type) && + this.props.line.is_reward_line ); } - _isGiftCardOrEWalletReward() { - const coupon = this.env.pos.couponCache[this.props.line.coupon_id]; - if (coupon) { - const program = this.env.pos.program_by_id[coupon.program_id]; - return ( - ["ewallet", "gift_card"].includes(program.program_type) && - this.props.line.is_reward_line - ); - } - return false; + return false; + }, + _getGiftCardOrEWalletBalance() { + const coupon = this.env.pos.couponCache[this.props.line.coupon_id]; + if (coupon) { + return this.env.pos.format_currency(coupon.balance); } - _getGiftCardOrEWalletBalance() { - const coupon = this.env.pos.couponCache[this.props.line.coupon_id]; - if (coupon) { - return this.env.pos.format_currency(coupon.balance); - } - return this.env.pos.format_currency(0); - } - }; - -Registries.Component.extend(Orderline, PosLoyaltyOrderline); + return this.env.pos.format_currency(0); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/PartnerLine.js b/addons/pos_loyalty/static/src/js/PartnerLine.js index a883368f068..963d9136164 100644 --- a/addons/pos_loyalty/static/src/js/PartnerLine.js +++ b/addons/pos_loyalty/static/src/js/PartnerLine.js @@ -1,23 +1,18 @@ /** @odoo-module */ -import PartnerLine from "@point_of_sale/js/Screens/PartnerListScreen/PartnerLine"; -import Registries from "@point_of_sale/js/Registries"; +import { PartnerLine } from "@point_of_sale/js/Screens/PartnerListScreen/PartnerLine"; +import { patch } from "@web/core/utils/patch"; -const PosLoyaltyPartnerLine = (PartnerLine) => - class extends PartnerLine { - _getLoyaltyPointsRepr(loyaltyCard) { - const program = this.env.pos.program_by_id[loyaltyCard.program_id]; - if (program.program_type === "ewallet") { - return `${program.name}: ${this.env.pos.format_currency(loyaltyCard.balance)}`; - } - const balanceRepr = this.env.pos.format_pr(loyaltyCard.balance, 0.01); - if (program.portal_visible) { - return `${balanceRepr} ${program.portal_point_name}`; - } - return _.str.sprintf(this.env._t("%s Points"), balanceRepr); +patch(PartnerLine.prototype, "pos_loyalty.PartnerLine", { + _getLoyaltyPointsRepr(loyaltyCard) { + const program = this.env.pos.program_by_id[loyaltyCard.program_id]; + if (program.program_type === "ewallet") { + return `${program.name}: ${this.env.pos.format_currency(loyaltyCard.balance)}`; } - }; - -Registries.Component.extend(PartnerLine, PosLoyaltyPartnerLine); - -export default PartnerLine; + const balanceRepr = this.env.pos.format_pr(loyaltyCard.balance, 0.01); + if (program.portal_visible) { + return `${balanceRepr} ${program.portal_point_name}`; + } + return _.str.sprintf(this.env._t("%s Points"), balanceRepr); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/PartnerListScreen.js b/addons/pos_loyalty/static/src/js/PartnerListScreen.js index 1f212e48c35..5ab7d96da10 100644 --- a/addons/pos_loyalty/static/src/js/PartnerListScreen.js +++ b/addons/pos_loyalty/static/src/js/PartnerListScreen.js @@ -1,19 +1,14 @@ /** @odoo-module */ -import PartnerListScreen from "@point_of_sale/js/Screens/PartnerListScreen/PartnerListScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PartnerListScreen } from "@point_of_sale/js/Screens/PartnerListScreen/PartnerListScreen"; +import { patch } from "@web/core/utils/patch"; -const PosLoyaltyPartnerListScreen = (PartnerListScreen) => - class extends PartnerListScreen { - /** - * Needs to be set to true to show the loyalty points in the partner list. - * @override - */ - get isBalanceDisplayed() { - return true; - } - }; - -Registries.Component.extend(PartnerListScreen, PosLoyaltyPartnerListScreen); - -export default PartnerListScreen; +patch(PartnerListScreen.prototype, "pos_loyalty.PartnerListScreen", { + /** + * Needs to be set to true to show the loyalty points in the partner list. + * @override + */ + get isBalanceDisplayed() { + return true; + }, +}); diff --git a/addons/pos_loyalty/static/src/js/PaymentScreen.js b/addons/pos_loyalty/static/src/js/PaymentScreen.js index d965bff676b..c0f2368ec90 100644 --- a/addons/pos_loyalty/static/src/js/PaymentScreen.js +++ b/addons/pos_loyalty/static/src/js/PaymentScreen.js @@ -1,176 +1,175 @@ /** @odoo-module **/ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; import session from "web.session"; import { PosLoyaltyCard } from "@pos_loyalty/js/Loyalty"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; -export const PosLoyaltyPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - //@override - async validateOrder(isForceValidate) { - const pointChanges = {}; - const newCodes = []; - for (const pe of Object.values(this.currentOrder.couponPointChanges)) { - if (pe.coupon_id > 0) { - pointChanges[pe.coupon_id] = pe.points; - } else if (pe.barcode && !pe.giftCardId) { - // New coupon with a specific code, validate that it does not exist - newCodes.push(pe.barcode); - } +patch(PaymentScreen.prototype, "pos_loyalty.PaymentScreen", { + //@override + async validateOrder(isForceValidate) { + const _super = this._super; + const pointChanges = {}; + const newCodes = []; + for (const pe of Object.values(this.currentOrder.couponPointChanges)) { + if (pe.coupon_id > 0) { + pointChanges[pe.coupon_id] = pe.points; + } else if (pe.barcode && !pe.giftCardId) { + // New coupon with a specific code, validate that it does not exist + newCodes.push(pe.barcode); } - for (const line of this.currentOrder._get_reward_lines()) { - if (line.coupon_id < 1) { - continue; - } - if (!pointChanges[line.coupon_id]) { - pointChanges[line.coupon_id] = -line.points_cost; - } else { - pointChanges[line.coupon_id] -= line.points_cost; - } - } - if (!(await this._isOrderValid(isForceValidate))) { - return; - } - // No need to do an rpc if no existing coupon is being used. - if (!_.isEmpty(pointChanges) || newCodes.length) { - try { - const { successful, payload } = await this.rpc({ - model: "pos.order", - method: "validate_coupon_programs", - args: [[], pointChanges, newCodes], - kwargs: { context: session.user_context }, - }); - // Payload may contain the points of the concerned coupons to be updated in case of error. (So that rewards can be corrected) - if (payload && payload.updated_points) { - for (const pointChange of Object.entries(payload.updated_points)) { - if (this.env.pos.couponCache[pointChange[0]]) { - this.env.pos.couponCache[pointChange[0]].balance = pointChange[1]; - } - } - } - if (payload && payload.removed_coupons) { - for (const couponId of payload.removed_coupons) { - if (this.env.pos.couponCache[couponId]) { - delete this.env.pos.couponCache[couponId]; - } - } - this.currentOrder.codeActivatedCoupons = - this.currentOrder.codeActivatedCoupons.filter( - (coupon) => !payload.removed_coupons.includes(coupon.id) - ); - } - if (!successful) { - this.showPopup("ErrorPopup", { - title: this.env._t("Error validating rewards"), - body: payload.message, - }); - return; - } - } catch { - // Do nothing with error, while this validation step is nice for error messages - // it should not be blocking. - } - } - await super.validateOrder(...arguments); } - - /** - * @override - */ - async _postPushOrderResolve(order, server_ids) { - // Compile data for our function - const rewardLines = order._get_reward_lines(); - const partner = order.get_partner(); - let couponData = Object.values(order.couponPointChanges).reduce((agg, pe) => { - agg[pe.coupon_id] = Object.assign({}, pe, { - points: - pe.points - - order._getPointsCorrection(this.env.pos.program_by_id[pe.program_id]), - }); - const program = this.env.pos.program_by_id[pe.program_id]; - if (program.is_nominative && partner) { - agg[pe.coupon_id].partner_id = partner.id; - } - return agg; - }, {}); - for (const line of rewardLines) { - const reward = this.env.pos.reward_by_id[line.reward_id]; - if (!couponData[line.coupon_id]) { - couponData[line.coupon_id] = { - points: 0, - program_id: reward.program_id.id, - coupon_id: line.coupon_id, - barcode: false, - }; - } - if (!couponData[line.coupon_id].line_codes) { - couponData[line.coupon_id].line_codes = []; - } - if (!couponData[line.coupon_id].line_codes.includes(line.reward_identifier_code)) { - !couponData[line.coupon_id].line_codes.push(line.reward_identifier_code); - } - couponData[line.coupon_id].points -= line.points_cost; + for (const line of this.currentOrder._get_reward_lines()) { + if (line.coupon_id < 1) { + continue; } - // We actually do not care about coupons for 'current' programs that did not claim any reward, they will be lost if not validated - couponData = Object.fromEntries( - Object.entries(couponData).filter(([key, value]) => { - const program = this.env.pos.program_by_id[value.program_id]; - if (program.applies_on === "current") { - return value.line_codes && value.line_codes.length; - } - return true; - }) - ); - if (!_.isEmpty(couponData)) { - const payload = await this.rpc({ + if (!pointChanges[line.coupon_id]) { + pointChanges[line.coupon_id] = -line.points_cost; + } else { + pointChanges[line.coupon_id] -= line.points_cost; + } + } + if (!(await this._isOrderValid(isForceValidate))) { + return; + } + // No need to do an rpc if no existing coupon is being used. + if (!_.isEmpty(pointChanges) || newCodes.length) { + try { + const { successful, payload } = await this.rpc({ model: "pos.order", - method: "confirm_coupon_programs", - args: [server_ids, couponData], + method: "validate_coupon_programs", + args: [[], pointChanges, newCodes], kwargs: { context: session.user_context }, }); - if (payload.coupon_updates) { - for (const couponUpdate of payload.coupon_updates) { - let dbCoupon = this.env.pos.couponCache[couponUpdate.old_id]; - if (dbCoupon) { - dbCoupon.id = couponUpdate.id; - dbCoupon.balance = couponUpdate.points; - dbCoupon.code = couponUpdate.code; - } else { - dbCoupon = new PosLoyaltyCard( - couponUpdate.code, - couponUpdate.id, - couponUpdate.program_id, - couponUpdate.partner_id, - couponUpdate.points - ); - } - delete this.env.pos.couponCache[couponUpdate.old_id]; - this.env.pos.couponCache[couponUpdate.id] = dbCoupon; - } - } - // Update the usage count since it is checked based on local data - if (payload.program_updates) { - for (const programUpdate of payload.program_updates) { - const program = this.env.pos.program_by_id[programUpdate.program_id]; - if (program) { - program.total_order_count = programUpdate.usages; + // Payload may contain the points of the concerned coupons to be updated in case of error. (So that rewards can be corrected) + if (payload && payload.updated_points) { + for (const pointChange of Object.entries(payload.updated_points)) { + if (this.env.pos.couponCache[pointChange[0]]) { + this.env.pos.couponCache[pointChange[0]].balance = pointChange[1]; } } } - if (payload.coupon_report) { - for (const report_entry of Object.entries(payload.coupon_report)) { - await this.env.legacyActionManager.do_action(report_entry[0], { - additional_context: { - active_ids: report_entry[1], - }, - }); + if (payload && payload.removed_coupons) { + for (const couponId of payload.removed_coupons) { + if (this.env.pos.couponCache[couponId]) { + delete this.env.pos.couponCache[couponId]; + } } + this.currentOrder.codeActivatedCoupons = + this.currentOrder.codeActivatedCoupons.filter( + (coupon) => !payload.removed_coupons.includes(coupon.id) + ); } - order.new_coupon_info = payload.new_coupon_info; + if (!successful) { + this.showPopup(ErrorPopup, { + title: this.env._t("Error validating rewards"), + body: payload.message, + }); + return; + } + } catch { + // Do nothing with error, while this validation step is nice for error messages + // it should not be blocking. } - return super._postPushOrderResolve(order, server_ids); } - }; - -Registries.Component.extend(PaymentScreen, PosLoyaltyPaymentScreen); + await _super(...arguments); + }, + /** + * @override + */ + async _postPushOrderResolve(order, server_ids) { + // Compile data for our function + const _super = this._super; + const rewardLines = order._get_reward_lines(); + const partner = order.get_partner(); + let couponData = Object.values(order.couponPointChanges).reduce((agg, pe) => { + agg[pe.coupon_id] = Object.assign({}, pe, { + points: + pe.points - + order._getPointsCorrection(this.env.pos.program_by_id[pe.program_id]), + }); + const program = this.env.pos.program_by_id[pe.program_id]; + if (program.is_nominative && partner) { + agg[pe.coupon_id].partner_id = partner.id; + } + return agg; + }, {}); + for (const line of rewardLines) { + const reward = this.env.pos.reward_by_id[line.reward_id]; + if (!couponData[line.coupon_id]) { + couponData[line.coupon_id] = { + points: 0, + program_id: reward.program_id.id, + coupon_id: line.coupon_id, + barcode: false, + }; + } + if (!couponData[line.coupon_id].line_codes) { + couponData[line.coupon_id].line_codes = []; + } + if (!couponData[line.coupon_id].line_codes.includes(line.reward_identifier_code)) { + !couponData[line.coupon_id].line_codes.push(line.reward_identifier_code); + } + couponData[line.coupon_id].points -= line.points_cost; + } + // We actually do not care about coupons for 'current' programs that did not claim any reward, they will be lost if not validated + couponData = Object.fromEntries( + Object.entries(couponData).filter(([key, value]) => { + const program = this.env.pos.program_by_id[value.program_id]; + if (program.applies_on === "current") { + return value.line_codes && value.line_codes.length; + } + return true; + }) + ); + if (!_.isEmpty(couponData)) { + const payload = await this.rpc({ + model: "pos.order", + method: "confirm_coupon_programs", + args: [server_ids, couponData], + kwargs: { context: session.user_context }, + }); + if (payload.coupon_updates) { + for (const couponUpdate of payload.coupon_updates) { + let dbCoupon = this.env.pos.couponCache[couponUpdate.old_id]; + if (dbCoupon) { + dbCoupon.id = couponUpdate.id; + dbCoupon.balance = couponUpdate.points; + dbCoupon.code = couponUpdate.code; + } else { + dbCoupon = new PosLoyaltyCard( + couponUpdate.code, + couponUpdate.id, + couponUpdate.program_id, + couponUpdate.partner_id, + couponUpdate.points + ); + } + delete this.env.pos.couponCache[couponUpdate.old_id]; + this.env.pos.couponCache[couponUpdate.id] = dbCoupon; + } + } + // Update the usage count since it is checked based on local data + if (payload.program_updates) { + for (const programUpdate of payload.program_updates) { + const program = this.env.pos.program_by_id[programUpdate.program_id]; + if (program) { + program.total_order_count = programUpdate.usages; + } + } + } + if (payload.coupon_report) { + for (const report_entry of Object.entries(payload.coupon_report)) { + await this.env.legacyActionManager.do_action(report_entry[0], { + additional_context: { + active_ids: report_entry[1], + }, + }); + } + } + order.new_coupon_info = payload.new_coupon_info; + } + return _super(order, server_ids); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/ProductScreen.js b/addons/pos_loyalty/static/src/js/ProductScreen.js index a1d53fb106a..477159c2467 100644 --- a/addons/pos_loyalty/static/src/js/ProductScreen.js +++ b/addons/pos_loyalty/static/src/js/ProductScreen.js @@ -1,256 +1,256 @@ /** @odoo-module **/ -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; +import { patch } from "@web/core/utils/patch"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; +import { TextInputPopup } from "@point_of_sale/js/Popups/TextInputPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; -export const PosLoyaltyProductScreen = (ProductScreen) => - class extends ProductScreen { - setup() { - super.setup(); - useBarcodeReader({ - coupon: this._onCouponScan, +patch(ProductScreen.prototype, "pos_loyalty.ProductScreen", { + setup() { + this._super(...arguments); + useBarcodeReader({ + coupon: this._onCouponScan, + }); + }, + async _onClickPay() { + const order = this.env.pos.get_order(); + const eWalletLine = order + .get_orderlines() + .find((line) => line.getEWalletGiftCardProgramType() === "ewallet"); + if (eWalletLine && !order.get_partner()) { + const { confirmed } = await this.showPopup(ConfirmPopup, { + title: this.env._t("Customer needed"), + body: this.env._t("eWallet requires a customer to be selected"), }); - } - async _onClickPay() { - const order = this.env.pos.get_order(); - const eWalletLine = order - .get_orderlines() - .find((line) => line.getEWalletGiftCardProgramType() === "ewallet"); - if (eWalletLine && !order.get_partner()) { - const { confirmed } = await this.showPopup("ConfirmPopup", { - title: this.env._t("Customer needed"), - body: this.env._t("eWallet requires a customer to be selected"), - }); + if (confirmed) { + const { confirmed, payload: newPartner } = await this.showTempScreen( + "PartnerListScreen", + { partner: null } + ); if (confirmed) { - const { confirmed, payload: newPartner } = await this.showTempScreen( - "PartnerListScreen", - { partner: null } - ); - if (confirmed) { - order.set_partner(newPartner); - order.updatePricelist(newPartner); - } + order.set_partner(newPartner); + order.updatePricelist(newPartner); + } + } + } else { + return this._super(...arguments); + } + }, + /** + * Sets up the options for the gift card product. + * @param {object} program + * @param {object} options + * @returns {Promise} whether to proceed with adding the product or not + */ + async _setupGiftCardOptions(program, options) { + options.quantity = 1; + options.merge = false; + options.eWalletGiftCardProgram = program; + + // If gift card program setting is 'scan_use', ask for the code. + if (this.env.pos.config.gift_card_settings == "scan_use") { + const { confirmed, payload: code } = await this.showPopup(TextInputPopup, { + title: this.env._t("Generate a Gift Card"), + startingValue: "", + placeholder: this.env._t("Enter the gift card code"), + }); + if (!confirmed) { + return false; + } + const trimmedCode = code.trim(); + if (trimmedCode && trimmedCode.startsWith("044")) { + // check if the code exist in the database + // if so, use its balance, otherwise, use the unit price of the gift card product + const fetchedGiftCard = await this.rpc({ + model: "loyalty.card", + method: "search_read", + args: [ + [ + ["code", "=", trimmedCode], + ["program_id", "=", program.id], + ], + ["points", "source_pos_order_id"], + ], + }); + // There should be maximum one gift card for a given code. + const giftCard = fetchedGiftCard[0]; + if (giftCard && giftCard.source_pos_order_id) { + this.showPopup(ErrorPopup, { + title: this.env._t("This gift card has already been sold"), + body: this.env._t( + "You cannot sell a gift card that has already been sold." + ), + }); + return false; + } + options.giftBarcode = trimmedCode; + if (giftCard) { + // Use the balance of the gift card as the price of the orderline. + // NOTE: No need to convert the points to price because when opening a session, + // the gift card programs are made sure to have 1 point = 1 currency unit. + options.price = giftCard.points; + options.giftCardId = giftCard.id; } } else { - return super._onClickPay(...arguments); + this.showNotification("Please enter a valid gift card code."); + return false; } } - /** - * Sets up the options for the gift card product. - * @param {object} program - * @param {object} options - * @returns {Promise} whether to proceed with adding the product or not - */ - async _setupGiftCardOptions(program, options) { - options.quantity = 1; - options.merge = false; - options.eWalletGiftCardProgram = program; - - // If gift card program setting is 'scan_use', ask for the code. - if (this.env.pos.config.gift_card_settings == "scan_use") { - const { confirmed, payload: code } = await this.showPopup("TextInputPopup", { - title: this.env._t("Generate a Gift Card"), - startingValue: "", - placeholder: this.env._t("Enter the gift card code"), - }); - if (!confirmed) { - return false; - } - const trimmedCode = code.trim(); - if (trimmedCode && trimmedCode.startsWith("044")) { - // check if the code exist in the database - // if so, use its balance, otherwise, use the unit price of the gift card product - const fetchedGiftCard = await this.rpc({ - model: "loyalty.card", - method: "search_read", - args: [ - [ - ["code", "=", trimmedCode], - ["program_id", "=", program.id], - ], - ["points", "source_pos_order_id"], - ], - }); - // There should be maximum one gift card for a given code. - const giftCard = fetchedGiftCard[0]; - if (giftCard && giftCard.source_pos_order_id) { - this.showPopup("ErrorPopup", { - title: this.env._t("This gift card has already been sold"), - body: this.env._t( - "You cannot sell a gift card that has already been sold." - ), - }); - return false; - } - options.giftBarcode = trimmedCode; - if (giftCard) { - // Use the balance of the gift card as the price of the orderline. - // NOTE: No need to convert the points to price because when opening a session, - // the gift card programs are made sure to have 1 point = 1 currency unit. - options.price = giftCard.points; - options.giftCardId = giftCard.id; - } - } else { - this.showNotification("Please enter a valid gift card code."); - return false; - } - } - return true; - } - async setupEWalletOptions(program, options) { - options.quantity = 1; - options.merge = false; - options.eWalletGiftCardProgram = program; - return true; - } - /** - * If the product is a potential reward, also apply the reward. - * @override - */ - async _addProduct(product, options) { - const linkedProgramIds = this.env.pos.productId2ProgramIds[product.id] || []; - const linkedPrograms = linkedProgramIds.map((id) => this.env.pos.program_by_id[id]); - let selectedProgram = null; - if (linkedPrograms.length > 1) { - const { confirmed, payload: program } = await this.showPopup("SelectionPopup", { - title: this.env._t("Select program"), - list: linkedPrograms.map((program) => ({ - id: program.id, - item: program, - label: program.name, - })), - }); - if (confirmed) { - selectedProgram = program; - } else { - // Do nothing here if the selection is cancelled. - return; - } - } else if (linkedPrograms.length === 1) { - selectedProgram = linkedPrograms[0]; - } - if (selectedProgram && selectedProgram.program_type == "gift_card") { - const shouldProceed = await this._setupGiftCardOptions(selectedProgram, options); - if (!shouldProceed) { - return; - } - } else if (selectedProgram && selectedProgram.program_type == "ewallet") { - const shouldProceed = await this.setupEWalletOptions(selectedProgram, options); - if (!shouldProceed) { - return; - } - } - const order = this.env.pos.get_order(); - const potentialRewards = order.getPotentialFreeProductRewards(); - const rewardsToApply = []; - for (const reward of potentialRewards) { - for (const reward_product_id of reward.reward.reward_product_ids) { - if (reward_product_id == product.id) { - rewardsToApply.push(reward); - } - } - } - await super._addProduct(product, options); - await order._updatePrograms(); - if (rewardsToApply.length == 1) { - const reward = rewardsToApply[0]; - order._applyReward(reward.reward, reward.coupon_id, { product: product.id }); - } - } - - _onCouponScan(code) { - // IMPROVEMENT: Ability to understand if the scanned code is to be paid or to be redeemed. - this.currentOrder.activateCode(code.base_code); - } - - async _updateSelectedOrderline(event) { - const selectedLine = this.currentOrder.get_selected_orderline(); - if (event.detail.key === "-") { - if (selectedLine.eWalletGiftCardProgram) { - // Do not allow negative quantity or price in a gift card or ewallet orderline. - // Refunding gift card or ewallet is not supported. - this.showNotification( - this.env._t( - "You cannot set negative quantity or price to gift card or ewallet." - ), - 4000 - ); - return; - } - } - if ( - selectedLine && - selectedLine.is_reward_line && - !selectedLine.manual_reward && - (event.detail.key === "Backspace" || event.detail.key === "Delete") - ) { - const reward = this.env.pos.reward_by_id[selectedLine.reward_id]; - const { confirmed } = await this.showPopup("ConfirmPopup", { - title: this.env._t("Deactivating reward"), - body: _.str.sprintf( - this.env._t( - "Are you sure you want to remove %s from this order?\n You will still be able to claim it through the reward button." - ), - reward.description - ), - cancelText: this.env._t("No"), - confirmText: this.env._t("Yes"), - }); - if (confirmed) { - event.detail.buffer = null; - } else { - // Cancel backspace - return; - } - } - return super._updateSelectedOrderline(...arguments); - } - - /** - * 1/ Perform the usual set value operation (super._setValue) if the line being modified - * is not a reward line or if it is a reward line, the `val` being set is '' or 'remove' only. - * - * 2/ Update activated programs and coupons when removing a reward line. - * - * 3/ Trigger 'update-rewards' if the line being modified is a regular line or - * if removing a reward line. - * - * @override - */ - _setValue(val) { - const selectedLine = this.currentOrder.get_selected_orderline(); - if ( - !selectedLine || - !selectedLine.is_reward_line || - (selectedLine.is_reward_line && ["", "remove"].includes(val)) - ) { - super._setValue(val); - } - if (!selectedLine) { + return true; + }, + async setupEWalletOptions(program, options) { + options.quantity = 1; + options.merge = false; + options.eWalletGiftCardProgram = program; + return true; + }, + /** + * If the product is a potential reward, also apply the reward. + * @override + */ + async _addProduct(product, options) { + const _super = this._super; + const linkedProgramIds = this.env.pos.productId2ProgramIds[product.id] || []; + const linkedPrograms = linkedProgramIds.map((id) => this.env.pos.program_by_id[id]); + let selectedProgram = null; + if (linkedPrograms.length > 1) { + const { confirmed, payload: program } = await this.showPopup(SelectionPopup, { + title: this.env._t("Select program"), + list: linkedPrograms.map((program) => ({ + id: program.id, + item: program, + label: program.name, + })), + }); + if (confirmed) { + selectedProgram = program; + } else { + // Do nothing here if the selection is cancelled. return; } - if (selectedLine.is_reward_line && val === "remove") { - this.currentOrder.disabledRewards.add(selectedLine.reward_id); - const coupon = this.env.pos.couponCache[selectedLine.coupon_id]; - if ( - coupon && - coupon.id > 0 && - this.currentOrder.codeActivatedCoupons.find((c) => c.code === coupon.code) - ) { - delete this.env.pos.couponCache[selectedLine.coupon_id]; - this.currentOrder.codeActivatedCoupons.splice( - this.currentOrder.codeActivatedCoupons.findIndex((coupon) => { - return coupon.id === selectedLine.coupon_id; - }), - 1 - ); - } + } else if (linkedPrograms.length === 1) { + selectedProgram = linkedPrograms[0]; + } + if (selectedProgram && selectedProgram.program_type == "gift_card") { + const shouldProceed = await this._setupGiftCardOptions(selectedProgram, options); + if (!shouldProceed) { + return; } - if (!selectedLine.is_reward_line || (selectedLine.is_reward_line && val === "remove")) { - selectedLine.order._updateRewards(); + } else if (selectedProgram && selectedProgram.program_type == "ewallet") { + const shouldProceed = await this.setupEWalletOptions(selectedProgram, options); + if (!shouldProceed) { + return; } } - }; - -Registries.Component.extend(ProductScreen, PosLoyaltyProductScreen); + const order = this.env.pos.get_order(); + const potentialRewards = order.getPotentialFreeProductRewards(); + const rewardsToApply = []; + for (const reward of potentialRewards) { + for (const reward_product_id of reward.reward.reward_product_ids) { + if (reward_product_id == product.id) { + rewardsToApply.push(reward); + } + } + } + await _super(product, options); + await order._updatePrograms(); + if (rewardsToApply.length == 1) { + const reward = rewardsToApply[0]; + order._applyReward(reward.reward, reward.coupon_id, { product: product.id }); + } + }, + _onCouponScan(code) { + // IMPROVEMENT: Ability to understand if the scanned code is to be paid or to be redeemed. + this.currentOrder.activateCode(code.base_code); + }, + async _updateSelectedOrderline(event) { + const _super = this._super; + const selectedLine = this.currentOrder.get_selected_orderline(); + if (event.detail.key === "-") { + if (selectedLine.eWalletGiftCardProgram) { + // Do not allow negative quantity or price in a gift card or ewallet orderline. + // Refunding gift card or ewallet is not supported. + this.showNotification( + this.env._t( + "You cannot set negative quantity or price to gift card or ewallet." + ), + 4000 + ); + return; + } + } + if ( + selectedLine && + selectedLine.is_reward_line && + !selectedLine.manual_reward && + (event.detail.key === "Backspace" || event.detail.key === "Delete") + ) { + const reward = this.env.pos.reward_by_id[selectedLine.reward_id]; + const { confirmed } = await this.showPopup(ConfirmPopup, { + title: this.env._t("Deactivating reward"), + body: _.str.sprintf( + this.env._t( + "Are you sure you want to remove %s from this order?\n You will still be able to claim it through the reward button." + ), + reward.description + ), + cancelText: this.env._t("No"), + confirmText: this.env._t("Yes"), + }); + if (confirmed) { + event.detail.buffer = null; + } else { + // Cancel backspace + return; + } + } + return _super(...arguments); + }, + /** + * 1/ Perform the usual set value operation (this._super(val)) if the line being modified + * is not a reward line or if it is a reward line, the `val` being set is '' or 'remove' only. + * + * 2/ Update activated programs and coupons when removing a reward line. + * + * 3/ Trigger 'update-rewards' if the line being modified is a regular line or + * if removing a reward line. + * + * @override + */ + _setValue(val) { + const selectedLine = this.currentOrder.get_selected_orderline(); + if ( + !selectedLine || + !selectedLine.is_reward_line || + (selectedLine.is_reward_line && ["", "remove"].includes(val)) + ) { + this._super(val); + } + if (!selectedLine) { + return; + } + if (selectedLine.is_reward_line && val === "remove") { + this.currentOrder.disabledRewards.add(selectedLine.reward_id); + const coupon = this.env.pos.couponCache[selectedLine.coupon_id]; + if ( + coupon && + coupon.id > 0 && + this.currentOrder.codeActivatedCoupons.find((c) => c.code === coupon.code) + ) { + delete this.env.pos.couponCache[selectedLine.coupon_id]; + this.currentOrder.codeActivatedCoupons.splice( + this.currentOrder.codeActivatedCoupons.findIndex((coupon) => { + return coupon.id === selectedLine.coupon_id; + }), + 1 + ); + } + } + if (!selectedLine.is_reward_line || (selectedLine.is_reward_line && val === "remove")) { + selectedLine.order._updateRewards(); + } + }, +}); diff --git a/addons/pos_loyalty/static/src/js/TicketScreen.js b/addons/pos_loyalty/static/src/js/TicketScreen.js index 444ec85a844..053b8688498 100644 --- a/addons/pos_loyalty/static/src/js/TicketScreen.js +++ b/addons/pos_loyalty/static/src/js/TicketScreen.js @@ -1,58 +1,55 @@ /** @odoo-module **/ -import TicketScreen from "@point_of_sale/js/Screens/TicketScreen/TicketScreen"; -import Registries from "@point_of_sale/js/Registries"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { TicketScreen } from "@point_of_sale/js/Screens/TicketScreen/TicketScreen"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { patch } from "@web/core/utils/patch"; /** * Prevent refunding ewallet/gift card lines. */ -export const PosLoyaltyTicketScreen = (TicketScreen) => - class PosLoyaltyTicketScreen extends TicketScreen { - _onUpdateSelectedOrderline() { - const order = this.getSelectedSyncedOrder(); - if (!order) { - return NumberBuffer.reset(); - } - const selectedOrderlineId = this.getSelectedOrderlineId(); - const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); - if (orderline && this._isEWalletGiftCard(orderline)) { - this._showNotAllowedRefundNotification(); - return NumberBuffer.reset(); - } - return super._onUpdateSelectedOrderline(...arguments); +patch(TicketScreen.prototype, "pos_loyalty.TicketScreen", { + _onUpdateSelectedOrderline() { + const order = this.getSelectedSyncedOrder(); + if (!order) { + return numberBuffer.reset(); } - _prepareAutoRefundOnOrder(order) { - const selectedOrderlineId = this.getSelectedOrderlineId(); - const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); - if (this._isEWalletGiftCard(orderline)) { - this._showNotAllowedRefundNotification(); - return false; - } - return super._prepareAutoRefundOnOrder(...arguments); + const selectedOrderlineId = this.getSelectedOrderlineId(); + const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); + if (orderline && this._isEWalletGiftCard(orderline)) { + this._showNotAllowedRefundNotification(); + return numberBuffer.reset(); } - _showNotAllowedRefundNotification() { - this.showNotification( - this.env._t( - "Refunding a top up or reward product for an eWallet or gift card program is not allowed." - ), - 5000 - ); - } - _isEWalletGiftCard(orderline) { - const linkedProgramIds = this.env.pos.productId2ProgramIds[orderline.product.id]; - if (linkedProgramIds) { - return linkedProgramIds.length > 0; - } - if (orderline.is_reward_line) { - const reward = this.env.pos.reward_by_id[orderline.reward_id]; - const program = reward && reward.program_id; - if (program && ["gift_card", "ewallet"].includes(program.program_type)) { - return true; - } - } + return this._super(...arguments); + }, + _prepareAutoRefundOnOrder(order) { + const selectedOrderlineId = this.getSelectedOrderlineId(); + const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); + if (this._isEWalletGiftCard(orderline)) { + this._showNotAllowedRefundNotification(); return false; } - }; - -Registries.Component.extend(TicketScreen, PosLoyaltyTicketScreen); + return this._super(...arguments); + }, + _showNotAllowedRefundNotification() { + this.showNotification( + this.env._t( + "Refunding a top up or reward product for an eWallet or gift card program is not allowed." + ), + 5000 + ); + }, + _isEWalletGiftCard(orderline) { + const linkedProgramIds = this.env.pos.productId2ProgramIds[orderline.product.id]; + if (linkedProgramIds) { + return linkedProgramIds.length > 0; + } + if (orderline.is_reward_line) { + const reward = this.env.pos.reward_by_id[orderline.reward_id]; + const program = reward && reward.program_id; + if (program && ["gift_card", "ewallet"].includes(program.program_type)) { + return true; + } + } + return false; + }, +}); diff --git a/addons/pos_mercury/static/src/js/OrderReceipt.js b/addons/pos_mercury/static/src/js/OrderReceipt.js index d7dffe12674..e35c4baddb5 100644 --- a/addons/pos_mercury/static/src/js/OrderReceipt.js +++ b/addons/pos_mercury/static/src/js/OrderReceipt.js @@ -1,24 +1,19 @@ /** @odoo-module */ -import OrderReceipt from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt"; -import Registries from "@point_of_sale/js/Registries"; +import { OrderReceipt } from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt"; +import { patch } from "@web/core/utils/patch"; -const PosMercuryOrderReceipt = (OrderReceipt) => - class extends OrderReceipt { - /** - * The receipt has signature if one of the paymentlines - * is paid with mercury. - */ - get hasPosMercurySignature() { - for (const line of this.paymentlines) { - if (line.mercury_data) { - return true; - } +patch(OrderReceipt.prototype, "pos_mercury.OrderReceipt", { + /** + * The receipt has signature if one of the paymentlines + * is paid with mercury. + */ + get hasPosMercurySignature() { + for (const line of this.paymentlines) { + if (line.mercury_data) { + return true; } - return false; } - }; - -Registries.Component.extend(OrderReceipt, PosMercuryOrderReceipt); - -export default OrderReceipt; + return false; + }, +}); diff --git a/addons/pos_mercury/static/src/js/PaymentScreen.js b/addons/pos_mercury/static/src/js/PaymentScreen.js index ab9ee9c2e2f..3f0af630101 100644 --- a/addons/pos_mercury/static/src/js/PaymentScreen.js +++ b/addons/pos_mercury/static/src/js/PaymentScreen.js @@ -1,10 +1,13 @@ /** @odoo-module */ import { _t } from "web.core"; -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { patch } from "@web/core/utils/patch"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { PaymentTransactionPopup } from "./PaymentTransactionPopup"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; // Lookup table to store status and error messages const lookUpCodeTransaction = { @@ -91,497 +94,475 @@ const lookUpCodeTransaction = { }, }; -const PosMercuryPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - setup() { - super.setup(); - if (this.env.pos.getOnlinePaymentMethods().length !== 0) { - useBarcodeReader({ - credit: this.credit_code_action, - }); +patch(PaymentScreen.prototype, "pos_mercury.PaymentScreen", { + setup() { + this._super(...arguments); + if (this.env.pos.getOnlinePaymentMethods().length !== 0) { + useBarcodeReader({ + credit: this.credit_code_action, + }); + } + // How long we wait for the odoo server to deliver the response of + // a Vantiv transaction + this.server_timeout_in_ms = 95000; + + // How many Vantiv transactions we send without receiving a + // response + this.server_retries = 3; + }, + /** + * The card reader acts as a barcode scanner. This sets up + * the numberBuffer to not immediately act on keyboard + * input. + * + * @override + */ + get _getNumberBufferConfig() { + const res = this._super(); + res["useWithBarcode"] = true; + return res; + }, + /** + * Finish any pending input before trying to validate. + * + * @override + */ + async validateOrder(isForceValidate) { + numberBuffer.capture(); + return this._super(...arguments); + }, + /** + * Finish any pending input before sending a request to a terminal. + * + * @override + */ + async _sendPaymentRequest({ detail: line }) { + numberBuffer.capture(); + return this._super(...arguments); + }, + _get_swipe_pending_line() { + var i = 0; + var lines = this.env.pos.get_order().get_paymentlines(); + + for (i = 0; i < lines.length; i++) { + if (lines[i].mercury_swipe_pending) { + return lines[i]; } - // How long we wait for the odoo server to deliver the response of - // a Vantiv transaction - this.server_timeout_in_ms = 95000; - - // How many Vantiv transactions we send without receiving a - // response - this.server_retries = 3; } - /** - * The card reader acts as a barcode scanner. This sets up - * the NumberBuffer to not immediately act on keyboard - * input. - * - * @override - */ - get _getNumberBufferConfig() { - const res = super._getNumberBufferConfig; - res["useWithBarcode"] = true; - return res; - } + return 0; + }, + _does_credit_payment_line_exist(amount, card_number, card_brand, card_owner_name) { + var i = 0; + var lines = this.env.pos.get_order().get_paymentlines(); - /** - * Finish any pending input before trying to validate. - * - * @override - */ - async validateOrder(isForceValidate) { - NumberBuffer.capture(); - return super.validateOrder(...arguments); - } - - /** - * Finish any pending input before sending a request to a terminal. - * - * @override - */ - async _sendPaymentRequest({ detail: line }) { - NumberBuffer.capture(); - return super._sendPaymentRequest(...arguments); - } - - _get_swipe_pending_line() { - var i = 0; - var lines = this.env.pos.get_order().get_paymentlines(); - - for (i = 0; i < lines.length; i++) { - if (lines[i].mercury_swipe_pending) { - return lines[i]; - } + for (i = 0; i < lines.length; i++) { + if ( + lines[i].mercury_amount === amount && + lines[i].mercury_card_number === card_number && + lines[i].mercury_card_brand === card_brand && + lines[i].mercury_card_owner_name === card_owner_name + ) { + return true; } - - return 0; } - _does_credit_payment_line_exist(amount, card_number, card_brand, card_owner_name) { - var i = 0; - var lines = this.env.pos.get_order().get_paymentlines(); + return false; + }, + retry_mercury_transaction(def, response, retry_nr, can_connect_to_server, callback, args) { + var self = this; + var message = ""; - for (i = 0; i < lines.length; i++) { - if ( - lines[i].mercury_amount === amount && - lines[i].mercury_card_number === card_number && - lines[i].mercury_card_brand === card_brand && - lines[i].mercury_card_owner_name === card_owner_name - ) { - return true; - } - } - - return false; - } - - retry_mercury_transaction(def, response, retry_nr, can_connect_to_server, callback, args) { - var self = this; - var message = ""; - - if (retry_nr < self.server_retries) { - if (response) { - message = "Retry #" + (retry_nr + 1) + "...

" + response.message; - } else { - message = "Retry #" + (retry_nr + 1) + "..."; - } - def.notify({ - message: message, - }); - - setTimeout(function () { - callback.apply(self, args); - }, 1000); + if (retry_nr < self.server_retries) { + if (response) { + message = "Retry #" + (retry_nr + 1) + "...

" + response.message; } else { - if (response) { - message = - "Error " + - response.error + - ": " + - lookUpCodeTransaction["TimeoutError"][response.error] + - "
" + - response.message; + message = "Retry #" + (retry_nr + 1) + "..."; + } + def.notify({ + message: message, + }); + + setTimeout(function () { + callback.apply(self, args); + }, 1000); + } else { + if (response) { + message = + "Error " + + response.error + + ": " + + lookUpCodeTransaction["TimeoutError"][response.error] + + "
" + + response.message; + } else { + if (can_connect_to_server) { + message = self.env._t("No response from Vantiv (Vantiv down?)"); } else { - if (can_connect_to_server) { - message = self.env._t("No response from Vantiv (Vantiv down?)"); + message = self.env._t("No response from server (connected to network?)"); + } + } + def.resolve({ + message: message, + auto_close: false, + }); + } + }, + // Handler to manage the card reader string + credit_code_transaction(parsed_result, old_deferred, retry_nr) { + var order = this.env.pos.get_order(); + if (order.get_due(order.selected_paymentline) < 0) { + this.showPopup(ErrorPopup, { + title: this.env._t("Refunds not supported"), + body: this.env._t( + "Credit card refunds are not supported. Instead select your credit card payment method, click 'Validate' and refund the original charge manually through the Vantiv backend." + ), + }); + return; + } + + if (this.env.pos.getOnlinePaymentMethods().length === 0) { + return; + } + + var self = this; + var decodedMagtek = self.env.pos.decodeMagtek(parsed_result.code); + + if (!decodedMagtek) { + this.showPopup(ErrorPopup, { + title: this.env._t("Could not read card"), + body: this.env._t( + "This can be caused by a badly executed swipe or by not having your keyboard layout set to US QWERTY (not US International)." + ), + }); + return; + } + + var swipe_pending_line = self._get_swipe_pending_line(); + var purchase_amount = 0; + + if (swipe_pending_line) { + purchase_amount = swipe_pending_line.get_amount(); + } else { + purchase_amount = self.env.pos.get_order().get_due(); + } + + var transaction = { + encrypted_key: decodedMagtek["encrypted_key"], + encrypted_block: decodedMagtek["encrypted_block"], + transaction_type: "Credit", + transaction_code: "Sale", + invoice_no: self.env.pos.get_order().uid.replace(/-/g, ""), + purchase: purchase_amount, + payment_method_id: parsed_result.payment_method_id, + }; + + var def = old_deferred || new $.Deferred(); + retry_nr = retry_nr || 0; + + // show the transaction popup. + // the transaction deferred is used to update transaction status + // if we have a previous deferred it indicates that this is a retry + if (!old_deferred) { + self.showPopup(PaymentTransactionPopup, { + transaction: def, + }); + def.notify({ + message: this.env._t("Handling transaction..."), + }); + } + + this.rpc( + { + model: "pos_mercury.mercury_transaction", + method: "do_payment", + args: [transaction], + }, + { + timeout: self.server_timeout_in_ms, + } + ) + .then(function (data) { + // if not receiving a response from Vantiv, we should retry + if (data === "timeout") { + self.retry_mercury_transaction( + def, + null, + retry_nr, + true, + self.credit_code_transaction, + [parsed_result, def, retry_nr + 1] + ); + return; + } + + if (data === "not setup") { + def.resolve({ + message: self.env._t("Please setup your Vantiv merchant account."), + }); + return; + } + + if (data === "internal error") { + def.resolve({ + message: self.env._t("Odoo error while processing transaction."), + }); + return; + } + + var response = self.env.pos.decodeMercuryResponse(data); + response.payment_method_id = parsed_result.payment_method_id; + + if (response.status === "Approved") { + // AP* indicates a duplicate request, so don't add anything for those + if ( + response.message === "AP*" && + self._does_credit_payment_line_exist( + response.authorize, + decodedMagtek["number"], + response.card_type, + decodedMagtek["name"] + ) + ) { + def.resolve({ + message: lookUpCodeTransaction["Approved"][response.error], + auto_close: true, + }); } else { - message = self.env._t("No response from server (connected to network?)"); + // If the payment is approved, add a payment line + var order = self.env.pos.get_order(); + + if (swipe_pending_line) { + order.select_paymentline(swipe_pending_line); + } else { + order.add_paymentline( + self.payment_methods_by_id[parsed_result.payment_method_id] + ); + } + + order.selected_paymentline.paid = true; + order.selected_paymentline.mercury_swipe_pending = false; + order.selected_paymentline.mercury_amount = response.authorize; + order.selected_paymentline.set_amount(response.authorize); + order.selected_paymentline.mercury_card_number = decodedMagtek["number"]; + order.selected_paymentline.mercury_card_brand = response.card_type; + order.selected_paymentline.mercury_card_owner_name = decodedMagtek["name"]; + order.selected_paymentline.mercury_ref_no = response.ref_no; + order.selected_paymentline.mercury_record_no = response.record_no; + order.selected_paymentline.mercury_invoice_no = response.invoice_no; + order.selected_paymentline.mercury_auth_code = response.auth_code; + order.selected_paymentline.mercury_data = response; // used to reverse transactions + order.selected_paymentline.set_credit_card_name(); + + numberBuffer.reset(); + order.trigger("change", order); // needed so that export_to_JSON gets triggered + self.render(); + + if (response.message === "PARTIAL AP") { + def.resolve({ + message: self.env._t("Partially approved"), + auto_close: false, + }); + } else { + def.resolve({ + message: lookUpCodeTransaction["Approved"][response.error], + auto_close: true, + }); + } } } - def.resolve({ - message: message, - auto_close: false, - }); - } - } - // Handler to manage the card reader string - credit_code_transaction(parsed_result, old_deferred, retry_nr) { - var order = this.env.pos.get_order(); - if (order.get_due(order.selected_paymentline) < 0) { - this.showPopup("ErrorPopup", { - title: this.env._t("Refunds not supported"), - body: this.env._t( - "Credit card refunds are not supported. Instead select your credit card payment method, click 'Validate' and refund the original charge manually through the Vantiv backend." - ), - }); - return; - } - - if (this.env.pos.getOnlinePaymentMethods().length === 0) { - return; - } - - var self = this; - var decodedMagtek = self.env.pos.decodeMagtek(parsed_result.code); - - if (!decodedMagtek) { - this.showPopup("ErrorPopup", { - title: this.env._t("Could not read card"), - body: this.env._t( - "This can be caused by a badly executed swipe or by not having your keyboard layout set to US QWERTY (not US International)." - ), - }); - return; - } - - var swipe_pending_line = self._get_swipe_pending_line(); - var purchase_amount = 0; - - if (swipe_pending_line) { - purchase_amount = swipe_pending_line.get_amount(); - } else { - purchase_amount = self.env.pos.get_order().get_due(); - } - - var transaction = { - encrypted_key: decodedMagtek["encrypted_key"], - encrypted_block: decodedMagtek["encrypted_block"], - transaction_type: "Credit", - transaction_code: "Sale", - invoice_no: self.env.pos.get_order().uid.replace(/-/g, ""), - purchase: purchase_amount, - payment_method_id: parsed_result.payment_method_id, - }; - - var def = old_deferred || new $.Deferred(); - retry_nr = retry_nr || 0; - - // show the transaction popup. - // the transaction deferred is used to update transaction status - // if we have a previous deferred it indicates that this is a retry - if (!old_deferred) { - self.showPopup("PaymentTransactionPopup", { - transaction: def, - }); - def.notify({ - message: this.env._t("Handling transaction..."), - }); - } - - this.rpc( - { - model: "pos_mercury.mercury_transaction", - method: "do_payment", - args: [transaction], - }, - { - timeout: self.server_timeout_in_ms, - } - ) - .then(function (data) { - // if not receiving a response from Vantiv, we should retry - if (data === "timeout") { + // if an error related to timeout or connectivity issues arised, then retry the same transaction + else { + if (lookUpCodeTransaction["TimeoutError"][response.error]) { + // recoverable error self.retry_mercury_transaction( def, - null, + response, retry_nr, true, self.credit_code_transaction, [parsed_result, def, retry_nr + 1] ); - return; - } - - if (data === "not setup") { - def.resolve({ - message: self.env._t("Please setup your Vantiv merchant account."), - }); - return; - } - - if (data === "internal error") { - def.resolve({ - message: self.env._t("Odoo error while processing transaction."), - }); - return; - } - - var response = self.env.pos.decodeMercuryResponse(data); - response.payment_method_id = parsed_result.payment_method_id; - - if (response.status === "Approved") { - // AP* indicates a duplicate request, so don't add anything for those - if ( - response.message === "AP*" && - self._does_credit_payment_line_exist( - response.authorize, - decodedMagtek["number"], - response.card_type, - decodedMagtek["name"] - ) - ) { - def.resolve({ - message: lookUpCodeTransaction["Approved"][response.error], - auto_close: true, - }); - } else { - // If the payment is approved, add a payment line - var order = self.env.pos.get_order(); - - if (swipe_pending_line) { - order.select_paymentline(swipe_pending_line); - } else { - order.add_paymentline( - self.payment_methods_by_id[parsed_result.payment_method_id] - ); - } - - order.selected_paymentline.paid = true; - order.selected_paymentline.mercury_swipe_pending = false; - order.selected_paymentline.mercury_amount = response.authorize; - order.selected_paymentline.set_amount(response.authorize); - order.selected_paymentline.mercury_card_number = - decodedMagtek["number"]; - order.selected_paymentline.mercury_card_brand = response.card_type; - order.selected_paymentline.mercury_card_owner_name = - decodedMagtek["name"]; - order.selected_paymentline.mercury_ref_no = response.ref_no; - order.selected_paymentline.mercury_record_no = response.record_no; - order.selected_paymentline.mercury_invoice_no = response.invoice_no; - order.selected_paymentline.mercury_auth_code = response.auth_code; - order.selected_paymentline.mercury_data = response; // used to reverse transactions - order.selected_paymentline.set_credit_card_name(); - - NumberBuffer.reset(); - order.trigger("change", order); // needed so that export_to_JSON gets triggered - self.render(); - - if (response.message === "PARTIAL AP") { - def.resolve({ - message: self.env._t("Partially approved"), - auto_close: false, - }); - } else { - def.resolve({ - message: lookUpCodeTransaction["Approved"][response.error], - auto_close: true, - }); - } - } - } - - // if an error related to timeout or connectivity issues arised, then retry the same transaction - else { - if (lookUpCodeTransaction["TimeoutError"][response.error]) { - // recoverable error - self.retry_mercury_transaction( - def, - response, - retry_nr, - true, - self.credit_code_transaction, - [parsed_result, def, retry_nr + 1] - ); - } else { - // not recoverable - def.resolve({ - message: "Error " + response.error + ":
" + response.message, - auto_close: false, - }); - } - } - }) - .catch(function () { - self.retry_mercury_transaction( - def, - null, - retry_nr, - false, - self.credit_code_transaction, - [parsed_result, def, retry_nr + 1] - ); - }); - } - - credit_code_cancel() { - return; - } - - credit_code_action(parsed_result) { - var online_payment_methods = this.env.pos.getOnlinePaymentMethods(); - - if (online_payment_methods.length === 1) { - parsed_result.payment_method_id = online_payment_methods[0].item; - this.credit_code_transaction(parsed_result); - } else { - // this is for supporting another payment system like mercury - const selectionList = online_payment_methods.map((paymentMethod) => ({ - id: paymentMethod.item, - label: paymentMethod.label, - isSelected: false, - item: paymentMethod.item, - })); - this.showPopup("SelectionPopup", { - title: this.env._t("Pay with: "), - list: selectionList, - }).then(({ confirmed, payload: selectedPaymentMethod }) => { - if (confirmed) { - parsed_result.payment_method_id = selectedPaymentMethod; - this.credit_code_transaction(parsed_result); } else { - this.credit_code_cancel(); + // not recoverable + def.resolve({ + message: "Error " + response.error + ":
" + response.message, + auto_close: false, + }); } - }); - } - } - - remove_paymentline_by_ref(line) { - this.env.pos.get_order().remove_paymentline(line); - NumberBuffer.reset(); - this.render(); - } - - do_reversal(line, is_voidsale, old_deferred, retry_nr) { - var def = old_deferred || new $.Deferred(); - var self = this; - retry_nr = retry_nr || 0; - - // show the transaction popup. - // the transaction deferred is used to update transaction status - this.showPopup("PaymentTransactionPopup", { - transaction: def, - }); - - var request_data = _.extend( - { - transaction_type: "Credit", - transaction_code: "VoidSaleByRecordNo", - }, - line.mercury_data - ); - - var message = ""; - var rpc_method = ""; - - if (is_voidsale) { - message = this.env._t("Reversal failed, sending VoidSale..."); - rpc_method = "do_voidsale"; - } else { - message = this.env._t("Sending reversal..."); - rpc_method = "do_reversal"; - } - - if (!old_deferred) { - def.notify({ - message: message, - }); - } - - this.rpc( - { - model: "pos_mercury.mercury_transaction", - method: rpc_method, - args: [request_data], - }, - { - timeout: self.server_timeout_in_ms, } - ) - .then(function (data) { - if (data === "timeout") { - self.retry_mercury_transaction( - def, - null, - retry_nr, - true, - self.do_reversal, - [line, is_voidsale, def, retry_nr + 1] - ); - return; - } + }) + .catch(function () { + self.retry_mercury_transaction( + def, + null, + retry_nr, + false, + self.credit_code_transaction, + [parsed_result, def, retry_nr + 1] + ); + }); + }, + credit_code_cancel() { + return; + }, + credit_code_action(parsed_result) { + var online_payment_methods = this.env.pos.getOnlinePaymentMethods(); - if (data === "internal error") { - def.resolve({ - message: self.env._t("Odoo error while processing transaction."), - }); - return; - } + if (online_payment_methods.length === 1) { + parsed_result.payment_method_id = online_payment_methods[0].item; + this.credit_code_transaction(parsed_result); + } else { + // this is for supporting another payment system like mercury + const selectionList = online_payment_methods.map((paymentMethod) => ({ + id: paymentMethod.item, + label: paymentMethod.label, + isSelected: false, + item: paymentMethod.item, + })); + this.showPopup(SelectionPopup, { + title: this.env._t("Pay with: "), + list: selectionList, + }).then(({ confirmed, payload: selectedPaymentMethod }) => { + if (confirmed) { + parsed_result.payment_method_id = selectedPaymentMethod; + this.credit_code_transaction(parsed_result); + } else { + this.credit_code_cancel(); + } + }); + } + }, + remove_paymentline_by_ref(line) { + this.env.pos.get_order().remove_paymentline(line); + numberBuffer.reset(); + this.render(); + }, + do_reversal(line, is_voidsale, old_deferred, retry_nr) { + var def = old_deferred || new $.Deferred(); + var self = this; + retry_nr = retry_nr || 0; - var response = self.env.pos.decodeMercuryResponse(data); + // show the transaction popup. + // the transaction deferred is used to update transaction status + this.showPopup(PaymentTransactionPopup, { + transaction: def, + }); - if (!is_voidsale) { - if (response.status != "Approved" || response.message != "REVERSED") { - // reversal was not successful, send voidsale - self.do_reversal(line, true); - } else { - // reversal was successful - def.resolve({ - message: self.env._t("Reversal succeeded"), - }); + var request_data = _.extend( + { + transaction_type: "Credit", + transaction_code: "VoidSaleByRecordNo", + }, + line.mercury_data + ); - self.remove_paymentline_by_ref(line); - } - } else { - // voidsale ended, nothing more we can do - if (response.status === "Approved") { - def.resolve({ - message: self.env._t("VoidSale succeeded"), - }); + var message = ""; + var rpc_method = ""; - self.remove_paymentline_by_ref(line); - } else { - def.resolve({ - message: "Error " + response.error + ":
" + response.message, - }); - } - } - }) - .catch(function () { - self.retry_mercury_transaction(def, null, retry_nr, false, self.do_reversal, [ + if (is_voidsale) { + message = this.env._t("Reversal failed, sending VoidSale..."); + rpc_method = "do_voidsale"; + } else { + message = this.env._t("Sending reversal..."); + rpc_method = "do_reversal"; + } + + if (!old_deferred) { + def.notify({ + message: message, + }); + } + + this.rpc( + { + model: "pos_mercury.mercury_transaction", + method: rpc_method, + args: [request_data], + }, + { + timeout: self.server_timeout_in_ms, + } + ) + .then(function (data) { + if (data === "timeout") { + self.retry_mercury_transaction(def, null, retry_nr, true, self.do_reversal, [ line, is_voidsale, def, retry_nr + 1, ]); - }); + return; + } + + if (data === "internal error") { + def.resolve({ + message: self.env._t("Odoo error while processing transaction."), + }); + return; + } + + var response = self.env.pos.decodeMercuryResponse(data); + + if (!is_voidsale) { + if (response.status != "Approved" || response.message != "REVERSED") { + // reversal was not successful, send voidsale + self.do_reversal(line, true); + } else { + // reversal was successful + def.resolve({ + message: self.env._t("Reversal succeeded"), + }); + + self.remove_paymentline_by_ref(line); + } + } else { + // voidsale ended, nothing more we can do + if (response.status === "Approved") { + def.resolve({ + message: self.env._t("VoidSale succeeded"), + }); + + self.remove_paymentline_by_ref(line); + } else { + def.resolve({ + message: "Error " + response.error + ":
" + response.message, + }); + } + } + }) + .catch(function () { + self.retry_mercury_transaction(def, null, retry_nr, false, self.do_reversal, [ + line, + is_voidsale, + def, + retry_nr + 1, + ]); + }); + }, + /** + * @override + */ + deletePaymentLine(event) { + const { cid } = event.detail; + const line = this.paymentLines.find((line) => line.cid === cid); + if (line.mercury_data) { + this.do_reversal(line, false); + } else { + this._super(event); } - - /** - * @override - */ - deletePaymentLine(event) { - const { cid } = event.detail; - const line = this.paymentLines.find((line) => line.cid === cid); - if (line.mercury_data) { - this.do_reversal(line, false); - } else { - super.deletePaymentLine(event); - } + }, + /** + * @override + */ + addNewPaymentLine({ detail: paymentMethod }) { + const order = this.env.pos.get_order(); + const res = this._super(...arguments); + if (res && paymentMethod.pos_mercury_config_id) { + order.selected_paymentline.mercury_swipe_pending = true; + order.trigger("change", order); + this.render(); } - - /** - * @override - */ - addNewPaymentLine({ detail: paymentMethod }) { - const order = this.env.pos.get_order(); - const res = super.addNewPaymentLine(...arguments); - if (res && paymentMethod.pos_mercury_config_id) { - order.selected_paymentline.mercury_swipe_pending = true; - order.trigger("change", order); - this.render(); - } - } - }; - -Registries.Component.extend(PaymentScreen, PosMercuryPaymentScreen); - -export default PaymentScreen; + }, +}); diff --git a/addons/pos_mercury/static/src/js/PaymentScreenPaymentLines.js b/addons/pos_mercury/static/src/js/PaymentScreenPaymentLines.js index c2e9be0e948..1f7a532ba90 100644 --- a/addons/pos_mercury/static/src/js/PaymentScreenPaymentLines.js +++ b/addons/pos_mercury/static/src/js/PaymentScreenPaymentLines.js @@ -1,28 +1,23 @@ /** @odoo-module */ -import PaymentScreenPaymentLines from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreenPaymentLines"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreenPaymentLines } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreenPaymentLines"; +import { patch } from "@web/core/utils/patch"; -const PosMercuryPaymentLines = (PaymentScreenPaymentLines) => - class extends PaymentScreenPaymentLines { - /** - * @override - */ - selectedLineClass(line) { - return Object.assign({}, super.selectedLineClass(line), { - o_pos_mercury_swipe_pending: line.mercury_swipe_pending, - }); - } - /** - * @override - */ - unselectedLineClass(line) { - return Object.assign({}, super.unselectedLineClass(line), { - o_pos_mercury_swipe_pending: line.mercury_swipe_pending, - }); - } - }; - -Registries.Component.extend(PaymentScreenPaymentLines, PosMercuryPaymentLines); - -export default PaymentScreenPaymentLines; +patch(PaymentScreenPaymentLines.prototype, "pos_mercury.PaymentScreenPaymentLines", { + /** + * @override + */ + selectedLineClass(line) { + return Object.assign({}, this._super(line), { + o_pos_mercury_swipe_pending: line.mercury_swipe_pending, + }); + }, + /** + * @override + */ + unselectedLineClass(line) { + return Object.assign({}, this._super(line), { + o_pos_mercury_swipe_pending: line.mercury_swipe_pending, + }); + }, +}); diff --git a/addons/pos_mercury/static/src/js/PaymentTransactionPopup.js b/addons/pos_mercury/static/src/js/PaymentTransactionPopup.js index eac1536d78f..e558af77fb2 100644 --- a/addons/pos_mercury/static/src/js/PaymentTransactionPopup.js +++ b/addons/pos_mercury/static/src/js/PaymentTransactionPopup.js @@ -1,12 +1,19 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; const { useState } = owl; -class PaymentTransactionPopup extends AbstractAwaitablePopup { +export class PaymentTransactionPopup extends AbstractAwaitablePopup { + static template = "PaymentTransactionPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + title: _lt("Online Payment"), + body: "", + cancelKey: false, + }; + setup() { super.setup(); this.state = useState({ message: "", confirmButtonIsShown: false }); @@ -26,14 +33,3 @@ class PaymentTransactionPopup extends AbstractAwaitablePopup { }); } } -PaymentTransactionPopup.template = "PaymentTransactionPopup"; -PaymentTransactionPopup.defaultProps = { - confirmText: _lt("Ok"), - title: _lt("Online Payment"), - body: "", - cancelKey: false, -}; - -Registries.Component.add(PaymentTransactionPopup); - -export default PaymentTransactionPopup; diff --git a/addons/pos_mercury/static/src/js/PosPopupController.js b/addons/pos_mercury/static/src/js/PosPopupController.js new file mode 100644 index 00000000000..e408bf835a9 --- /dev/null +++ b/addons/pos_mercury/static/src/js/PosPopupController.js @@ -0,0 +1,9 @@ +/* @odoo-module */ + +import { PosPopupController } from "@point_of_sale/js/Popups/PosPopupController"; +import { patch } from "@web/core/utils/patch"; +import { PaymentTransactionPopup } from "./PaymentTransactionPopup"; + +patch(PosPopupController, "pos_mercury.PosPopupController", { + components: { ...PosPopupController.components, PaymentTransactionPopup }, +}); diff --git a/addons/pos_mercury/static/src/js/ProductScreen.js b/addons/pos_mercury/static/src/js/ProductScreen.js index a454ea8357e..46088f624f1 100644 --- a/addons/pos_mercury/static/src/js/ProductScreen.js +++ b/addons/pos_mercury/static/src/js/ProductScreen.js @@ -1,24 +1,20 @@ /** @odoo-module */ -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; +import { patch } from "@web/core/utils/patch"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; -const PosMercuryProductScreen = (ProductScreen) => - class extends ProductScreen { - setup() { - super.setup(); - useBarcodeReader({ - credit: this.credit_error_action, - }); - } - credit_error_action() { - this.showPopup("ErrorPopup", { - body: this.env._t("Go to payment screen to use cards"), - }); - } - }; - -Registries.Component.extend(ProductScreen, PosMercuryProductScreen); - -export default ProductScreen; +patch(ProductScreen.prototype, "pos_mercury.ProductScreen", { + setup() { + this._super(...arguments); + useBarcodeReader({ + credit: this.credit_error_action, + }); + }, + credit_error_action() { + this.showPopup(ErrorPopup, { + body: this.env._t("Go to payment screen to use cards"), + }); + }, +}); diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js index 9a831da5ee4..b76285d9166 100644 --- a/addons/pos_mercury/static/src/js/pos_mercury.js +++ b/addons/pos_mercury/static/src/js/pos_mercury.js @@ -1,146 +1,140 @@ /** @odoo-module */ import { PosGlobalState, Order, Payment } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const PosMercuryPosGlobalState = (PosGlobalState) => - class PosMercuryPosGlobalState extends PosGlobalState { - getOnlinePaymentMethods() { - var online_payment_methods = []; +patch(PosGlobalState.prototype, "pos_mercury.PosGlobalState", { + getOnlinePaymentMethods() { + var online_payment_methods = []; - $.each(this.payment_methods, function (i, payment_method) { - if (payment_method.pos_mercury_config_id) { - online_payment_methods.push({ - label: payment_method.name, - item: payment_method.id, - }); - } - }); - - return online_payment_methods; - } - decodeMagtek(magtekInput) { - // Regular expression to identify and extract data from the track 1 & 2 of the magnetic code - // FIXME: ` -_` is a character range from 32 to 95 which contains a lot of things and also covers `A-Z` - // this is almost certainly a mistake. - var _track1_regex = /%B?([0-9]*)\^([A-Z/ -_]*)\^([0-9]{4})(.{3})([^?]+)\?/; - - var track1 = magtekInput.match(_track1_regex); - var magtek_generated = magtekInput.split("|"); - - var to_return = {}; - try { - track1.shift(); // get rid of complete match - to_return["number"] = track1.shift().substr(-4); - to_return["name"] = track1.shift(); - track1.shift(); // expiration date - track1.shift(); // service code - track1.shift(); // discretionary data - track1.shift(); // zero pad - - magtek_generated.shift(); // track1 and track2 - magtek_generated.shift(); // clear text crc - magtek_generated.shift(); // encryption counter - to_return["encrypted_block"] = magtek_generated.shift(); - magtek_generated.shift(); // enc session id - magtek_generated.shift(); // device serial - magtek_generated.shift(); // magneprint data - magtek_generated.shift(); // magneprint status - magtek_generated.shift(); // enc track3 - to_return["encrypted_key"] = magtek_generated.shift(); - magtek_generated.shift(); // enc track1 - magtek_generated.shift(); // reader enc status - - return to_return; - } catch { - return 0; + $.each(this.payment_methods, function (i, payment_method) { + if (payment_method.pos_mercury_config_id) { + online_payment_methods.push({ + label: payment_method.name, + item: payment_method.id, + }); } - } - decodeMercuryResponse(data) { - // get rid of xml version declaration and just keep the RStream - // from the response because the xml contains two version - // declarations. One for the SOAP, and one for the content. Maybe - // we should unpack the SOAP layer in python? - data = data.replace(/.*<\?xml version="1.0"\?>/, ""); - data = data.replace(/<\/CreditTransactionResult>.*/, ""); + }); - var xml = $($.parseXML(data)); - var cmd_response = xml.find("CmdResponse"); - var tran_response = xml.find("TranResponse"); + return online_payment_methods; + }, + decodeMagtek(magtekInput) { + // Regular expression to identify and extract data from the track 1 & 2 of the magnetic code + // FIXME: ` -_` is a character range from 32 to 95 which contains a lot of things and also covers `A-Z` + // this is almost certainly a mistake. + var _track1_regex = /%B?([0-9]*)\^([A-Z/ -_]*)\^([0-9]{4})(.{3})([^?]+)\?/; - return { - status: cmd_response.find("CmdStatus").text(), - message: cmd_response.find("TextResponse").text(), - error: cmd_response.find("DSIXReturnCode").text(), - card_type: tran_response.find("CardType").text(), - auth_code: tran_response.find("AuthCode").text(), - acq_ref_data: tran_response.find("AcqRefData").text(), - process_data: tran_response.find("ProcessData").text(), - invoice_no: tran_response.find("InvoiceNo").text(), - ref_no: tran_response.find("RefNo").text(), - record_no: tran_response.find("RecordNo").text(), - purchase: parseFloat(tran_response.find("Purchase").text()), - authorize: parseFloat(tran_response.find("Authorize").text()), - }; - } - }; -Registries.Model.extend(PosGlobalState, PosMercuryPosGlobalState); + var track1 = magtekInput.match(_track1_regex); + var magtek_generated = magtekInput.split("|"); -const PosMercuryPayment = (Payment) => - class PosMercuryPayment extends Payment { - init_from_JSON(json) { - super.init_from_JSON(...arguments); + var to_return = {}; + try { + track1.shift(); // get rid of complete match + to_return["number"] = track1.shift().substr(-4); + to_return["name"] = track1.shift(); + track1.shift(); // expiration date + track1.shift(); // service code + track1.shift(); // discretionary data + track1.shift(); // zero pad - this.paid = json.paid; - this.mercury_card_number = json.mercury_card_number; - this.mercury_card_brand = json.mercury_card_brand; - this.mercury_card_owner_name = json.mercury_card_owner_name; - this.mercury_ref_no = json.mercury_ref_no; - this.mercury_record_no = json.mercury_record_no; - this.mercury_invoice_no = json.mercury_invoice_no; - this.mercury_auth_code = json.mercury_auth_code; - this.mercury_data = json.mercury_data; - this.mercury_swipe_pending = json.mercury_swipe_pending; + magtek_generated.shift(); // track1 and track2 + magtek_generated.shift(); // clear text crc + magtek_generated.shift(); // encryption counter + to_return["encrypted_block"] = magtek_generated.shift(); + magtek_generated.shift(); // enc session id + magtek_generated.shift(); // device serial + magtek_generated.shift(); // magneprint data + magtek_generated.shift(); // magneprint status + magtek_generated.shift(); // enc track3 + to_return["encrypted_key"] = magtek_generated.shift(); + magtek_generated.shift(); // enc track1 + magtek_generated.shift(); // reader enc status - this.set_credit_card_name(); + return to_return; + } catch { + return 0; } - export_as_JSON() { - return _.extend(super.export_as_JSON(...arguments), { - paid: this.paid, - mercury_card_number: this.mercury_card_number, - mercury_card_brand: this.mercury_card_brand, - mercury_card_owner_name: this.mercury_card_owner_name, - mercury_ref_no: this.mercury_ref_no, - mercury_record_no: this.mercury_record_no, - mercury_invoice_no: this.mercury_invoice_no, - mercury_auth_code: this.mercury_auth_code, - mercury_data: this.mercury_data, - mercury_swipe_pending: this.mercury_swipe_pending, - }); - } - set_credit_card_name() { - if (this.mercury_card_number) { - this.name = this.mercury_card_brand + " (****" + this.mercury_card_number + ")"; - } - } - is_done() { - var res = super.is_done(...arguments); - return res && !this.mercury_swipe_pending; - } - export_for_printing() { - const result = super.export_for_printing(...arguments); - result.mercury_data = this.mercury_data; - result.mercury_auth_code = this.mercury_auth_code; - return result; - } - }; -Registries.Model.extend(Payment, PosMercuryPayment); + }, + decodeMercuryResponse(data) { + // get rid of xml version declaration and just keep the RStream + // from the response because the xml contains two version + // declarations. One for the SOAP, and one for the content. Maybe + // we should unpack the SOAP layer in python? + data = data.replace(/.*<\?xml version="1.0"\?>/, ""); + data = data.replace(/<\/CreditTransactionResult>.*/, ""); -const PosMercuryOrder = (Order) => - class PosMercuryOrder extends Order { - electronic_payment_in_progress() { - var res = super.electronic_payment_in_progress(...arguments); - return res || this.get_paymentlines().some((line) => line.mercury_swipe_pending); + var xml = $($.parseXML(data)); + var cmd_response = xml.find("CmdResponse"); + var tran_response = xml.find("TranResponse"); + + return { + status: cmd_response.find("CmdStatus").text(), + message: cmd_response.find("TextResponse").text(), + error: cmd_response.find("DSIXReturnCode").text(), + card_type: tran_response.find("CardType").text(), + auth_code: tran_response.find("AuthCode").text(), + acq_ref_data: tran_response.find("AcqRefData").text(), + process_data: tran_response.find("ProcessData").text(), + invoice_no: tran_response.find("InvoiceNo").text(), + ref_no: tran_response.find("RefNo").text(), + record_no: tran_response.find("RecordNo").text(), + purchase: parseFloat(tran_response.find("Purchase").text()), + authorize: parseFloat(tran_response.find("Authorize").text()), + }; + }, +}); + +patch(Payment.prototype, "pos_mercury.Payment", { + init_from_JSON(json) { + this._super(...arguments); + + this.paid = json.paid; + this.mercury_card_number = json.mercury_card_number; + this.mercury_card_brand = json.mercury_card_brand; + this.mercury_card_owner_name = json.mercury_card_owner_name; + this.mercury_ref_no = json.mercury_ref_no; + this.mercury_record_no = json.mercury_record_no; + this.mercury_invoice_no = json.mercury_invoice_no; + this.mercury_auth_code = json.mercury_auth_code; + this.mercury_data = json.mercury_data; + this.mercury_swipe_pending = json.mercury_swipe_pending; + + this.set_credit_card_name(); + }, + export_as_JSON() { + return _.extend(this._super(...arguments), { + paid: this.paid, + mercury_card_number: this.mercury_card_number, + mercury_card_brand: this.mercury_card_brand, + mercury_card_owner_name: this.mercury_card_owner_name, + mercury_ref_no: this.mercury_ref_no, + mercury_record_no: this.mercury_record_no, + mercury_invoice_no: this.mercury_invoice_no, + mercury_auth_code: this.mercury_auth_code, + mercury_data: this.mercury_data, + mercury_swipe_pending: this.mercury_swipe_pending, + }); + }, + set_credit_card_name() { + if (this.mercury_card_number) { + this.name = this.mercury_card_brand + " (****" + this.mercury_card_number + ")"; } - }; -Registries.Model.extend(Order, PosMercuryOrder); + }, + is_done() { + var res = this._super(...arguments); + return res && !this.mercury_swipe_pending; + }, + export_for_printing() { + const result = this._super(...arguments); + result.mercury_data = this.mercury_data; + result.mercury_auth_code = this.mercury_auth_code; + return result; + }, +}); + +patch(Order.prototype, "pos_mercury.Order", { + electronic_payment_in_progress() { + var res = this._super(...arguments); + return res || this.get_paymentlines().some((line) => line.mercury_swipe_pending); + }, +}); diff --git a/addons/pos_restaurant/static/src/app/navbar.js b/addons/pos_restaurant/static/src/app/navbar.js new file mode 100644 index 00000000000..b1cc74460a3 --- /dev/null +++ b/addons/pos_restaurant/static/src/app/navbar.js @@ -0,0 +1,9 @@ +/** @odoo-module */ + +import { Navbar } from "@point_of_sale/app/navbar/navbar"; +import { BackToFloorButton } from "../js/ChromeWidgets/BackToFloorButton"; +import { patch } from "@web/core/utils/patch"; + +patch(Navbar, "pos_restaurant.Navbar", { + components: { ...Navbar.components, BackToFloorButton }, +}); diff --git a/addons/pos_restaurant/static/src/js/Chrome.js b/addons/pos_restaurant/static/src/js/Chrome.js index d8c436ad463..2827cbacb4e 100644 --- a/addons/pos_restaurant/static/src/js/Chrome.js +++ b/addons/pos_restaurant/static/src/js/Chrome.js @@ -1,106 +1,101 @@ /** @odoo-module */ -import Chrome from "@point_of_sale/js/Chrome"; -import Registries from "@point_of_sale/js/Registries"; +import { Chrome } from "@point_of_sale/js/Chrome"; +import { patch } from "@web/core/utils/patch"; const NON_IDLE_EVENTS = "mousemove mousedown touchstart touchend touchmove click scroll keypress".split(/\s+/); let IDLE_TIMER_SETTER; -const PosResChrome = (Chrome) => - class extends Chrome { - /** - * @override - */ - async start() { - await super.start(); - if (this.env.pos.config.iface_floorplan) { - this._setActivityListeners(); - } +patch(Chrome.prototype, "pos_restaurant.Chrome", { + /** + * @override + */ + async start() { + await this._super(); + if (this.env.pos.config.iface_floorplan) { + this._setActivityListeners(); } - /** - * @override - * Do not set `FloorScreen` to the order. - */ - _setScreenData(name) { - if (name === "FloorScreen") { - return; - } - super._setScreenData(...arguments); + }, + /** + * @override + * Do not set `FloorScreen` to the order. + */ + _setScreenData(name) { + if (name === "FloorScreen") { + return; } - /** - * @override - * `FloorScreen` is the start screen if there are floors. - */ - get startScreen() { - if (this.env.pos.config.iface_floorplan) { - const table = this.env.pos.table; - return { name: "FloorScreen", props: { floor: table ? table.floor : null } }; - } else { - return super.startScreen; - } + this._super(...arguments); + }, + /** + * @override + * `FloorScreen` is the start screen if there are floors. + */ + get startScreen() { + if (this.env.pos.config.iface_floorplan) { + const table = this.env.pos.table; + return { name: "FloorScreen", props: { floor: table ? table.floor : null } }; + } else { + return this._super(); } - _setActivityListeners() { - IDLE_TIMER_SETTER = this._setIdleTimer.bind(this); + }, + _setActivityListeners() { + IDLE_TIMER_SETTER = this._setIdleTimer.bind(this); + for (const event of NON_IDLE_EVENTS) { + window.addEventListener(event, IDLE_TIMER_SETTER); + } + }, + _setIdleTimer() { + clearTimeout(this.idleTimer); + if (this._shouldResetIdleTimer()) { + this.idleTimer = setTimeout(() => { + this._actionAfterIdle(); + }, 60000); + } + }, + async _actionAfterIdle() { + const isPopupClosed = await new Promise((resolve) => + this.env.posbus.trigger("close-popups-but-error", { resolve }) + ); + if (isPopupClosed) { + if (this.state.tempScreen) { + this.trigger("close-temp-screen"); + } + const table = this.env.pos.table; + const order = this.env.pos.get_order(); + if (order && order.get_screen_data().name === "ReceiptScreen") { + // When the order is finalized, we can safely remove it from the memory + // We check that it's in ReceiptScreen because we want to keep the order if it's in a tipping state + this.env.pos.removeOrder(order); + } + this.showScreen("FloorScreen", { floor: table ? table.floor : null }); + } + }, + _shouldResetIdleTimer() { + const stayPaymentScreen = + this.mainScreen.name === "PaymentScreen" && + this.env.pos.get_order().paymentlines.length > 0; + return ( + this.env.pos.config.iface_floorplan && + !stayPaymentScreen && + this.mainScreen.name !== "FloorScreen" + ); + }, + __showScreen() { + this._super(...arguments); + this._setIdleTimer(); + }, + /** + * @override + * Before closing pos, we remove the event listeners set on window + * for detecting activities outside FloorScreen. + */ + async _closePos() { + if (IDLE_TIMER_SETTER) { for (const event of NON_IDLE_EVENTS) { - window.addEventListener(event, IDLE_TIMER_SETTER); + window.removeEventListener(event, IDLE_TIMER_SETTER); } } - _setIdleTimer() { - clearTimeout(this.idleTimer); - if (this._shouldResetIdleTimer()) { - this.idleTimer = setTimeout(() => { - this._actionAfterIdle(); - }, 60000); - } - } - async _actionAfterIdle() { - const isPopupClosed = await new Promise((resolve) => - this.env.posbus.trigger("close-popups-but-error", { resolve }) - ); - if (isPopupClosed) { - if (this.state.tempScreen) { - this.trigger("close-temp-screen"); - } - const table = this.env.pos.table; - const order = this.env.pos.get_order(); - if (order && order.get_screen_data().name === "ReceiptScreen") { - // When the order is finalized, we can safely remove it from the memory - // We check that it's in ReceiptScreen because we want to keep the order if it's in a tipping state - this.env.pos.removeOrder(order); - } - this.showScreen("FloorScreen", { floor: table ? table.floor : null }); - } - } - _shouldResetIdleTimer() { - const stayPaymentScreen = - this.mainScreen.name === "PaymentScreen" && - this.env.pos.get_order().paymentlines.length > 0; - return ( - this.env.pos.config.iface_floorplan && - !stayPaymentScreen && - this.mainScreen.name !== "FloorScreen" - ); - } - __showScreen() { - super.__showScreen(...arguments); - this._setIdleTimer(); - } - /** - * @override - * Before closing pos, we remove the event listeners set on window - * for detecting activities outside FloorScreen. - */ - async _closePos() { - if (IDLE_TIMER_SETTER) { - for (const event of NON_IDLE_EVENTS) { - window.removeEventListener(event, IDLE_TIMER_SETTER); - } - } - await super._closePos(); - } - }; - -Registries.Component.extend(Chrome, PosResChrome); - -export default Chrome; + await this._super(); + }, +}); diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js index 9b23c218cbf..e9dae09141f 100644 --- a/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js @@ -1,10 +1,11 @@ /** @odoo-module */ import { usePos } from "@point_of_sale/app/pos_store"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class BackToFloorButton extends PosComponent { + static template = "BackToFloorButton"; -class BackToFloorButton extends PosComponent { setup() { super.setup(); this.pos = usePos(); @@ -22,8 +23,3 @@ class BackToFloorButton extends PosComponent { this.showScreen("FloorScreen", { floor: this.floor }); } } -BackToFloorButton.template = "BackToFloorButton"; - -Registries.Component.add(BackToFloorButton); - -export default BackToFloorButton; diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js index 3a0d6df6dad..cde5868dbbc 100644 --- a/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js @@ -1,43 +1,38 @@ /** @odoo-module */ -import TicketButton from "@point_of_sale/js/ChromeWidgets/TicketButton"; -import Registries from "@point_of_sale/js/Registries"; +import { TicketButton } from "@point_of_sale/js/ChromeWidgets/TicketButton"; +import { patch } from "@web/core/utils/patch"; -const PosResTicketButton = (TicketButton) => - class extends TicketButton { - async onClick() { - if ( - this.env.pos.config.iface_floorplan && - !this.isTicketScreenShown && - !this.env.pos.table - ) { - try { - this.env.pos.setLoadingOrderState(true); - await this.env.pos._syncAllOrdersFromServer(); - } finally { - this.env.pos.setLoadingOrderState(false); - this.showScreen("TicketScreen"); - } - } else { - super.onClick(); +patch(TicketButton.prototype, "pos_restaurant.TicketButton", { + async onClick() { + if ( + this.env.pos.config.iface_floorplan && + !this.isTicketScreenShown && + !this.env.pos.table + ) { + try { + this.env.pos.setLoadingOrderState(true); + await this.env.pos._syncAllOrdersFromServer(); + } finally { + this.env.pos.setLoadingOrderState(false); + this.showScreen("TicketScreen"); } + } else { + this._super(); } - /** - * If no table is set to pos, which means the current main screen - * is floor screen, then the order count should be based on all the orders. - */ - get count() { - if (!this.env.pos || !this.env.pos.config) { - return 0; - } - if (this.env.pos.config.iface_floorplan && this.env.pos.table) { - return this.env.pos.getTableOrders(this.env.pos.table.id).length; - } else { - return super.count; - } + }, + /** + * If no table is set to pos, which means the current main screen + * is floor screen, then the order count should be based on all the orders. + */ + get count() { + if (!this.env.pos || !this.env.pos.config) { + return 0; } - }; - -Registries.Component.extend(TicketButton, PosResTicketButton); - -export default TicketButton; + if (this.env.pos.config.iface_floorplan && this.env.pos.table) { + return this.env.pos.getTableOrders(this.env.pos.table.id).length; + } else { + return this._super(); + } + }, +}); diff --git a/addons/pos_restaurant/static/src/js/Popups/PosPopupController.js b/addons/pos_restaurant/static/src/js/Popups/PosPopupController.js index e6e3174aa38..7dd2a06613f 100644 --- a/addons/pos_restaurant/static/src/js/Popups/PosPopupController.js +++ b/addons/pos_restaurant/static/src/js/Popups/PosPopupController.js @@ -1,28 +1,25 @@ /* @odoo-module */ -import PosPopupController from "@point_of_sale/js/Popups/PosPopupController"; -import Registries from "@point_of_sale/js/Registries"; +import { PosPopupController } from "@point_of_sale/js/Popups/PosPopupController"; import { useBus } from "@web/core/utils/hooks"; +import { patch } from "@web/core/utils/patch"; -export const PosResPopupController = (PosPopupController) => - class extends PosPopupController { - setup() { - super.setup(); - useBus(this.env.posbus, "close-popups-but-error", this._closePopupsButError); - } - _closePopupsButError(event) { - const { resolve } = event.detail; - const isErrorPopupOpen = this.popups.some((popup) => - popup.name.toLowerCase().includes("error") - ); - if (!isErrorPopupOpen) { - for (const popup of this.popups) { - popup.props.resolve(false); - } - this.popups.length = 0; // clearing the array but keep the useState +patch(PosPopupController.prototype, "pos_restaurant.PosPopupController", { + setup() { + this._super(...arguments); + useBus(this.env.posbus, "close-popups-but-error", this._closePopupsButError); + }, + _closePopupsButError(event) { + const { resolve } = event.detail; + const isErrorPopupOpen = this.popups.some((popup) => + popup.name.toLowerCase().includes("error") + ); + if (!isErrorPopupOpen) { + for (const popup of this.popups) { + popup.props.resolve(false); } - resolve(!isErrorPopupOpen); + this.popups.length = 0; // clearing the array but keep the useState } - }; - -Registries.Component.extend(PosPopupController, PosResPopupController); + resolve(!isErrorPopupOpen); + }, +}); diff --git a/addons/pos_restaurant/static/src/js/Resizeable.js b/addons/pos_restaurant/static/src/js/Resizeable.js index 12b87f51abb..1089fbb87c5 100644 --- a/addons/pos_restaurant/static/src/js/Resizeable.js +++ b/addons/pos_restaurant/static/src/js/Resizeable.js @@ -1,12 +1,13 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { onMounted, useExternalListener } = owl; -class Resizeable extends PosComponent { +export class Resizeable extends PosComponent { + static template = "Resizeable"; + setup() { super.setup(); useExternalListener(document, "mousemove", this.resizeN); @@ -322,8 +323,3 @@ class Resizeable extends PosComponent { this.trigger("resize-end", { size, loc }); } } -Resizeable.template = "Resizeable"; - -Registries.Component.add(Resizeable); - -export default Resizeable; diff --git a/addons/pos_restaurant/static/src/js/Screens/BillScreen.js b/addons/pos_restaurant/static/src/js/Screens/BillScreen.js index a42149bdcca..1915a1d9499 100644 --- a/addons/pos_restaurant/static/src/js/Screens/BillScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/BillScreen.js @@ -1,29 +1,24 @@ /** @odoo-module */ -import ReceiptScreen from "@point_of_sale/js/Screens/ReceiptScreen/ReceiptScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { ReceiptScreen } from "@point_of_sale/js/Screens/ReceiptScreen/ReceiptScreen"; +import { registry } from "@web/core/registry"; -const BillScreen = (ReceiptScreen) => { - class BillScreen extends ReceiptScreen { - confirm() { - this.props.resolve({ confirmed: true, payload: null }); - this.trigger("close-temp-screen"); - } - whenClosing() { - this.confirm(); - } - /** - * @override - */ - async printReceipt() { - await super.printReceipt(); - this.currentOrder._printed = false; - } +export class BillScreen extends ReceiptScreen { + static template = "BillScreen"; + confirm() { + this.props.resolve({ confirmed: true, payload: null }); + this.trigger("close-temp-screen"); } - BillScreen.template = "BillScreen"; - return BillScreen; -}; + whenClosing() { + this.confirm(); + } + /** + * @override + */ + async printReceipt() { + await super.printReceipt(); + this.currentOrder._printed = false; + } +} -Registries.Component.addByExtending(BillScreen, ReceiptScreen); - -export default BillScreen; +registry.category("pos_screens").add("BillScreen", BillScreen); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js index ddc398dc2df..152875aa8de 100644 --- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js @@ -1,18 +1,14 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { useState } = owl; -class EditBar extends PosComponent { +export class EditBar extends PosComponent { + static template = "EditBar"; + setup() { super.setup(); this.state = useState({ isColorPicker: false }); } } -EditBar.template = "EditBar"; - -Registries.Component.add(EditBar); - -export default EditBar; diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js index 8bd8cac600f..5ad6f57d60d 100644 --- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js @@ -1,12 +1,17 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +import { Draggable } from "@point_of_sale/js/Misc/Draggable"; +import { Resizeable } from "../../Resizeable"; const { onMounted, onPatched } = owl; -class EditableTable extends PosComponent { +export class EditableTable extends PosComponent { + static template = "EditableTable"; + static components = { Draggable, Resizeable }; + setup() { super.setup(); useListener("resize-end", this._onResizeEnd); @@ -52,8 +57,3 @@ class EditableTable extends PosComponent { this.props.onSaveTable(this.props.table); } } -EditableTable.template = "EditableTable"; - -Registries.Component.add(EditableTable); - -export default EditableTable; diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js index 2ff1a6d2c0d..986a31353af 100644 --- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js @@ -1,12 +1,24 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { debounce } from "@web/core/utils/timing"; +import { registry } from "@web/core/registry"; + +import { TextInputPopup } from "@point_of_sale/js/Popups/TextInputPopup"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; + +import { EditableTable } from "./EditableTable"; +import { EditBar } from "./EditBar"; +import { TableWidget } from "./TableWidget"; const { onPatched, onMounted, onWillUnmount, useRef, useState } = owl; -class FloorScreen extends PosComponent { +export class FloorScreen extends PosComponent { + static components = { EditableTable, EditBar, TableWidget }; + static template = "FloorScreen"; + static hideOrderSelector = true; + /** * @param {Object} props * @param {Object} props.floor @@ -219,7 +231,7 @@ class FloorScreen extends PosComponent { if (!selectedTable) { return; } - const { confirmed, payload: newName } = await this.showPopup("TextInputPopup", { + const { confirmed, payload: newName } = await this.showPopup(TextInputPopup, { startingValue: selectedTable.name, title: this.env._t("Table Name ?"), }); @@ -236,7 +248,7 @@ class FloorScreen extends PosComponent { if (!selectedTable) { return; } - const { confirmed, payload: inputNumber } = await this.showPopup("NumberPopup", { + const { confirmed, payload: inputNumber } = await this.showPopup(NumberPopup, { startingValue: selectedTable.seats, cheap: true, title: this.env._t("Number of Seats ?"), @@ -277,7 +289,7 @@ class FloorScreen extends PosComponent { if (!this.selectedTable) { return; } - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Are you sure ?"), body: this.env._t("Removing a table cannot be undone"), }); @@ -306,9 +318,5 @@ class FloorScreen extends PosComponent { this.env.pos.TICKET_SCREEN_STATE.syncedOrders.cache = {}; } } -FloorScreen.template = "FloorScreen"; -FloorScreen.hideOrderSelector = true; -Registries.Component.add(FloorScreen); - -export default FloorScreen; +registry.category("pos_screens").add("FloorScreen", FloorScreen); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js index e0004d66160..ea39ae668f7 100644 --- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; /** * props: { @@ -9,7 +8,9 @@ import Registries from "@point_of_sale/js/Registries"; * table: table object, * } */ -class TableWidget extends PosComponent { +export class TableWidget extends PosComponent { + static template = "TableWidget"; + setup() { owl.onMounted(this.onMounted); } @@ -77,8 +78,3 @@ class TableWidget extends PosComponent { return hasChangesCount ? { printing: true } : hasSkippedCount ? { skipped: true } : {}; } } -TableWidget.template = "TableWidget"; - -Registries.Component.add(TableWidget); - -export default TableWidget; diff --git a/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js index 84b6c08d266..0fe93c63fe0 100644 --- a/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js @@ -1,47 +1,43 @@ /** @odoo-module */ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const PosResPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - static showBackToFloorButton = true; - setup() { - super.setup(); - useListener("send-payment-adjust", this._sendPaymentAdjust); +patch(PaymentScreen, "pos_restaurant.PaymentScreen", { + showBackToFloorButton: true, +}); + +patch(PaymentScreen.prototype, "pos_restaurant.PaymentScreen", { + setup() { + this._super(...arguments); + useListener("send-payment-adjust", this._sendPaymentAdjust); + }, + async _sendPaymentAdjust({ detail: line }) { + const previous_amount = line.get_amount(); + const amount_diff = line.order.get_total_with_tax() - line.order.get_total_paid(); + line.set_amount(previous_amount + amount_diff); + line.set_payment_status("waiting"); + + const payment_terminal = line.payment_method.payment_terminal; + const isAdjustSuccessful = await payment_terminal.send_payment_adjust(line.cid); + if (isAdjustSuccessful) { + line.set_payment_status("done"); + } else { + line.set_amount(previous_amount); + line.set_payment_status("done"); } - - async _sendPaymentAdjust({ detail: line }) { - const previous_amount = line.get_amount(); - const amount_diff = line.order.get_total_with_tax() - line.order.get_total_paid(); - line.set_amount(previous_amount + amount_diff); - line.set_payment_status("waiting"); - - const payment_terminal = line.payment_method.payment_terminal; - const isAdjustSuccessful = await payment_terminal.send_payment_adjust(line.cid); - if (isAdjustSuccessful) { - line.set_payment_status("done"); - } else { - line.set_amount(previous_amount); - line.set_payment_status("done"); - } + }, + get nextScreen() { + const order = this.currentOrder; + if (!this.env.pos.config.set_tip_after_payment || order.is_tipped) { + return this._super(); } - - get nextScreen() { - const order = this.currentOrder; - if (!this.env.pos.config.set_tip_after_payment || order.is_tipped) { - return super.nextScreen; - } - // Take the first payment method as the main payment. - const mainPayment = order.get_paymentlines()[0]; - if (mainPayment.canBeAdjusted()) { - return "TipScreen"; - } - return super.nextScreen; + // Take the first payment method as the main payment. + const mainPayment = order.get_paymentlines()[0]; + if (mainPayment.canBeAdjusted()) { + return "TipScreen"; } - }; - -Registries.Component.extend(PaymentScreen, PosResPaymentScreen); - -export default PosResPaymentScreen; + return this._super(); + }, +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js index f7eb87b95fc..6d53dbbdedc 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js @@ -1,11 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { TextAreaPopup } from "@point_of_sale/js/Popups/TextAreaPopup"; + +export class OrderlineNoteButton extends PosComponent { + static template = "OrderlineNoteButton"; -class OrderlineNoteButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -18,7 +20,7 @@ class OrderlineNoteButton extends PosComponent { return; } - const { confirmed, payload: inputNote } = await this.showPopup("TextAreaPopup", { + const { confirmed, payload: inputNote } = await this.showPopup(TextAreaPopup, { startingValue: this.selectedOrderline.get_note(), title: this.env._t("Add Internal Note"), }); @@ -28,7 +30,6 @@ class OrderlineNoteButton extends PosComponent { } } } -OrderlineNoteButton.template = "OrderlineNoteButton"; ProductScreen.addControlButton({ component: OrderlineNoteButton, @@ -36,7 +37,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.iface_orderline_notes; }, }); - -Registries.Component.add(OrderlineNoteButton); - -export default OrderlineNoteButton; diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js index 5f46e27e382..a22f8ef785f 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js @@ -1,11 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; -class PrintBillButton extends PosComponent { +export class PrintBillButton extends PosComponent { + static template = "PrintBillButton"; + setup() { super.setup(); useListener("click", this.onClick); @@ -18,7 +19,6 @@ class PrintBillButton extends PosComponent { this.showTempScreen("BillScreen"); } } -PrintBillButton.template = "PrintBillButton"; ProductScreen.addControlButton({ component: PrintBillButton, @@ -26,7 +26,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.iface_printbill; }, }); - -Registries.Component.add(PrintBillButton); - -export default PrintBillButton; diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js index f5cbbcb68f5..0b687865e97 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js @@ -1,11 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; -class SplitBillButton extends PosComponent { +export class SplitBillButton extends PosComponent { + static template = "SplitBillButton"; + setup() { super.setup(); useListener("click", this.onClick); @@ -22,7 +23,6 @@ class SplitBillButton extends PosComponent { this.showScreen("SplitBillScreen"); } } -SplitBillButton.template = "SplitBillButton"; ProductScreen.addControlButton({ component: SplitBillButton, @@ -30,7 +30,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.iface_splitbill; }, }); - -Registries.Component.add(SplitBillButton); - -export default SplitBillButton; diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js index d8df58e212f..63b58318d86 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js @@ -1,8 +1,8 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; /** * IMPROVEMENT: Perhaps this class is quite complicated for its worth. @@ -10,7 +10,9 @@ import Registries from "@point_of_sale/js/Registries"; * Also, the current order changes when the selectedOrder in pos is changed. * After setting new current order, we update the listeners. */ -class SubmitOrderButton extends PosComponent { +export class SubmitOrderButton extends PosComponent { + static template = "SubmitOrderButton"; + setup() { super.setup(); this.clicked = false; //mutex, we don't want to be able to spam the printers @@ -25,7 +27,7 @@ class SubmitOrderButton extends PosComponent { if (isPrintSuccessful) { order.updatePrintedResume(); } else { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Printing failed"), body: this.env._t("Failed in printing the changes in the order"), }); @@ -51,7 +53,6 @@ class SubmitOrderButton extends PosComponent { }; } } -SubmitOrderButton.template = "SubmitOrderButton"; ProductScreen.addControlButton({ component: SubmitOrderButton, @@ -59,7 +60,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.module_pos_restaurant && this.env.pos.unwatched.printers.length; }, }); - -Registries.Component.add(SubmitOrderButton); - -export default SubmitOrderButton; diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js index 4bd1a06ad7f..9d9c2525583 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js @@ -1,11 +1,14 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; + +export class TableGuestsButton extends PosComponent { + static template = "TableGuestsButton"; -class TableGuestsButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -17,7 +20,7 @@ class TableGuestsButton extends PosComponent { return this.currentOrder ? this.currentOrder.getCustomerCount() : 0; } async onClick() { - const { confirmed, payload: inputNumber } = await this.showPopup("NumberPopup", { + const { confirmed, payload: inputNumber } = await this.showPopup(NumberPopup, { startingValue: this.nGuests, cheap: true, title: this.env._t("Guests ?"), @@ -29,7 +32,7 @@ class TableGuestsButton extends PosComponent { // Set the maximum number possible for an integer const max_capacity = 2 ** 31 - 1; if (guestCount > max_capacity) { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Blocked action"), body: _.str.sprintf( this.env._t("You cannot put a number that exceeds %s "), @@ -42,7 +45,6 @@ class TableGuestsButton extends PosComponent { } } } -TableGuestsButton.template = "TableGuestsButton"; ProductScreen.addControlButton({ component: TableGuestsButton, @@ -50,7 +52,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.module_pos_restaurant; }, }); - -Registries.Component.add(TableGuestsButton); - -export default TableGuestsButton; diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js index 7c950027e65..748781b4f12 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js @@ -1,11 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; -class TransferOrderButton extends PosComponent { +export class TransferOrderButton extends PosComponent { + static template = "TransferOrderButton"; + setup() { super.setup(); useListener("click", this.onClick); @@ -15,7 +16,6 @@ class TransferOrderButton extends PosComponent { this.showScreen("FloorScreen"); } } -TransferOrderButton.template = "TransferOrderButton"; ProductScreen.addControlButton({ component: TransferOrderButton, @@ -23,7 +23,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.iface_floorplan; }, }); - -Registries.Component.add(TransferOrderButton); - -export default TransferOrderButton; diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js index e4f81bc217b..95a7c35be4c 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js @@ -1,44 +1,39 @@ /** @odoo-module */ -import Orderline from "@point_of_sale/js/Screens/ProductScreen/Orderline"; -import Registries from "@point_of_sale/js/Registries"; +import { Orderline } from "@point_of_sale/js/Screens/ProductScreen/Orderline"; +import { patch } from "@web/core/utils/patch"; -const PosResOrderline = (Orderline) => - class extends Orderline { - /** - * @override - */ - get addedClasses() { - const res = super.addedClasses; - Object.assign(res, { - dirty: this.props.line.mp_dirty, - skip: this.props.line.mp_skip, - }); - return res; +patch(Orderline.prototype, "pos_restaurant.Orderline", { + /** + * @override + */ + get addedClasses() { + const res = this._super(); + Object.assign(res, { + dirty: this.props.line.mp_dirty, + skip: this.props.line.mp_skip, + }); + return res; + }, + /** + * @override + * if doubleclick, change mp_dirty to mp_skip + * + * IMPROVEMENT: Instead of handling both double click and click in single + * method, perhaps we can separate double click from single click. + */ + selectLine() { + const line = this.props.line; // the orderline + if (this.env.pos.get_order().selected_orderline.id !== line.id) { + this.mp_dbclk_time = new Date().getTime(); + } else if (!this.mp_dbclk_time) { + this.mp_dbclk_time = new Date().getTime(); + } else if (this.mp_dbclk_time + 500 > new Date().getTime()) { + line.set_skip(!line.mp_skip); + this.mp_dbclk_time = 0; + } else { + this.mp_dbclk_time = new Date().getTime(); } - /** - * @override - * if doubleclick, change mp_dirty to mp_skip - * - * IMPROVEMENT: Instead of handling both double click and click in single - * method, perhaps we can separate double click from single click. - */ - selectLine() { - const line = this.props.line; // the orderline - if (this.env.pos.get_order().selected_orderline.id !== line.id) { - this.mp_dbclk_time = new Date().getTime(); - } else if (!this.mp_dbclk_time) { - this.mp_dbclk_time = new Date().getTime(); - } else if (this.mp_dbclk_time + 500 > new Date().getTime()) { - line.set_skip(!line.mp_skip); - this.mp_dbclk_time = 0; - } else { - this.mp_dbclk_time = new Date().getTime(); - } - super.selectLine(); - } - }; - -Registries.Component.extend(Orderline, PosResOrderline); - -export default Orderline; + this._super(); + }, +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ProductScreen.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ProductScreen.js index 485528b7abb..9b95c9738ee 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ProductScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ProductScreen.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { patch } from "@web/core/utils/patch"; patch(ProductScreen, "pos_restaurant.ProductScreen", { diff --git a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js index 92a8a61cf2f..fc20a649a7c 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -1,40 +1,38 @@ /** @odoo-module */ -import ReceiptScreen from "@point_of_sale/js/Screens/ReceiptScreen/ReceiptScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { ReceiptScreen } from "@point_of_sale/js/Screens/ReceiptScreen/ReceiptScreen"; import { usePos } from "@point_of_sale/app/pos_store"; +import { patch } from "@web/core/utils/patch"; import { onWillUnmount } from "@odoo/owl"; -const PosResReceiptScreen = (ReceiptScreen) => - class extends ReceiptScreen { - static showBackToFloorButton = true; - setup() { - super.setup(); - this.pos = usePos(); - onWillUnmount(() => { - // When leaving the receipt screen to the floor screen the order is paid and can be removed - if (this.pos.mainScreen.name === "FloorScreen") { - this.env.pos.removeOrder(this.currentOrder); - } - }); - } - //@override - _addNewOrder() { - if (!this.env.pos.config.iface_floorplan) { - super._addNewOrder(); - } - } - //@override - get nextScreen() { - if (this.env.pos.config.iface_floorplan) { - const table = this.env.pos.table; - return { name: "FloorScreen", props: { floor: table ? table.floor : null } }; - } else { - return super.nextScreen; - } - } - }; +patch(ReceiptScreen, "pos_restaurant.ReceiptScreen", { + showBackToFloorButton: true, +}); -Registries.Component.extend(ReceiptScreen, PosResReceiptScreen); - -export default ReceiptScreen; +patch(ReceiptScreen.prototype, "pos_restaurant.ReceiptScreen", { + setup() { + this._super(...arguments); + this.pos = usePos(); + onWillUnmount(() => { + // When leaving the receipt screen to the floor screen the order is paid and can be removed + if (this.pos.mainScreen.name === "FloorScreen") { + this.env.pos.removeOrder(this.currentOrder); + } + }); + }, + //@override + _addNewOrder() { + if (!this.env.pos.config.iface_floorplan) { + this._super(); + } + }, + //@override + get nextScreen() { + if (this.env.pos.config.iface_floorplan) { + const table = this.env.pos.table; + return { name: "FloorScreen", props: { floor: table ? table.floor : null } }; + } else { + return this._super(); + } + }, +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js index 717f5dc2bb0..64918e48f42 100644 --- a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js @@ -1,13 +1,18 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { useListener } from "@web/core/utils/hooks"; import { Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; + +import { SplitOrderline } from "./SplitOrderline"; +import { registry } from "@web/core/registry"; const { useState, onMounted } = owl; -class SplitBillScreen extends PosComponent { +export class SplitBillScreen extends PosComponent { + static template = "SplitBillScreen"; + static components = { SplitOrderline }; + setup() { super.setup(); useListener("click-line", this.onClickLine); @@ -19,7 +24,7 @@ class SplitBillScreen extends PosComponent { // Should create the new order outside of the constructor because // sequence_number of pos_session is modified. which will trigger // rerendering which will rerender this screen and will be infinite loop. - this.newOrder = Order.create( + this.newOrder = new Order( {}, { pos: this.env.pos, @@ -190,8 +195,5 @@ class SplitBillScreen extends PosComponent { } } } -SplitBillScreen.template = "SplitBillScreen"; -Registries.Component.add(SplitBillScreen); - -export default SplitBillScreen; +registry.category("pos_screens").add("SplitBillScreen", SplitBillScreen); diff --git a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js index 52b86cbbd0f..b3191a17af1 100644 --- a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js @@ -1,10 +1,11 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class SplitOrderline extends PosComponent { + static template = "SplitOrderline"; -class SplitOrderline extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -16,8 +17,3 @@ class SplitOrderline extends PosComponent { this.trigger("click-line", this.props.line); } } -SplitOrderline.template = "SplitOrderline"; - -Registries.Component.add(SplitOrderline); - -export default SplitOrderline; diff --git a/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js index 39353296c3d..85eeb963c96 100644 --- a/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js @@ -1,184 +1,184 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import TicketScreen from "@point_of_sale/js/Screens/TicketScreen/TicketScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { TicketScreen } from "@point_of_sale/js/Screens/TicketScreen/TicketScreen"; import { useAutofocus } from "@web/core/utils/hooks"; +import { patch } from "@web/core/utils/patch"; import { parse } from "web.field_utils"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; const { useState } = owl; -const PosResTicketScreen = (TicketScreen) => - class extends TicketScreen { - close() { - if (!this.env.pos.config.iface_floorplan) { - super.close(); +patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", { + close() { + if (!this.env.pos.config.iface_floorplan) { + this._super(); + } else { + const order = this.env.pos.get_order(); + if (order) { + const { name: screenName } = order.get_screen_data(); + this.showScreen(screenName); } else { - const order = this.env.pos.get_order(); - if (order) { - const { name: screenName } = order.get_screen_data(); - this.showScreen(screenName); - } else { - this.showScreen("FloorScreen"); - } + this.showScreen("FloorScreen"); } } - _getScreenToStatusMap() { - return Object.assign(super._getScreenToStatusMap(), { - PaymentScreen: this.env.pos.config.set_tip_after_payment - ? "OPEN" - : super._getScreenToStatusMap().PaymentScreen, - TipScreen: "TIPPING", - }); + }, + _getScreenToStatusMap() { + return Object.assign(this._super(), { + PaymentScreen: this.env.pos.config.set_tip_after_payment + ? "OPEN" + : this._super().PaymentScreen, + TipScreen: "TIPPING", + }); + }, + getTable(order) { + return `${order.getTable().floor.name} (${order.getTable().name})`; + }, + //@override + _getSearchFields() { + if (!this.env.pos.config.iface_floorplan) { + return this._super(); } - getTable(order) { - return `${order.getTable().floor.name} (${order.getTable().name})`; + return Object.assign({}, this._super(), { + TABLE: { + repr: this.getTable.bind(this), + displayName: this.env._t("Table"), + modelField: "table_id.name", + }, + }); + }, + async _setOrder(order) { + if (!this.env.pos.config.iface_floorplan || this.env.pos.table) { + this._super(order); + } else { + // we came from the FloorScreen + const orderTable = order.getTable(); + await this.env.pos.setTable(orderTable, order.uid); + this.close(); } - //@override - _getSearchFields() { - if (!this.env.pos.config.iface_floorplan) { - return super._getSearchFields(); - } - return Object.assign({}, super._getSearchFields(), { - TABLE: { - repr: this.getTable.bind(this), - displayName: this.env._t("Table"), - modelField: "table_id.name", - }, - }); + }, + shouldShowNewOrderButton() { + return this.env.pos.config.iface_floorplan + ? Boolean(this.env.pos.table) + : this._super(); + }, + _getOrderList() { + if (this.env.pos.table) { + return this.env.pos.getTableOrders(this.env.pos.table.id); } - async _setOrder(order) { - if (!this.env.pos.config.iface_floorplan || this.env.pos.table) { - super._setOrder(order); + return this._super(); + }, + async settleTips() { + // set tip in each order + for (const order of this.getFilteredOrderList()) { + const tipAmount = parse.float(order.uiState.TipScreen.inputTipAmount || "0"); + const serverId = this.env.pos.validated_orders_name_server_id_map[order.name]; + if (!serverId) { + console.warn( + `${order.name} is not yet sync. Sync it to server before setting a tip.` + ); } else { - // we came from the FloorScreen - const orderTable = order.getTable(); - await this.env.pos.setTable(orderTable, order.uid); - this.close(); - } - } - shouldShowNewOrderButton() { - return this.env.pos.config.iface_floorplan - ? Boolean(this.env.pos.table) - : super.shouldShowNewOrderButton(); - } - _getOrderList() { - if (this.env.pos.table) { - return this.env.pos.getTableOrders(this.env.pos.table.id); - } - return super._getOrderList(); - } - async settleTips() { - // set tip in each order - for (const order of this.getFilteredOrderList()) { - const tipAmount = parse.float(order.uiState.TipScreen.inputTipAmount || "0"); - const serverId = this.env.pos.validated_orders_name_server_id_map[order.name]; - if (!serverId) { - console.warn( - `${order.name} is not yet sync. Sync it to server before setting a tip.` - ); - } else { - const result = await this.setTip(order, serverId, tipAmount); - if (!result) { - break; - } + const result = await this.setTip(order, serverId, tipAmount); + if (!result) { + break; } } } - //@override - _selectNextOrder(currentOrder) { - if (this.env.pos.config.iface_floorplan && this.env.pos.table) { - return super._selectNextOrder(...arguments); + }, + //@override + _selectNextOrder(currentOrder) { + if (this.env.pos.config.iface_floorplan && this.env.pos.table) { + return this._super(...arguments); + } + }, + //@override + async _onDeleteOrder() { + await this._super(...arguments); + if (this.env.pos.config.iface_floorplan) { + if (!this.env.pos.table) { + this.env.pos._removeOrdersFromServer(); + } + const orderList = this.env.pos.table + ? this.env.pos.getTableOrders(this.env.pos.table.id) + : this.env.pos.orders; + if (orderList.length == 0) { + this.showScreen("FloorScreen"); } } - //@override - async _onDeleteOrder() { - await super._onDeleteOrder(...arguments); - if (this.env.pos.config.iface_floorplan) { - if (!this.env.pos.table) { - this.env.pos._removeOrdersFromServer(); - } - const orderList = this.env.pos.table - ? this.env.pos.getTableOrders(this.env.pos.table.id) - : this.env.pos.orders; - if (orderList.length == 0) { - this.showScreen("FloorScreen"); - } - } - } - async setTip(order, serverId, amount) { - try { - const paymentline = order.get_paymentlines()[0]; - if (paymentline.payment_method.payment_terminal) { - paymentline.amount += amount; - this.env.pos.set_order(order, { silent: true }); - await paymentline.payment_method.payment_terminal.send_payment_adjust( - paymentline.cid - ); - } - - if (!amount) { - await this.setNoTip(serverId); - } else { - order.finalized = false; - order.set_tip(amount); - order.finalized = true; - const tip_line = order.selected_orderline; - await this.rpc({ - method: "set_tip", - model: "pos.order", - args: [serverId, tip_line.export_as_JSON()], - }); - } - if (order === this.env.pos.get_order()) { - this._selectNextOrder(order); - } - this.env.pos.removeOrder(order); - return true; - } catch { - const { confirmed } = await this.showPopup("ConfirmPopup", { - title: "Failed to set tip", - body: `Failed to set tip to ${order.name}. Do you want to proceed on setting the tips of the remaining?`, - }); - return confirmed; - } - } - async setNoTip(serverId) { - await this.rpc({ - method: "set_no_tip", - model: "pos.order", - args: [serverId], - }); - } - _getOrderStates() { - const result = super._getOrderStates(); - if (this.env.pos.config.set_tip_after_payment) { - result.delete("PAYMENT"); - result.set("OPEN", { text: this.env._t("Open"), indented: true }); - result.set("TIPPING", { text: this.env._t("Tipping"), indented: true }); - } - return result; - } - async _onDoRefund() { - if (this.env.pos.config.iface_floorplan) { - this.env.pos.setTable( - this.getSelectedSyncedOrder().table - ? this.getSelectedSyncedOrder().table - : Object.values(this.env.pos.tables_by_id)[0] + }, + async setTip(order, serverId, amount) { + try { + const paymentline = order.get_paymentlines()[0]; + if (paymentline.payment_method.payment_terminal) { + paymentline.amount += amount; + this.env.pos.set_order(order, { silent: true }); + await paymentline.payment_method.payment_terminal.send_payment_adjust( + paymentline.cid ); } - super._onDoRefund(); - } - isDefaultOrderEmpty(order) { - if (this.env.pos.config.iface_floorplan) { - return false; - } - return super.isDefaultOrderEmpty(...arguments); - } - }; -Registries.Component.extend(TicketScreen, PosResTicketScreen); + if (!amount) { + await this.setNoTip(serverId); + } else { + order.finalized = false; + order.set_tip(amount); + order.finalized = true; + const tip_line = order.selected_orderline; + await this.rpc({ + method: "set_tip", + model: "pos.order", + args: [serverId, tip_line.export_as_JSON()], + }); + } + if (order === this.env.pos.get_order()) { + this._selectNextOrder(order); + } + this.env.pos.removeOrder(order); + return true; + } catch { + const { confirmed } = await this.showPopup(ConfirmPopup, { + title: "Failed to set tip", + body: `Failed to set tip to ${order.name}. Do you want to proceed on setting the tips of the remaining?`, + }); + return confirmed; + } + }, + async setNoTip(serverId) { + await this.rpc({ + method: "set_no_tip", + model: "pos.order", + args: [serverId], + }); + }, + _getOrderStates() { + const result = this._super(); + if (this.env.pos.config.set_tip_after_payment) { + result.delete("PAYMENT"); + result.set("OPEN", { text: this.env._t("Open"), indented: true }); + result.set("TIPPING", { text: this.env._t("Tipping"), indented: true }); + } + return result; + }, + async _onDoRefund() { + if (this.env.pos.config.iface_floorplan) { + this.env.pos.setTable( + this.getSelectedSyncedOrder().table + ? this.getSelectedSyncedOrder().table + : Object.values(this.env.pos.tables_by_id)[0] + ); + } + this._super(); + }, + isDefaultOrderEmpty(order) { + if (this.env.pos.config.iface_floorplan) { + return false; + } + return this._super(...arguments); + }, +}); export class TipCell extends PosComponent { + static template = "TipCell"; + setup() { super.setup(); this.state = useState({ isEditing: false }); @@ -200,6 +200,7 @@ export class TipCell extends PosComponent { this.state.isEditing = true; } } -TipCell.template = "TipCell"; -Registries.Component.add(TipCell); +patch(TicketScreen, "pos_restaurant.TicketScreen.components", { + components: { ...TicketScreen.components, TipCell }, +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js index b55d8fc279a..0e195e30e51 100644 --- a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js @@ -1,13 +1,16 @@ /** @odoo-module */ -import Registries from "@point_of_sale/js/Registries"; -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { parse } from "web.field_utils"; import { renderToString } from "@web/core/utils/render"; +import { registry } from "@web/core/registry"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; const { onMounted } = owl; -class TipScreen extends PosComponent { +export class TipScreen extends PosComponent { + static template = "pos_restaurant.TipScreen"; static showBackToFloorButton = true; setup() { super.setup(); @@ -44,7 +47,7 @@ class TipScreen extends PosComponent { const serverId = this.env.pos.validated_orders_name_server_id_map[order.name]; if (!serverId) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Unsynced order"), body: this.env._t( "This order is not yet synced to server. Make sure it is synced then try again." @@ -64,7 +67,7 @@ class TipScreen extends PosComponent { } if (amount > 0.25 * this.totalAmount) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: "Are you sure?", body: `${this.env.pos.format_currency( amount @@ -136,7 +139,7 @@ class TipScreen extends PosComponent { async _printIoT(receipt) { const printResult = await this.env.proxy.printer.print_receipt(receipt); if (!printResult.successful) { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: printResult.message.title, body: printResult.message.body, }); @@ -148,7 +151,7 @@ class TipScreen extends PosComponent { $(this.el).find(".pos-receipt-container").html(receipt); window.print(); } catch { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Printing is not supported on some browsers"), body: this.env._t( "Printing is not supported on some browsers due to no default printing protocol " + @@ -158,8 +161,5 @@ class TipScreen extends PosComponent { } } } -TipScreen.template = "pos_restaurant.TipScreen"; -Registries.Component.add(TipScreen); - -export default TipScreen; +registry.category("pos_screens").add("TipScreen", TipScreen); diff --git a/addons/pos_restaurant/static/src/js/models.js b/addons/pos_restaurant/static/src/js/models.js index c6e7039ba04..d017dc9fa83 100644 --- a/addons/pos_restaurant/static/src/js/models.js +++ b/addons/pos_restaurant/static/src/js/models.js @@ -1,683 +1,676 @@ /** @odoo-module */ import { PosGlobalState, Order, Orderline, Payment } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; import { uuidv4, batched } from "@point_of_sale/js/utils"; import core from "web.core"; import { Printer } from "@point_of_sale/js/printers"; +import { patch } from "@web/core/utils/patch"; const QWeb = core.qweb; const TIMEOUT = 7500; -const PosRestaurantPosGlobalState = (PosGlobalState) => - class PosRestaurantPosGlobalState extends PosGlobalState { - constructor(obj) { - super(obj); - this.orderToTransfer = null; // table transfer feature - this.ordersToUpdateSet = new Set(); // used to know which orders need to be sent to the back end when syncing - this.transferredOrdersSet = new Set(); // used to know which orders has been transferred but not sent to the back end yet - this.loadingOrderState = false; // used to prevent orders fetched to be put in the update set during the reactive change +patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", { + setup() { + this._super(...arguments); + this.orderToTransfer = null; // table transfer feature + this.ordersToUpdateSet = new Set(); // used to know which orders need to be sent to the back end when syncing + this.transferredOrdersSet = new Set(); // used to know which orders has been transferred but not sent to the back end yet + this.loadingOrderState = false; // used to prevent orders fetched to be put in the update set during the reactive change + }, + //@override + async _processData(loadedData) { + await this._super(...arguments); + if (this.config.is_table_management) { + this.floors = loadedData["restaurant.floor"]; + this.loadRestaurantFloor(); } - //@override - async _processData(loadedData) { - await super._processData(...arguments); - if (this.config.is_table_management) { - this.floors = loadedData["restaurant.floor"]; - this.loadRestaurantFloor(); - } - if (this.config.module_pos_restaurant) { - this._loadRestaurantPrinter(loadedData["restaurant.printer"]); - } + if (this.config.module_pos_restaurant) { + this._loadRestaurantPrinter(loadedData["restaurant.printer"]); } - //@override - _onReactiveOrderUpdated(order) { - super._onReactiveOrderUpdated(...arguments); - if (this.config.iface_floorplan && !this.loadingOrderState) { - this.ordersToUpdateSet.add(order); - } - } - //@override - removeOrder(order, removeFromServer = true) { - super.removeOrder(...arguments); - if (this.config.iface_floorplan && removeFromServer) { - if (this.ordersToUpdateSet.has(order)) { - this.ordersToUpdateSet.delete(order); - } - if (order.server_id && !order.finalized) { - this.db.set_order_to_remove_from_server(order); - } - } - } - //@override - async after_load_server_data() { - var res = await super.after_load_server_data(...arguments); - if (this.config.iface_floorplan) { - this.table = null; - } - return res; - } - //@override - // if we have tables, we do not load a default order, as the default order will be - // set when the user selects a table. - set_start_order() { - if (!this.config.iface_floorplan) { - super.set_start_order(...arguments); - } - } - //@override - add_new_order() { - const order = super.add_new_order(); + }, + //@override + _onReactiveOrderUpdated(order) { + this._super(...arguments); + if (this.config.iface_floorplan && !this.loadingOrderState) { this.ordersToUpdateSet.add(order); - return order; } - //@override - createReactiveOrder(json) { - let reactiveOrder = super.createReactiveOrder(...arguments); - if (this.config.iface_printers) { - const updateOrderChanges = () => { - if (reactiveOrder.get_screen_data().name === "ProductScreen") { - reactiveOrder.updateChangesToPrint(); - } - }; - reactiveOrder = owl.reactive(reactiveOrder, batched(updateOrderChanges)); - reactiveOrder.updateChangesToPrint(); + }, + //@override + removeOrder(order, removeFromServer = true) { + this._super(...arguments); + if (this.config.iface_floorplan && removeFromServer) { + if (this.ordersToUpdateSet.has(order)) { + this.ordersToUpdateSet.delete(order); } - return reactiveOrder; - } - //@override - async load_orders() { - this.loadingOrderState = true; - await super.load_orders(); - this.loadingOrderState = false; - } - _loadRestaurantPrinter(printers) { - this.unwatched.printers = []; - // list of product categories that belong to one or more order printer - this.printers_category_ids_set = new Set(); - for (const printerConfig of printers) { - const printer = this.create_printer(printerConfig); - printer.config = printerConfig; - this.unwatched.printers.push(printer); - for (const id of printer.config.product_categories_ids) { - this.printers_category_ids_set.add(id); - } - } - this.config.iface_printers = !!this.unwatched.printers.length; - } - async _getTableOrdersFromServer(tableIds) { - this.set_synch("connecting", 1); - try { - const orders = await this.env.services.rpc( - { - model: "pos.order", - method: "get_table_draft_orders", - args: [tableIds], - }, - { - timeout: TIMEOUT, - shadow: true, - } - ); - this.set_synch("connected"); - return orders; - } catch (error) { - this.set_synch("error"); - throw error; + if (order.server_id && !order.finalized) { + this.db.set_order_to_remove_from_server(order); } } - /** - * Sync orders that got updated to the back end - * @param tableId ID of the table we want to sync - */ - async _syncTableOrdersToServer() { - await this._pushOrdersToServer(); - await this._removeOrdersFromServer(); - // This need to be called here otherwise _onReactiveOrderUpdated() will be called after the set is being cleared - this.ordersToUpdateSet.clear(); - this.transferredOrdersSet.clear(); - } - /** - * Send the orders to be saved to the back end - * @throw error - */ - async _pushOrdersToServer() { - const ordersUidsToSync = [...this.ordersToUpdateSet].map((order) => order.uid); - const ordersToSync = this.db.get_unpaid_orders_to_sync(ordersUidsToSync); - const ordersResponse = await this._save_to_server(ordersToSync, { draft: true }); - const tableOrders = [...this.ordersToUpdateSet].map((order) => order); - ordersResponse.forEach((orderResponseData) => - this._updateTableOrder(orderResponseData, tableOrders) - ); - } - // created this hook for modularity - _updateTableOrder(ordersResponseData, tableOrders) { - const order = tableOrders.find( - (order) => order.name === ordersResponseData.pos_reference - ); - order.server_id = ordersResponseData.id; - return order; - } - /** - * Remove the deleted orders from the backend. - * @throw error - */ - async _removeOrdersFromServer() { - const removedOrdersIds = this.db.get_ids_to_remove_from_server(); - if (removedOrdersIds.length === 0) { - return; - } - - const timeout = TIMEOUT * removedOrdersIds.length; - this.set_synch("connecting", removedOrdersIds.length); - try { - const removeOrdersResponseData = await this.env.services.rpc( - { - model: "pos.order", - method: "remove_from_ui", - args: [removedOrdersIds], - }, - { - timeout: timeout, - shadow: true, - } - ); - this.set_synch("connected"); - this._postRemoveFromServer(removedOrdersIds, removeOrdersResponseData); - } catch (reason) { - const error = reason.message; - if (error.code === 200) { - // Business Logic Error, not a connection problem - //if warning do not need to display traceback!! - if (error.data.exception_type == "warning") { - delete error.data.debug; - } - } - // important to throw error here and let the rendering component handle the error - console.warn("Failed to remove orders:", removedOrdersIds); - throw error; - } - } - // to override - _postRemoveFromServer(serverIds, data) { - this.db.set_ids_removed_from_server(serverIds); - } - /** - * Replace all the orders of a table by orders fetched from the backend - * @param tableId ID of the table - * @throws error - */ - async _syncTableOrdersFromServer(tableId) { - await this._removeOrdersFromServer(); // in case we were offline and we deleted orders in the mean time - const ordersJsons = await this._getTableOrdersFromServer([tableId]); - const tableOrders = this.getTableOrders(tableId); - this._replaceOrders(tableOrders, ordersJsons); - } - async _syncAllOrdersFromServer() { - await this._removeOrdersFromServer(); // in case we were offline and we deleted orders in the mean time - const tableIds = [].concat( - ...this.floors.map((floor) => floor.tables.map((table) => table.id)) - ); - const ordersJsons = await this._getTableOrdersFromServer(tableIds); // get all orders - await this._syncTableOrdersToServer(); // to prevent losing the transferred orders - const allOrders = [...this.get_order_list()]; - this._replaceOrders(allOrders, ordersJsons); - } - _replaceOrders(ordersToReplace, newOrdersJsons) { - ordersToReplace.forEach((order) => { - // We don't remove the validated orders because we still want to see them in the ticket screen. - // Orders in 'ReceiptScreen' or 'TipScreen' are validated orders. - if (order.server_id && !order.finalized && !this.transferredOrdersSet.has(order)) { - this.removeOrder(order, false); - } - }); - newOrdersJsons.forEach((json) => { - // Because of the offline feature, some draft orders fetched from the backend will appear - // to belong in different table, but in fact they are already moved. - const transferredOrder = [...this.transferredOrdersSet].find( - (order) => order.uid === json.uid - ); - const isSameTable = transferredOrder && transferredOrder.tableId === json.tableId; - if (isSameTable) { - // this means we transferred back to the original table, we'll prioritize the server state - this.removeOrder(transferredOrder, false); - } - if (!transferredOrder || isSameTable) { - const order = this.createReactiveOrder(json); - this.orders.add(order); - } - }); - } - setLoadingOrderState(bool) { - this.loadingOrderState = bool; - } - loadRestaurantFloor() { - // we do this in the front end due to the circular/recursive reference needed - // Ignore floorplan features if no floor specified. - this.config.iface_floorplan = !!(this.floors && this.floors.length > 0); - if (this.config.iface_floorplan) { - this.floors_by_id = {}; - this.tables_by_id = {}; - for (const floor of this.floors) { - this.floors_by_id[floor.id] = floor; - for (const table of floor.tables) { - this.tables_by_id[table.id] = table; - table.floor = floor; - } - } - } - } - async setTable(table, orderUid = null) { - this.table = table; - try { - this.loadingOrderState = true; - await this._syncTableOrdersFromServer(table.id); - } finally { - this.loadingOrderState = false; - const currentOrder = this.getTableOrders(table.id).find((order) => - orderUid ? order.uid === orderUid : !order.finalized - ); - if (currentOrder) { - this.set_order(currentOrder); - } else { - this.add_new_order(); - } - } - } - getTableOrders(tableId) { - return this.get_order_list().filter((order) => order.tableId === tableId); - } - unsetTable() { - this._syncTableOrdersToServer(); + }, + //@override + async after_load_server_data() { + var res = await this._super(...arguments); + if (this.config.iface_floorplan) { this.table = null; - this.set_order(null); } - setCurrentOrderToTransfer() { - this.orderToTransfer = this.selectedOrder; + return res; + }, + //@override + // if we have tables, we do not load a default order, as the default order will be + // set when the user selects a table. + set_start_order() { + if (!this.config.iface_floorplan) { + this._super(...arguments); } - async transferTable(table) { - this.table = table; - try { - this.loadingOrderState = true; - await this._syncTableOrdersFromServer(table.id); - } finally { - this.loadingOrderState = false; - this.orderToTransfer.tableId = table.id; - this.set_order(this.orderToTransfer); - this.transferredOrdersSet.add(this.orderToTransfer); - this.orderToTransfer = null; + }, + //@override + add_new_order() { + const order = this._super(); + this.ordersToUpdateSet.add(order); + return order; + }, + //@override + createReactiveOrder(json) { + let reactiveOrder = this._super(...arguments); + if (this.config.iface_printers) { + const updateOrderChanges = () => { + if (reactiveOrder.get_screen_data().name === "ProductScreen") { + reactiveOrder.updateChangesToPrint(); + } + }; + reactiveOrder = owl.reactive(reactiveOrder, batched(updateOrderChanges)); + reactiveOrder.updateChangesToPrint(); + } + return reactiveOrder; + }, + //@override + async load_orders() { + this.loadingOrderState = true; + await this._super(); + this.loadingOrderState = false; + }, + _loadRestaurantPrinter(printers) { + this.unwatched.printers = []; + // list of product categories that belong to one or more order printer + this.printers_category_ids_set = new Set(); + for (const printerConfig of printers) { + const printer = this.create_printer(printerConfig); + printer.config = printerConfig; + this.unwatched.printers.push(printer); + for (const id of printer.config.product_categories_ids) { + this.printers_category_ids_set.add(id); } } - getCustomerCount(tableId) { - const tableOrders = this.getTableOrders(tableId).filter((order) => !order.finalized); - return tableOrders.reduce((count, order) => count + order.getCustomerCount(), 0); + this.config.iface_printers = !!this.unwatched.printers.length; + }, + async _getTableOrdersFromServer(tableIds) { + this.set_synch("connecting", 1); + try { + const orders = await this.env.services.rpc( + { + model: "pos.order", + method: "get_table_draft_orders", + args: [tableIds], + }, + { + timeout: TIMEOUT, + shadow: true, + } + ); + this.set_synch("connected"); + return orders; + } catch (error) { + this.set_synch("error"); + throw error; } - create_printer(config) { - var url = config.proxy_ip || ""; - if (url.indexOf("//") < 0) { - url = window.location.protocol + "//" + url; - } - if ( - url.indexOf(":", url.indexOf("//") + 2) < 0 && - window.location.protocol !== "https:" - ) { - url = url + ":8069"; - } - return new Printer(url, this); + }, + /** + * Sync orders that got updated to the back end + * @param tableId ID of the table we want to sync + */ + async _syncTableOrdersToServer() { + await this._pushOrdersToServer(); + await this._removeOrdersFromServer(); + // This need to be called here otherwise _onReactiveOrderUpdated() will be called after the set is being cleared + this.ordersToUpdateSet.clear(); + this.transferredOrdersSet.clear(); + }, + /** + * Send the orders to be saved to the back end + * @throw error + */ + async _pushOrdersToServer() { + const ordersUidsToSync = [...this.ordersToUpdateSet].map((order) => order.uid); + const ordersToSync = this.db.get_unpaid_orders_to_sync(ordersUidsToSync); + const ordersResponse = await this._save_to_server(ordersToSync, { draft: true }); + const tableOrders = [...this.ordersToUpdateSet].map((order) => order); + ordersResponse.forEach((orderResponseData) => + this._updateTableOrder(orderResponseData, tableOrders) + ); + }, + // created this hook for modularity + _updateTableOrder(ordersResponseData, tableOrders) { + const order = tableOrders.find( + (order) => order.name === ordersResponseData.pos_reference + ); + order.server_id = ordersResponseData.id; + return order; + }, + /** + * Remove the deleted orders from the backend. + * @throw error + */ + async _removeOrdersFromServer() { + const removedOrdersIds = this.db.get_ids_to_remove_from_server(); + if (removedOrdersIds.length === 0) { + return; } - }; -Registries.Model.extend(PosGlobalState, PosRestaurantPosGlobalState); + + const timeout = TIMEOUT * removedOrdersIds.length; + this.set_synch("connecting", removedOrdersIds.length); + try { + const removeOrdersResponseData = await this.env.services.rpc( + { + model: "pos.order", + method: "remove_from_ui", + args: [removedOrdersIds], + }, + { + timeout: timeout, + shadow: true, + } + ); + this.set_synch("connected"); + this._postRemoveFromServer(removedOrdersIds, removeOrdersResponseData); + } catch (reason) { + const error = reason.message; + if (error.code === 200) { + // Business Logic Error, not a connection problem + //if warning do not need to display traceback!! + if (error.data.exception_type == "warning") { + delete error.data.debug; + } + } + // important to throw error here and let the rendering component handle the error + console.warn("Failed to remove orders:", removedOrdersIds); + throw error; + } + }, + // to override + _postRemoveFromServer(serverIds, data) { + this.db.set_ids_removed_from_server(serverIds); + }, + /** + * Replace all the orders of a table by orders fetched from the backend + * @param tableId ID of the table + * @throws error + */ + async _syncTableOrdersFromServer(tableId) { + await this._removeOrdersFromServer(); // in case we were offline and we deleted orders in the mean time + const ordersJsons = await this._getTableOrdersFromServer([tableId]); + const tableOrders = this.getTableOrders(tableId); + this._replaceOrders(tableOrders, ordersJsons); + }, + async _syncAllOrdersFromServer() { + await this._removeOrdersFromServer(); // in case we were offline and we deleted orders in the mean time + const tableIds = [].concat( + ...this.floors.map((floor) => floor.tables.map((table) => table.id)) + ); + const ordersJsons = await this._getTableOrdersFromServer(tableIds); // get all orders + await this._syncTableOrdersToServer(); // to prevent losing the transferred orders + const allOrders = [...this.get_order_list()]; + this._replaceOrders(allOrders, ordersJsons); + }, + _replaceOrders(ordersToReplace, newOrdersJsons) { + ordersToReplace.forEach((order) => { + // We don't remove the validated orders because we still want to see them in the ticket screen. + // Orders in 'ReceiptScreen' or 'TipScreen' are validated orders. + if (order.server_id && !order.finalized && !this.transferredOrdersSet.has(order)) { + this.removeOrder(order, false); + } + }); + newOrdersJsons.forEach((json) => { + // Because of the offline feature, some draft orders fetched from the backend will appear + // to belong in different table, but in fact they are already moved. + const transferredOrder = [...this.transferredOrdersSet].find( + (order) => order.uid === json.uid + ); + const isSameTable = transferredOrder && transferredOrder.tableId === json.tableId; + if (isSameTable) { + // this means we transferred back to the original table, we'll prioritize the server state + this.removeOrder(transferredOrder, false); + } + if (!transferredOrder || isSameTable) { + const order = this.createReactiveOrder(json); + this.orders.add(order); + } + }); + }, + setLoadingOrderState(bool) { + this.loadingOrderState = bool; + }, + loadRestaurantFloor() { + // we do this in the front end due to the circular/recursive reference needed + // Ignore floorplan features if no floor specified. + this.config.iface_floorplan = !!(this.floors && this.floors.length > 0); + if (this.config.iface_floorplan) { + this.floors_by_id = {}; + this.tables_by_id = {}; + for (const floor of this.floors) { + this.floors_by_id[floor.id] = floor; + for (const table of floor.tables) { + this.tables_by_id[table.id] = table; + table.floor = floor; + } + } + } + }, + async setTable(table, orderUid = null) { + this.table = table; + try { + this.loadingOrderState = true; + await this._syncTableOrdersFromServer(table.id); + } finally { + this.loadingOrderState = false; + const currentOrder = this.getTableOrders(table.id).find((order) => + orderUid ? order.uid === orderUid : !order.finalized + ); + if (currentOrder) { + this.set_order(currentOrder); + } else { + this.add_new_order(); + } + } + }, + getTableOrders(tableId) { + return this.get_order_list().filter((order) => order.tableId === tableId); + }, + unsetTable() { + this._syncTableOrdersToServer(); + this.table = null; + this.set_order(null); + }, + setCurrentOrderToTransfer() { + this.orderToTransfer = this.selectedOrder; + }, + async transferTable(table) { + this.table = table; + try { + this.loadingOrderState = true; + await this._syncTableOrdersFromServer(table.id); + } finally { + this.loadingOrderState = false; + this.orderToTransfer.tableId = table.id; + this.set_order(this.orderToTransfer); + this.transferredOrdersSet.add(this.orderToTransfer); + this.orderToTransfer = null; + } + }, + getCustomerCount(tableId) { + const tableOrders = this.getTableOrders(tableId).filter((order) => !order.finalized); + return tableOrders.reduce((count, order) => count + order.getCustomerCount(), 0); + }, + create_printer(config) { + var url = config.proxy_ip || ""; + if (url.indexOf("//") < 0) { + url = window.location.protocol + "//" + url; + } + if ( + url.indexOf(":", url.indexOf("//") + 2) < 0 && + window.location.protocol !== "https:" + ) { + url = url + ":8069"; + } + return new Printer(url, this); + }, +}); + // New orders are now associated with the current table, if any. -const PosRestaurantOrder = (Order) => - class PosRestaurantOrder extends Order { - constructor(obj, options) { - super(...arguments); - if (this.pos.config.module_pos_restaurant) { - if (this.pos.config.iface_floorplan && !this.tableId && !options.json) { - this.tableId = this.pos.table.id; - } - this.customerCount = this.customerCount || 1; +patch(Order.prototype, "pos_restaurant.Order", { + setup(options) { + this._super(...arguments); + if (this.pos.config.module_pos_restaurant) { + if (this.pos.config.iface_floorplan && !this.tableId && !options.json) { + this.tableId = this.pos.table.id; } - if (this.pos.config.iface_printers) { - // printedResume will store the previous state of the orderlines (when there were no skip), it will - // store all the orderlines even if the product are not printable. This way, when we add a new category in - // the printers, the already added products of the newly added category are not printed. - this.printedResume = owl.markRaw(this.printedResume || {}); // we don't wanna track it and re-render - // no need to store this in the backend, we can just compute it once the order is fetched from clicking a table - if (!this.printingChanges) { - this._resetPrintingChanges(); - } + this.customerCount = this.customerCount || 1; + } + if (this.pos.config.iface_printers) { + // printedResume will store the previous state of the orderlines (when there were no skip), it will + // store all the orderlines even if the product are not printable. This way, when we add a new category in + // the printers, the already added products of the newly added category are not printed. + this.printedResume = owl.markRaw(this.printedResume || {}); // we don't wanna track it and re-render + // no need to store this in the backend, we can just compute it once the order is fetched from clicking a table + if (!this.printingChanges) { + this._resetPrintingChanges(); } } - //@override - export_as_JSON() { - const json = super.export_as_JSON(...arguments); - if (this.pos.config.module_pos_restaurant) { - if (this.pos.config.iface_floorplan) { - json.table_id = this.tableId; - } - json.customer_count = this.customerCount; + }, + //@override + export_as_JSON() { + const json = this._super(...arguments); + if (this.pos.config.module_pos_restaurant) { + if (this.pos.config.iface_floorplan) { + json.table_id = this.tableId; } - if (this.pos.config.iface_printers) { - json.multiprint_resume = JSON.stringify(this.printedResume); - // so that it can be stored in local storage and be used when loading the pos in the floorscreen - json.printing_changes = JSON.stringify(this.printingChanges); - } - return json; + json.customer_count = this.customerCount; } - //@override - init_from_JSON(json) { - super.init_from_JSON(...arguments); - if (this.pos.config.module_pos_restaurant) { - if (this.pos.config.iface_floorplan) { - this.tableId = json.table_id; - } - this.customerCount = json.customer_count; - } - if (this.pos.config.iface_printers) { - this.printedResume = json.multiprint_resume && JSON.parse(json.multiprint_resume); - this.printingChanges = json.printing_changes && JSON.parse(json.printing_changes); - } + if (this.pos.config.iface_printers) { + json.multiprint_resume = JSON.stringify(this.printedResume); + // so that it can be stored in local storage and be used when loading the pos in the floorscreen + json.printing_changes = JSON.stringify(this.printingChanges); } - //@override - export_for_printing() { - const json = super.export_for_printing(...arguments); - if (this.pos.config.module_pos_restaurant) { - if (this.pos.config.iface_floorplan) { - json.table = this.getTable().name; - } - json.customer_count = this.getCustomerCount(); + return json; + }, + //@override + init_from_JSON(json) { + this._super(...arguments); + if (this.pos.config.module_pos_restaurant) { + if (this.pos.config.iface_floorplan) { + this.tableId = json.table_id; } - return json; + this.customerCount = json.customer_count; } - _resetPrintingChanges() { - this.printingChanges = { new: [], cancelled: [] }; + if (this.pos.config.iface_printers) { + this.printedResume = json.multiprint_resume && JSON.parse(json.multiprint_resume); + this.printingChanges = json.printing_changes && JSON.parse(json.printing_changes); } - /** - * @returns {{ [productKey: string]: { product_id: number, name: string, note: string, quantity: number } }} - */ - _computePrintChanges() { - const changes = {}; + }, + //@override + export_for_printing() { + const json = this._super(...arguments); + if (this.pos.config.module_pos_restaurant) { + if (this.pos.config.iface_floorplan) { + json.table = this.getTable().name; + } + json.customer_count = this.getCustomerCount(); + } + return json; + }, + _resetPrintingChanges() { + this.printingChanges = { new: [], cancelled: [] }; + }, + /** + * @returns {{ [productKey: string]: { product_id: number, name: string, note: string, quantity: number } }} + */ + _computePrintChanges() { + const changes = {}; - // If there's a new orderline, we add it otherwise we add the change if there's one - this.orderlines.forEach((line) => { - if (!line.mp_skip) { - const productId = line.get_product().id; - const note = line.get_note(); - const productKey = `${productId} - ${line.get_full_product_name()} - ${note}`; - const lineKey = `${line.uuid} - ${note}`; - const quantityDiff = - line.get_quantity() - - (this.printedResume[lineKey] ? this.printedResume[lineKey]["quantity"] : 0); - if (quantityDiff) { - if (!changes[productKey]) { - changes[productKey] = { - product_id: productId, - name: line.get_full_product_name(), - note: note, - quantity: quantityDiff, - }; - } else { - changes[productKey]["quantity"] += quantityDiff; - } - line.set_dirty(true); - } else { - line.set_dirty(false); - } - } - }); - - // If there's an orderline that's not present anymore, we consider it as removed (even if note changed) - for (const [lineKey, lineResume] of Object.entries(this.printedResume)) { - if (!this._getPrintedLine(lineKey)) { - const productKey = `${lineResume["product_id"]} - ${lineResume["name"]} - ${lineResume["note"]}`; + // If there's a new orderline, we add it otherwise we add the change if there's one + this.orderlines.forEach((line) => { + if (!line.mp_skip) { + const productId = line.get_product().id; + const note = line.get_note(); + const productKey = `${productId} - ${line.get_full_product_name()} - ${note}`; + const lineKey = `${line.uuid} - ${note}`; + const quantityDiff = + line.get_quantity() - + (this.printedResume[lineKey] ? this.printedResume[lineKey]["quantity"] : 0); + if (quantityDiff) { if (!changes[productKey]) { changes[productKey] = { - product_id: lineResume["product_id"], - name: lineResume["name"], - note: lineResume["note"], - quantity: -lineResume["quantity"], - }; - } else { - changes[productKey]["quantity"] -= lineResume["quantity"]; - } - } - } - - return changes; - } - _getPrintingCategoriesChanges(categories) { - return { - new: this.printingChanges["new"].filter((change) => - this.pos.db.is_product_in_category(categories, change["product_id"]) - ), - cancelled: this.printingChanges["cancelled"].filter((change) => - this.pos.db.is_product_in_category(categories, change["product_id"]) - ), - }; - } - _getPrintedLine(lineKey) { - return this.orderlines.find( - (line) => - line.uuid === this.printedResume[lineKey]["line_uuid"] && - line.note === this.printedResume[lineKey]["note"] - ); - } - getCustomerCount() { - return this.customerCount; - } - setCustomerCount(count) { - this.customerCount = Math.max(count, 0); - } - getTable() { - if (this.pos.config.iface_floorplan) { - return this.pos.tables_by_id[this.tableId]; - } - return null; - } - updatePrintedResume() { - // we first remove the removed orderlines - for (const lineKey in this.printedResume) { - if (!this._getPrintedLine(lineKey)) { - delete this.printedResume[lineKey]; - } - } - // we then update the added orderline or product quantity change - this.orderlines.forEach((line) => { - if (!line.mp_skip) { - const note = line.get_note(); - const lineKey = `${line.uuid} - ${note}`; - if (this.printedResume[lineKey]) { - this.printedResume[lineKey]["quantity"] = line.get_quantity(); - } else { - this.printedResume[lineKey] = { - line_uuid: line.uuid, - product_id: line.get_product().id, + product_id: productId, name: line.get_full_product_name(), note: note, - quantity: line.get_quantity(), + quantity: quantityDiff, }; + } else { + changes[productKey]["quantity"] += quantityDiff; } + line.set_dirty(true); + } else { line.set_dirty(false); } - }); - this._resetPrintingChanges(); - } - updateChangesToPrint() { - const changes = this._computePrintChanges(); // it's possible to have a change's quantity of 0 - // we thoroughly parse the changes we just computed to properly separate them into two - const toAdd = []; - const toRemove = []; - - for (const lineChange of Object.values(changes)) { - if (lineChange["quantity"] > 0) { - toAdd.push(lineChange); - } else if (lineChange["quantity"] < 0) { - lineChange["quantity"] *= -1; // we change the sign because that's how it is - toRemove.push(lineChange); - } } + }); - this.printingChanges = { new: toAdd, cancelled: toRemove }; - } - hasChangesToPrint() { - for (const printer of this.pos.unwatched.printers) { - const changes = this._getPrintingCategoriesChanges( - printer.config.product_categories_ids - ); - if (changes["new"].length > 0 || changes["cancelled"].length > 0) { - return true; - } - } - return false; - } - hasSkippedChanges() { - var orderlines = this.get_orderlines(); - for (var i = 0; i < orderlines.length; i++) { - if (orderlines[i].mp_skip) { - return true; - } - } - return false; - } - async printChanges() { - let isPrintSuccessful = true; - const d = new Date(); - let hours = "" + d.getHours(); - hours = hours.length < 2 ? "0" + hours : hours; - let minutes = "" + d.getMinutes(); - minutes = minutes.length < 2 ? "0" + minutes : minutes; - - for (const printer of this.pos.unwatched.printers) { - const changes = this._getPrintingCategoriesChanges( - printer.config.product_categories_ids - ); - if (changes["new"].length > 0 || changes["cancelled"].length > 0) { - const printingChanges = { - new: changes["new"], - cancelled: changes["cancelled"], - table_name: this.pos.config.iface_floorplan ? this.getTable().name : false, - floor_name: this.pos.config.iface_floorplan - ? this.getTable().floor.name - : false, - name: this.name || "unknown order", - time: { - hours, - minutes, - }, + // If there's an orderline that's not present anymore, we consider it as removed (even if note changed) + for (const [lineKey, lineResume] of Object.entries(this.printedResume)) { + if (!this._getPrintedLine(lineKey)) { + const productKey = `${lineResume["product_id"]} - ${lineResume["name"]} - ${lineResume["note"]}`; + if (!changes[productKey]) { + changes[productKey] = { + product_id: lineResume["product_id"], + name: lineResume["name"], + note: lineResume["note"], + quantity: -lineResume["quantity"], }; - const receipt = QWeb.render("OrderChangeReceipt", { changes: printingChanges }); - const result = await printer.print_receipt(receipt); - if (!result.successful) { - isPrintSuccessful = false; - } + } else { + changes[productKey]["quantity"] -= lineResume["quantity"]; } } - return isPrintSuccessful; } - }; -Registries.Model.extend(Order, PosRestaurantOrder); -const PosRestaurantOrderline = (Orderline) => - class PosRestaurantOrderline extends Orderline { - constructor() { - super(...arguments); - this.note = this.note || ""; - if (this.pos.config.iface_printers) { - this.uuid = this.uuid || uuidv4(); - // mp dirty is true if this orderline has changed since the last kitchen print - this.mp_dirty = false; - if (!this.mp_skip) { - // mp_skip is true if the cashier want this orderline - // not to be sent to the kitchen - this.mp_skip = false; + return changes; + }, + _getPrintingCategoriesChanges(categories) { + return { + new: this.printingChanges["new"].filter((change) => + this.pos.db.is_product_in_category(categories, change["product_id"]) + ), + cancelled: this.printingChanges["cancelled"].filter((change) => + this.pos.db.is_product_in_category(categories, change["product_id"]) + ), + }; + }, + _getPrintedLine(lineKey) { + return this.orderlines.find( + (line) => + line.uuid === this.printedResume[lineKey]["line_uuid"] && + line.note === this.printedResume[lineKey]["note"] + ); + }, + getCustomerCount() { + return this.customerCount; + }, + setCustomerCount(count) { + this.customerCount = Math.max(count, 0); + }, + getTable() { + if (this.pos.config.iface_floorplan) { + return this.pos.tables_by_id[this.tableId]; + } + return null; + }, + updatePrintedResume() { + // we first remove the removed orderlines + for (const lineKey in this.printedResume) { + if (!this._getPrintedLine(lineKey)) { + delete this.printedResume[lineKey]; + } + } + // we then update the added orderline or product quantity change + this.orderlines.forEach((line) => { + if (!line.mp_skip) { + const note = line.get_note(); + const lineKey = `${line.uuid} - ${note}`; + if (this.printedResume[lineKey]) { + this.printedResume[lineKey]["quantity"] = line.get_quantity(); + } else { + this.printedResume[lineKey] = { + line_uuid: line.uuid, + product_id: line.get_product().id, + name: line.get_full_product_name(), + note: note, + quantity: line.get_quantity(), + }; + } + line.set_dirty(false); + } + }); + this._resetPrintingChanges(); + }, + updateChangesToPrint() { + const changes = this._computePrintChanges(); // it's possible to have a change's quantity of 0 + // we thoroughly parse the changes we just computed to properly separate them into two + const toAdd = []; + const toRemove = []; + + for (const lineChange of Object.values(changes)) { + if (lineChange["quantity"] > 0) { + toAdd.push(lineChange); + } else if (lineChange["quantity"] < 0) { + lineChange["quantity"] *= -1; // we change the sign because that's how it is + toRemove.push(lineChange); + } + } + + this.printingChanges = { new: toAdd, cancelled: toRemove }; + }, + hasChangesToPrint() { + for (const printer of this.pos.unwatched.printers) { + const changes = this._getPrintingCategoriesChanges( + printer.config.product_categories_ids + ); + if (changes["new"].length > 0 || changes["cancelled"].length > 0) { + return true; + } + } + return false; + }, + hasSkippedChanges() { + var orderlines = this.get_orderlines(); + for (var i = 0; i < orderlines.length; i++) { + if (orderlines[i].mp_skip) { + return true; + } + } + return false; + }, + async printChanges() { + let isPrintSuccessful = true; + const d = new Date(); + let hours = "" + d.getHours(); + hours = hours.length < 2 ? "0" + hours : hours; + let minutes = "" + d.getMinutes(); + minutes = minutes.length < 2 ? "0" + minutes : minutes; + + for (const printer of this.pos.unwatched.printers) { + const changes = this._getPrintingCategoriesChanges( + printer.config.product_categories_ids + ); + if (changes["new"].length > 0 || changes["cancelled"].length > 0) { + const printingChanges = { + new: changes["new"], + cancelled: changes["cancelled"], + table_name: this.pos.config.iface_floorplan ? this.getTable().name : false, + floor_name: this.pos.config.iface_floorplan + ? this.getTable().floor.name + : false, + name: this.name || "unknown order", + time: { + hours, + minutes, + }, + }; + const receipt = QWeb.render("OrderChangeReceipt", { changes: printingChanges }); + const result = await printer.print_receipt(receipt); + if (!result.successful) { + isPrintSuccessful = false; } } } - //@override - can_be_merged_with(orderline) { - if (orderline.get_note() !== this.get_note()) { - return false; - } else { - return ( - !this.mp_skip && !orderline.mp_skip && super.can_be_merged_with(...arguments) - ); - } - } - //@override - clone() { - const orderline = super.clone(...arguments); - orderline.note = this.note; - return orderline; - } - //@override - export_as_JSON() { - const json = super.export_as_JSON(...arguments); - json.note = this.note; - if (this.pos.config.iface_printers) { - json.uuid = this.uuid; - json.mp_skip = this.mp_skip; - } - return json; - } - //@override - init_from_JSON(json) { - super.init_from_JSON(...arguments); - this.note = json.note; - if (this.pos.config.iface_printers) { - this.uuid = json.uuid; - this.mp_skip = json.mp_skip; - } - } - set_note(note) { - this.note = note; - } - get_note() { - return this.note; - } - set_skip(skip) { - if (this.mp_dirty && skip && !this.mp_skip) { - this.mp_skip = true; - } - if (this.mp_skip && !skip) { + return isPrintSuccessful; + }, +}); + +patch(Orderline.prototype, "pos_restaurant.Orderline", { + setup() { + this._super(...arguments); + this.note = this.note || ""; + if (this.pos.config.iface_printers) { + this.uuid = this.uuid || uuidv4(); + // mp dirty is true if this orderline has changed since the last kitchen print + this.mp_dirty = false; + if (!this.mp_skip) { + // mp_skip is true if the cashier want this orderline + // not to be sent to the kitchen this.mp_skip = false; } } - set_dirty(dirty) { - if (this.printable()) { - this.mp_dirty = dirty; - } - } - get_line_diff_hash() { - if (this.get_note()) { - return this.id + "|" + this.get_note(); - } else { - return "" + this.id; - } - } - // can this orderline be potentially printed ? - printable() { - return this.pos.db.is_product_in_category( - this.pos.printers_category_ids_set, - this.get_product().id + }, + //@override + can_be_merged_with(orderline) { + if (orderline.get_note() !== this.get_note()) { + return false; + } else { + return ( + !this.mp_skip && !orderline.mp_skip && this._super(...arguments) ); } - }; -Registries.Model.extend(Orderline, PosRestaurantOrderline); - -const PosRestaurantPayment = (Payment) => - class PosRestaurantPayment extends Payment { - /** - * Override this method to be able to show the 'Adjust Authorisation' button - * on a validated payment_line and to show the tip screen which allow - * tipping even after payment. By default, this returns true for all - * non-cash payment. - */ - canBeAdjusted() { - if (this.payment_method.payment_terminal) { - return this.payment_method.payment_terminal.canBeAdjusted(this.cid); - } - return !this.payment_method.is_cash_count; + }, + //@override + clone() { + const orderline = this._super(...arguments); + orderline.note = this.note; + return orderline; + }, + //@override + export_as_JSON() { + const json = this._super(...arguments); + json.note = this.note; + if (this.pos.config.iface_printers) { + json.uuid = this.uuid; + json.mp_skip = this.mp_skip; } - }; -Registries.Model.extend(Payment, PosRestaurantPayment); + return json; + }, + //@override + init_from_JSON(json) { + this._super(...arguments); + this.note = json.note; + if (this.pos.config.iface_printers) { + this.uuid = json.uuid; + this.mp_skip = json.mp_skip; + } + }, + set_note(note) { + this.note = note; + }, + get_note() { + return this.note; + }, + set_skip(skip) { + if (this.mp_dirty && skip && !this.mp_skip) { + this.mp_skip = true; + } + if (this.mp_skip && !skip) { + this.mp_skip = false; + } + }, + set_dirty(dirty) { + if (this.printable()) { + this.mp_dirty = dirty; + } + }, + get_line_diff_hash() { + if (this.get_note()) { + return this.id + "|" + this.get_note(); + } else { + return "" + this.id; + } + }, + // can this orderline be potentially printed ? + printable() { + return this.pos.db.is_product_in_category( + this.pos.printers_category_ids_set, + this.get_product().id + ); + }, +}); + +patch(Payment.prototype, "pos_restaurant.Payment", { + /** + * Override this method to be able to show the 'Adjust Authorisation' button + * on a validated payment_line and to show the tip screen which allow + * tipping even after payment. By default, this returns true for all + * non-cash payment. + */ + canBeAdjusted() { + if (this.payment_method.payment_terminal) { + return this.payment_method.payment_terminal.canBeAdjusted(this.cid); + } + return !this.payment_method.is_cash_count; + }, +}); diff --git a/addons/pos_restaurant/static/src/js/payment.js b/addons/pos_restaurant/static/src/js/payment.js index 27357278c0c..a944ce85a12 100644 --- a/addons/pos_restaurant/static/src/js/payment.js +++ b/addons/pos_restaurant/static/src/js/payment.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import PaymentInterface from "@point_of_sale/js/payment"; +import { PaymentInterface } from "@point_of_sale/js/payment"; PaymentInterface.include({ /** diff --git a/addons/pos_restaurant_adyen/static/src/js/payment_adyen.js b/addons/pos_restaurant_adyen/static/src/js/payment_adyen.js index 5fcc6b13d41..36f3e1ffaab 100644 --- a/addons/pos_restaurant_adyen/static/src/js/payment_adyen.js +++ b/addons/pos_restaurant_adyen/static/src/js/payment_adyen.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import PaymentAdyen from "@pos_adyen/js/payment_adyen"; +import { PaymentAdyen } from "@pos_adyen/js/payment_adyen"; PaymentAdyen.include({ _adyen_pay_data: function () { diff --git a/addons/pos_restaurant_stripe/static/src/js/payment_stripe.js b/addons/pos_restaurant_stripe/static/src/js/payment_stripe.js index 86bed16878d..c30e0d9c204 100644 --- a/addons/pos_restaurant_stripe/static/src/js/payment_stripe.js +++ b/addons/pos_restaurant_stripe/static/src/js/payment_stripe.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import PaymentStripe from "@pos_stripe/js/payment_stripe"; +import { PaymentStripe } from "@pos_stripe/js/payment_stripe"; PaymentStripe.include({ captureAfterPayment: async function (processPayment, line) { diff --git a/addons/pos_sale/static/src/js/OrderManagementScreen/MobileSaleOrderManagementScreen.js b/addons/pos_sale/static/src/js/OrderManagementScreen/MobileSaleOrderManagementScreen.js index 9339352fb6f..6a03cdd5a9e 100644 --- a/addons/pos_sale/static/src/js/OrderManagementScreen/MobileSaleOrderManagementScreen.js +++ b/addons/pos_sale/static/src/js/OrderManagementScreen/MobileSaleOrderManagementScreen.js @@ -1,25 +1,20 @@ /** @odoo-module */ -import SaleOrderManagementScreen from "@pos_sale/js/OrderManagementScreen/SaleOrderManagementScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { SaleOrderManagementScreen } from "@pos_sale/js/OrderManagementScreen/SaleOrderManagementScreen"; import { useListener } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; const { useState } = owl; -const MobileSaleOrderManagementScreen = (SaleOrderManagementScreen) => { - class MobileSaleOrderManagementScreen extends SaleOrderManagementScreen { - setup() { - super.setup(); - useListener("click-order", this._onShowDetails); - this.mobileState = useState({ showDetails: false }); - } - _onShowDetails() { - this.mobileState.showDetails = true; - } +export class MobileSaleOrderManagementScreen extends SaleOrderManagementScreen { + static template = "MobileSaleOrderManagementScreen"; + setup() { + super.setup(); + useListener("click-order", this._onShowDetails); + this.mobileState = useState({ showDetails: false }); } - MobileSaleOrderManagementScreen.template = "MobileSaleOrderManagementScreen"; - return MobileSaleOrderManagementScreen; -}; + _onShowDetails() { + this.mobileState.showDetails = true; + } +} -Registries.Component.addByExtending(MobileSaleOrderManagementScreen, SaleOrderManagementScreen); - -export default MobileSaleOrderManagementScreen; +registry.category("pos_screens").add("MobileSaleOrderManagementScreen", MobileSaleOrderManagementScreen); diff --git a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderFetcher.js b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderFetcher.js index 3a9351b0c57..c99b9c23b7e 100644 --- a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderFetcher.js +++ b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderFetcher.js @@ -124,4 +124,4 @@ class SaleOrderFetcher extends EventBus { } } -export default new SaleOrderFetcher(); +export const saleOrderFetcher = new SaleOrderFetcher(); diff --git a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderList.js b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderList.js index 9d795519abd..d2ab3de75be 100644 --- a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderList.js +++ b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderList.js @@ -1,8 +1,9 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +import { SaleOrderRow } from "./SaleOrderRow"; const { useState } = owl; @@ -10,7 +11,10 @@ const { useState } = owl; * @props {models.Order} [initHighlightedOrder] initially highligted order * @props {Array} orders */ -class SaleOrderList extends PosComponent { +export class SaleOrderList extends PosComponent { + static components = { SaleOrderRow }; + static template = "SaleOrderList"; + setup() { super.setup(); useListener("click-order", this._onClickOrder); @@ -23,8 +27,3 @@ class SaleOrderList extends PosComponent { this.state.highlightedOrder = order; } } -SaleOrderList.template = "SaleOrderList"; - -Registries.Component.add(SaleOrderList); - -export default SaleOrderList; diff --git a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementControlPanel.js b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementControlPanel.js index 4f6ab2781b8..d900fca3661 100644 --- a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementControlPanel.js +++ b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementControlPanel.js @@ -1,9 +1,8 @@ /** @odoo-module */ import { useAutofocus, useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; -import SaleOrderFetcher from "@pos_sale/js/OrderManagementScreen/SaleOrderFetcher"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { saleOrderFetcher } from "@pos_sale/js/OrderManagementScreen/SaleOrderFetcher"; import { orderManagement } from "@point_of_sale/js/PosContext"; const { useState } = owl; @@ -26,7 +25,9 @@ const SEARCH_FIELDS = ["name", "partner_id.display_name", "date_order"]; * @emits next-page * @emits search */ -class SaleOrderManagementControlPanel extends PosComponent { +export class SaleOrderManagementControlPanel extends PosComponent { + static template = "SaleOrderManagementControlPanel"; + setup() { super.setup(); this.orderManagementContext = useState(orderManagement); @@ -37,7 +38,7 @@ class SaleOrderManagementControlPanel extends PosComponent { if (currentPartner) { this.orderManagementContext.searchString = currentPartner.name; } - SaleOrderFetcher.setSearchDomain(this._computeDomain()); + saleOrderFetcher.setSearchDomain(this._computeDomain()); } onInputKeydown(event) { if (event.key === "Enter") { @@ -45,11 +46,11 @@ class SaleOrderManagementControlPanel extends PosComponent { } } get showPageControls() { - return SaleOrderFetcher.lastPage > 1; + return saleOrderFetcher.lastPage > 1; } get pageNumber() { - const currentPage = SaleOrderFetcher.currentPage; - const lastPage = SaleOrderFetcher.lastPage; + const currentPage = saleOrderFetcher.currentPage; + const lastPage = saleOrderFetcher.lastPage; return isNaN(lastPage) ? "" : `(${currentPage}/${lastPage})`; } get validSearchTags() { @@ -127,8 +128,3 @@ class SaleOrderManagementControlPanel extends PosComponent { this.onInputKeydown({ key: "Enter" }); } } -SaleOrderManagementControlPanel.template = "SaleOrderManagementControlPanel"; - -Registries.Component.add(SaleOrderManagementControlPanel); - -export default SaleOrderManagementControlPanel; diff --git a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementScreen.js b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementScreen.js index a207b0b254b..bf5afce6739 100644 --- a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementScreen.js +++ b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementScreen.js @@ -3,17 +3,29 @@ import { sprintf } from "web.utils"; import { parse } from "web.field_utils"; import { useListener } from "@web/core/utils/hooks"; -import ControlButtonsMixin from "@point_of_sale/js/ControlButtonsMixin"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; -import Registries from "@point_of_sale/js/Registries"; -import SaleOrderFetcher from "@pos_sale/js/OrderManagementScreen/SaleOrderFetcher"; -import IndependentToOrderScreen from "@point_of_sale/js/Misc/IndependentToOrderScreen"; +import { registry } from "@web/core/registry"; +import { ControlButtonsMixin } from "@point_of_sale/js/ControlButtonsMixin"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { saleOrderFetcher } from "@pos_sale/js/OrderManagementScreen/SaleOrderFetcher"; +import { IndependentToOrderScreen } from "@point_of_sale/js/Misc/IndependentToOrderScreen"; import { orderManagement } from "@point_of_sale/js/PosContext"; import { Orderline } from "@point_of_sale/js/models"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; + +import { SaleOrderList } from "./SaleOrderList"; +import { SaleOrderManagementControlPanel } from "./SaleOrderManagementControlPanel"; + const { onMounted, onWillUnmount, useState } = owl; -class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderScreen) { +export class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderScreen) { + static components = { SaleOrderList, SaleOrderManagementControlPanel }; + static template = "SaleOrderManagementScreen"; + static hideOrderSelector = true; + setup() { super.setup(); useListener("close-screen", this.close); @@ -22,14 +34,14 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc useListener("prev-page", this._onPrevPage); useListener("search", this._onSearch); - SaleOrderFetcher.setComponent(this); + saleOrderFetcher.setComponent(this); this.orderManagementContext = useState(orderManagement); onMounted(this.onMounted); onWillUnmount(this.onWillUnmount); } onMounted() { - SaleOrderFetcher.on("update", this, this.render); + saleOrderFetcher.on("update", this, this.render); // calculate how many can fit in the screen. // It is based on the height of the header element. @@ -41,41 +53,41 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc (flexContainer.offsetHeight - cpEl.offsetHeight - headerEl.offsetHeight) / headerEl.offsetHeight ); - SaleOrderFetcher.setNPerPage(val); + saleOrderFetcher.setNPerPage(val); // Fetch the order after mounting so that order management screen // is shown while fetching. - setTimeout(() => SaleOrderFetcher.fetch(), 0); + setTimeout(() => saleOrderFetcher.fetch(), 0); } onWillUnmount() { - SaleOrderFetcher.off("update", this); + saleOrderFetcher.off("update", this); } get selectedPartner() { const order = this.orderManagementContext.selectedOrder; return order ? order.get_partner() : null; } get orders() { - return SaleOrderFetcher.get(); + return saleOrderFetcher.get(); } async _setNumpadMode(event) { const { mode } = event.detail; this.numpadMode = mode; - NumberBuffer.reset(); + numberBuffer.reset(); } _onNextPage() { - SaleOrderFetcher.nextPage(); + saleOrderFetcher.nextPage(); } _onPrevPage() { - SaleOrderFetcher.prevPage(); + saleOrderFetcher.prevPage(); } _onSearch({ detail: domain }) { - SaleOrderFetcher.setSearchDomain(domain); - SaleOrderFetcher.setPage(1); - SaleOrderFetcher.fetch(); + saleOrderFetcher.setSearchDomain(domain); + saleOrderFetcher.setPage(1); + saleOrderFetcher.fetch(); } async _onClickSaleOrder(event) { const clickedOrder = event.detail; - const { confirmed, payload: selectedOption } = await this.showPopup("SelectionPopup", { + const { confirmed, payload: selectedOption } = await this.showPopup(SelectionPopup, { title: this.env._t("What do you want to do?"), list: [ { id: "0", label: this.env._t("Apply a down payment"), item: false }, @@ -86,7 +98,7 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc if (confirmed) { const currentPOSOrder = this.env.pos.get_order(); const sale_order = await this._getSaleOrder(clickedOrder.id); - clickedOrder.shipping_date = sale_order.shipping_date + clickedOrder.shipping_date = sale_order.shipping_date; try { await this.env.pos.load_new_partners(); } catch { @@ -104,7 +116,7 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc this.env._t("There was a problem in loading the %s customer."), sale_order.partner_id[1] ); - await this.showPopup("ErrorPopup", { title, body }); + await this.showPopup(ErrorPopup, { title, body }); } currentPOSOrder.set_partner( this.env.pos.db.get_partner_by_id(sale_order.partner_id[0]) @@ -134,7 +146,7 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc .filter((line) => !this.env.pos.db.get_product_by_id(line.product_id[0])) .map((line) => line.product_id[0]); if (product_to_add_in_pos.length) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Products not available in POS"), body: this.env._t( "Some of the products in your Sale Order are not available in POS, do you want to import them?" @@ -165,7 +177,7 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc continue; } - const new_line = Orderline.create( + const new_line = new Orderline( {}, { pos: this.env.pos, @@ -190,7 +202,7 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc // Ask once when `useLoadedLots` is undefined, then reuse it's value on the succeeding lines. const { confirmed } = useLoadedLots === undefined - ? await this.showPopup("ConfirmPopup", { + ? await this.showPopup(ConfirmPopup, { title: this.env._t("SN/Lots Loading"), body: this.env._t( "Do you want to load the SN/Lots linked to the Sales Order?" @@ -242,7 +254,7 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc down_payment = sale_order.amount_total; } - const { confirmed, payload } = await this.showPopup("NumberPopup", { + const { confirmed, payload } = await this.showPopup(NumberPopup, { title: sprintf( this.env._t("Percentage of %s"), this.env.pos.format_currency(sale_order.amount_total) @@ -253,7 +265,7 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc down_payment = (down_payment * parse.float(payload)) / 100; } - const new_line = Orderline.create( + const new_line = new Orderline( {}, { pos: this.env.pos, @@ -273,7 +285,7 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc "It seems that you didn't configure a down payment product in your point of sale.\ You can go to your point of sale configuration to choose one." ); - await this.showPopup("ErrorPopup", { title, body }); + await this.showPopup(ErrorPopup, { title, body }); } } @@ -302,15 +314,12 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc const sale_lines = await this._getSOLines(sale_order[0].order_line); sale_order[0].order_line = sale_lines; - + const picking_id = await this.rpc({ - model: 'stock.picking', - method: 'read', - args: [ - [sale_order[0].picking_ids[0]], - ['scheduled_date'], - ], - }); + model: "stock.picking", + method: "read", + args: [[sale_order[0].picking_ids[0]], ["scheduled_date"]], + }); sale_order[0].shipping_date = picking_id[0].scheduled_date; return sale_order[0]; @@ -326,9 +335,5 @@ class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderSc return so_lines; } } -SaleOrderManagementScreen.template = "SaleOrderManagementScreen"; -SaleOrderManagementScreen.hideOrderSelector = true; -Registries.Component.add(SaleOrderManagementScreen); - -export default SaleOrderManagementScreen; +registry.category("pos_screens").add("SaleOrderManagementScreen", SaleOrderManagementScreen); diff --git a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderRow.js b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderRow.js index 098c39cfbba..135ffdd4b6f 100644 --- a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderRow.js +++ b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderRow.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import utils from "web.utils"; /** @@ -9,7 +8,9 @@ import utils from "web.utils"; * @props columns * @emits click-order */ -class SaleOrderRow extends PosComponent { +export class SaleOrderRow extends PosComponent { + static template = "SaleOrderRow"; + get order() { return this.props.order; } @@ -69,8 +70,3 @@ class SaleOrderRow extends PosComponent { return salesman ? salesman[1] : null; } } -SaleOrderRow.template = "SaleOrderRow"; - -Registries.Component.add(SaleOrderRow); - -export default SaleOrderRow; diff --git a/addons/pos_sale/static/src/js/SetSaleOrderButton.js b/addons/pos_sale/static/src/js/SetSaleOrderButton.js index bac969024af..5b73ee8a727 100644 --- a/addons/pos_sale/static/src/js/SetSaleOrderButton.js +++ b/addons/pos_sale/static/src/js/SetSaleOrderButton.js @@ -1,12 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; import { Gui } from "@point_of_sale/js/Gui"; -class SetSaleOrderButton extends PosComponent { +export class SetSaleOrderButton extends PosComponent { + static template = "SetSaleOrderButton"; + setup() { super.setup(); useListener("click", this.onClick); @@ -33,7 +34,6 @@ class SetSaleOrderButton extends PosComponent { Gui.showScreen(screen); } } -SetSaleOrderButton.template = "SetSaleOrderButton"; ProductScreen.addControlButton({ component: SetSaleOrderButton, @@ -41,7 +41,3 @@ ProductScreen.addControlButton({ return true; }, }); - -Registries.Component.add(SetSaleOrderButton); - -export default SetSaleOrderButton; diff --git a/addons/pos_sale/static/src/js/models.js b/addons/pos_sale/static/src/js/models.js index f843ce530df..d553c26d4d4 100644 --- a/addons/pos_sale/static/src/js/models.js +++ b/addons/pos_sale/static/src/js/models.js @@ -1,89 +1,85 @@ /** @odoo-module */ import { Order, Orderline } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const PosSaleOrder = (Order) => - class PosSaleOrder extends Order { - //@override - select_orderline(orderline) { - super.select_orderline(...arguments); - if (orderline && orderline.product.id === this.pos.config.down_payment_product_id[0]) { - this.pos.numpadMode = "price"; - } +patch(Order.prototype, "pos_sale.Order", { + //@override + select_orderline(orderline) { + this._super(...arguments); + if (orderline && orderline.product.id === this.pos.config.down_payment_product_id[0]) { + this.pos.numpadMode = "price"; } - //@override - _get_ignored_product_ids_total_discount() { - const productIds = super._get_ignored_product_ids_total_discount(...arguments); - productIds.push(this.pos.config.down_payment_product_id[0]); - return productIds; - } - }; -Registries.Model.extend(Order, PosSaleOrder); + }, + //@override + _get_ignored_product_ids_total_discount() { + const productIds = this._super(...arguments); + productIds.push(this.pos.config.down_payment_product_id[0]); + return productIds; + }, +}); -const PosSaleOrderline = (Orderline) => - class PosSaleOrderline extends Orderline { - constructor(obj, options) { - super(...arguments); - // It is possible that this orderline is initialized using `init_from_JSON`, - // meaning, it is loaded from localStorage or from export_for_ui. This means - // that some fields has already been assigned. Therefore, we only set the options - // when the original value is falsy. - this.sale_order_origin_id = this.sale_order_origin_id || options.sale_order_origin_id; - this.sale_order_line_id = this.sale_order_line_id || options.sale_order_line_id; - this.down_payment_details = this.down_payment_details || options.down_payment_details; - this.customerNote = this.customerNote || options.customer_note; - if (this.sale_order_origin_id && this.sale_order_origin_id.shipping_date) { - this.order.setShippingDate(this.sale_order_origin_id.shipping_date); - } +patch(Orderline.prototype, "pos_sale.Orderline", { + setup(options) { + this._super(...arguments); + // It is possible that this orderline is initialized using `init_from_JSON`, + // meaning, it is loaded from localStorage or from export_for_ui. This means + // that some fields has already been assigned. Therefore, we only set the options + // when the original value is falsy. + this.sale_order_origin_id = this.sale_order_origin_id || options.sale_order_origin_id; + this.sale_order_line_id = this.sale_order_line_id || options.sale_order_line_id; + this.down_payment_details = this.down_payment_details || options.down_payment_details; + this.customerNote = this.customerNote || options.customer_note; + if (this.sale_order_origin_id && this.sale_order_origin_id.shipping_date) { + this.order.setShippingDate(this.sale_order_origin_id.shipping_date); } - init_from_JSON(json) { - super.init_from_JSON(...arguments); - this.sale_order_origin_id = json.sale_order_origin_id; - this.sale_order_line_id = json.sale_order_line_id; - this.down_payment_details = - json.down_payment_details && JSON.parse(json.down_payment_details); - } - export_as_JSON() { - const json = super.export_as_JSON(...arguments); - json.sale_order_origin_id = this.sale_order_origin_id; - json.sale_order_line_id = this.sale_order_line_id; - json.down_payment_details = - this.down_payment_details && JSON.stringify(this.down_payment_details); - return json; - } - get_sale_order() { - if (this.sale_order_origin_id) { - const value = { - name: this.sale_order_origin_id.name, - details: this.down_payment_details || false, - }; + }, + init_from_JSON(json) { + this._super(...arguments); + this.sale_order_origin_id = json.sale_order_origin_id; + this.sale_order_line_id = json.sale_order_line_id; + this.down_payment_details = + json.down_payment_details && JSON.parse(json.down_payment_details); + }, + export_as_JSON() { + const json = this._super(...arguments); + json.sale_order_origin_id = this.sale_order_origin_id; + json.sale_order_line_id = this.sale_order_line_id; + json.down_payment_details = + this.down_payment_details && JSON.stringify(this.down_payment_details); + return json; + }, + get_sale_order() { + if (this.sale_order_origin_id) { + const value = { + name: this.sale_order_origin_id.name, + details: this.down_payment_details || false, + }; - return value; - } - return false; + return value; } - export_for_printing() { - var json = super.export_for_printing(...arguments); - json.down_payment_details = this.down_payment_details; - if (this.sale_order_origin_id) { - json.so_reference = this.sale_order_origin_id.name; - } - return json; + return false; + }, + export_for_printing() { + var json = this._super(...arguments); + json.down_payment_details = this.down_payment_details; + if (this.sale_order_origin_id) { + json.so_reference = this.sale_order_origin_id.name; } - /** - * Set quantity based on the give sale order line. - * @param {'sale.order.line'} saleOrderLine - */ - setQuantityFromSOL(saleOrderLine) { - if (this.product.type === "service") { - this.set_quantity(saleOrderLine.qty_to_invoice); - } else { - this.set_quantity( - saleOrderLine.product_uom_qty - - Math.max(saleOrderLine.qty_delivered, saleOrderLine.qty_invoiced) - ); - } + return json; + }, + /** + * Set quantity based on the give sale order line. + * @param {'sale.order.line'} saleOrderLine + */ + setQuantityFromSOL(saleOrderLine) { + if (this.product.type === "service") { + this.set_quantity(saleOrderLine.qty_to_invoice); + } else { + this.set_quantity( + saleOrderLine.product_uom_qty - + Math.max(saleOrderLine.qty_delivered, saleOrderLine.qty_invoiced) + ); } - }; -Registries.Model.extend(Orderline, PosSaleOrderline); + }, +}); diff --git a/addons/pos_sale_loyalty/static/src/js/models.js b/addons/pos_sale_loyalty/static/src/js/models.js index a238f22c345..306b1090653 100644 --- a/addons/pos_sale_loyalty/static/src/js/models.js +++ b/addons/pos_sale_loyalty/static/src/js/models.js @@ -2,25 +2,23 @@ import { Orderline } from '@point_of_sale/js/models'; -import Registries from '@point_of_sale/js/Registries'; +import { patch } from "@web/core/utils/patch"; -export const PosSaleLoyaltyOrderline = (Orderline) => class PosSaleLoyaltyOrderline extends Orderline { +patch(Orderline.prototype, "pos_sale_loyalty.Orderline", { //@override ignoreLoyaltyPoints(args) { if (this.sale_order_origin_id) { return true; } - return super.ignoreLoyaltyPoints(args); - } + return this._super(args); + }, //@override setQuantityFromSOL(saleOrderLine) { // we need to consider reward product such as discount in a quotation if (saleOrderLine.reward_id) { this.set_quantity(saleOrderLine.product_uom_qty); } else { - super.setQuantityFromSOL(...arguments); + this._super(...arguments); } - } -}; - -Registries.Model.extend(Orderline, PosSaleLoyaltyOrderline); + }, +}); diff --git a/addons/pos_sale_product_configurator/static/src/js/models.js b/addons/pos_sale_product_configurator/static/src/js/models.js index f1ba2094bf9..c48a3b2af28 100644 --- a/addons/pos_sale_product_configurator/static/src/js/models.js +++ b/addons/pos_sale_product_configurator/static/src/js/models.js @@ -2,27 +2,27 @@ import { Gui } from "@point_of_sale/js/Gui"; import { Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const PosSaleProductConfiguratorOrder = (Order) => - class PosSaleProductConfiguratorOrder extends Order { - async add_product(product, options) { - super.add_product(...arguments); - if (product.optional_product_ids.length) { - // The `optional_product_ids` only contains ids of the product templates and not the product itself - // We don't load all the product template in the pos, so it'll be hard to know if the id comes from - // a product available in POS. We send a quick cal to the back end to verify. - const isProductLoaded = await this.pos.env.services.rpc({ - model: "product.product", - method: "has_optional_product_in_pos", - args: [[product.id]], - }); - if (isProductLoaded) { - const quantity = this.get_selected_orderline().get_quantity(); - const info = await this.pos.getProductInfo(product, quantity); - Gui.showPopup("ProductInfoPopup", { info: info, product: product }); - } +import { ProductInfoPopup } from "@point_of_sale/js/Popups/ProductInfoPopup"; + +patch(Order.prototype, "pos_sale_product_configurator.Order", { + async add_product(product, options) { + this._super(...arguments); + if (product.optional_product_ids.length) { + // The `optional_product_ids` only contains ids of the product templates and not the product itself + // We don't load all the product template in the pos, so it'll be hard to know if the id comes from + // a product available in POS. We send a quick cal to the back end to verify. + const isProductLoaded = await this.pos.env.services.rpc({ + model: "product.product", + method: "has_optional_product_in_pos", + args: [[product.id]], + }); + if (isProductLoaded) { + const quantity = this.get_selected_orderline().get_quantity(); + const info = await this.pos.getProductInfo(product, quantity); + Gui.showPopup(ProductInfoPopup, { info: info, product: product }); } } - }; -Registries.Model.extend(Order, PosSaleProductConfiguratorOrder); + }, +}); diff --git a/addons/pos_six/__manifest__.py b/addons/pos_six/__manifest__.py index f81dd7c7e8e..d46750a5087 100644 --- a/addons/pos_six/__manifest__.py +++ b/addons/pos_six/__manifest__.py @@ -16,7 +16,6 @@ 'point_of_sale.assets': [ 'pos_six/static/lib/six_timapi/timapi.js', 'pos_six/static/src/js/BalanceButton.js', - 'pos_six/static/src/js/Chrome.js', 'pos_six/static/src/js/models.js', 'pos_six/static/src/js/payment_six.js', 'pos_six/static/src/xml/**/*', diff --git a/addons/pos_six/static/app/navbar.js b/addons/pos_six/static/app/navbar.js new file mode 100644 index 00000000000..c6b37a95ccc --- /dev/null +++ b/addons/pos_six/static/app/navbar.js @@ -0,0 +1,9 @@ +/** @odoo-module */ + +import { Navbar } from "@point_of_sale/app/navbar/navbar"; +import { BalanceButton } from "@pos_hr/js/BalanceButton"; +import { patch } from "@web/core/utils/patch"; + +patch(Navbar, "pos_six.Navbar", { + components: { ...Navbar.components, BalanceButton }, +}); diff --git a/addons/pos_six/static/src/js/BalanceButton.js b/addons/pos_six/static/src/js/BalanceButton.js index 2f1a801f39d..eb567419005 100644 --- a/addons/pos_six/static/src/js/BalanceButton.js +++ b/addons/pos_six/static/src/js/BalanceButton.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class BalanceButton extends PosComponent { + static template = "BalanceButton"; -class BalanceButton extends PosComponent { sendBalance() { this.env.pos.payment_methods.map((pm) => { if (pm.use_payment_terminal === "six") { @@ -12,8 +13,3 @@ class BalanceButton extends PosComponent { }); } } -BalanceButton.template = "BalanceButton"; - -Registries.Component.add(BalanceButton); - -export default BalanceButton; diff --git a/addons/pos_six/static/src/js/payment_six.js b/addons/pos_six/static/src/js/payment_six.js index 0f5687e7200..7e0e73dc8f2 100644 --- a/addons/pos_six/static/src/js/payment_six.js +++ b/addons/pos_six/static/src/js/payment_six.js @@ -3,7 +3,8 @@ import { Gui } from "@point_of_sale/js/Gui"; import core from "web.core"; -import PaymentInterface from "@point_of_sale/js/payment"; +import { PaymentInterface } from "@point_of_sale/js/payment"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; var _t = core._t; @@ -15,7 +16,7 @@ window.onTimApiPublishLogRecord = function (record) { } }; -var PaymentSix = PaymentInterface.extend({ +export const PaymentSix = PaymentInterface.extend({ //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- @@ -118,7 +119,7 @@ var PaymentSix = PaymentInterface.extend({ if (event.exception) { if (event.exception.resultCode !== timapi.constants.ResultCode.apiCancelEcr) { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Transaction was not processed correctly"), body: event.exception.errorText, }); @@ -141,7 +142,7 @@ var PaymentSix = PaymentInterface.extend({ _onBalanceComplete: function (event, data) { if (event.exception) { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Balance Failed"), body: _t("The balance operation failed."), }); @@ -183,5 +184,3 @@ var PaymentSix = PaymentInterface.extend({ }); }, }); - -export default PaymentSix; diff --git a/addons/pos_stripe/static/src/js/models.js b/addons/pos_stripe/static/src/js/models.js index 476539ba4bb..840199dfba8 100644 --- a/addons/pos_stripe/static/src/js/models.js +++ b/addons/pos_stripe/static/src/js/models.js @@ -1,5 +1,5 @@ /** @odoo-module */ import { register_payment_method } from "@point_of_sale/js/models"; -import PaymentStripe from "@pos_stripe/js/payment_stripe"; +import { PaymentStripe } from "@pos_stripe/js/payment_stripe"; register_payment_method("stripe", PaymentStripe); diff --git a/addons/pos_stripe/static/src/js/payment_stripe.js b/addons/pos_stripe/static/src/js/payment_stripe.js index 3f4bbb5418e..e01c0dedf09 100644 --- a/addons/pos_stripe/static/src/js/payment_stripe.js +++ b/addons/pos_stripe/static/src/js/payment_stripe.js @@ -3,15 +3,16 @@ import core from "web.core"; import rpc from "web.rpc"; -import PaymentInterface from "@point_of_sale/js/payment"; +import { PaymentInterface } from "@point_of_sale/js/payment"; import { Gui } from "@point_of_sale/js/Gui"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; const _t = core._t; -const PaymentStripe = PaymentInterface.extend({ +export const PaymentStripe = PaymentInterface.extend({ init: function (pos, payment_method) { this._super(...arguments); - this.terminal = StripeTerminal.create({ + this.terminal = new StripeTerminal({ onFetchConnectionToken: this.stripeFetchConnectionToken.bind(this), onUnexpectedReaderDisconnect: this.stripeUnexpectedDisconnect.bind(this), }); @@ -142,7 +143,7 @@ const PaymentStripe = PaymentInterface.extend({ captureAfterPayment: async function (processPayment, line) { const capturePayment = await this.capturePayment(processPayment.paymentIntent.id); if (capturePayment.charges) - line.card_type = capturePayment.charges.data[0].payment_method_details.card_present.brand; + {line.card_type = capturePayment.charges.data[0].payment_method_details.card_present.brand;} line.transaction_id = capturePayment.id; }, @@ -239,11 +240,9 @@ const PaymentStripe = PaymentInterface.extend({ if (!title) { title = _t("Stripe Error"); } - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: title, body: msg, }); }, }); - -export default PaymentStripe;