From 990b10aece1439779b185d6a49ed26d9586c6c01 Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Fri, 9 Dec 2022 09:49:57 +0000 Subject: [PATCH] [REF] point_of_sale, *: convert legacy modules to ES odoo modules *: 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_loyalty, pos_mercury, pos_restaurant, pos_restaurant_adyen, pos_restaurant_stripe, pos_sale, pos_sale_product_configurator, pos_six, pos_stripe We are about to refactor most of the Javasript code base of the point of sale and related modules. In doing so, we will move a lot of files and modernize the entire code-base. In order to avoid diffcult rebases, their conversion to odoo-modules is done as a first step to avoid getting lots of conflicts on files that were unindented, which marks the entire file as being in conflict. Conflicts will occur during forward ports but they will be easier to manage as the changes will be much smaller in scope, and the author will have the context of the change that causes the conflict in mind when dealing with it. closes odoo/odoo#107621 Related: odoo/enterprise#34910 Signed-off-by: Trinh Jacky (trj) --- .../static/src/js/PaymentScreen.js | 49 +- addons/l10n_co_pos/static/src/js/pos.js | 57 +- .../l10n_fr_pos_cert/static/src/js/Chrome.js | 73 +- .../static/src/js/PaymentScreen.js | 33 +- .../static/src/js/Popups/ClosePosPopup.js | 42 +- addons/l10n_fr_pos_cert/static/src/js/pos.js | 153 +- .../static/src/js/OrderReceipt.js | 33 +- addons/l10n_in_pos/static/src/js/receipt.js | 25 +- addons/l10n_sa_pos/static/src/js/models.js | 119 +- .../static/src/backend/tours/point_of_sale.js | 66 +- .../static/src/entry/chrome_adapter.js | 18 +- addons/point_of_sale/static/src/entry/main.js | 2 +- addons/point_of_sale/static/src/js/Chrome.js | 988 +++--- .../src/js/ChromeWidgets/CashMoveButton.js | 101 +- .../src/js/ChromeWidgets/CashierName.js | 42 +- .../CustomerFacingDisplayButton.js | 200 +- .../src/js/ChromeWidgets/DebugWidget.js | 304 +- .../src/js/ChromeWidgets/HeaderButton.js | 58 +- .../src/js/ChromeWidgets/ProxyStatus.js | 137 +- .../src/js/ChromeWidgets/SaleDetailsButton.js | 62 +- .../src/js/ChromeWidgets/SyncNotification.js | 22 +- .../src/js/ChromeWidgets/TicketButton.js | 42 +- .../static/src/js/ClassRegistry.js | 516 ++- .../static/src/js/ComponentRegistry.js | 46 +- .../static/src/js/ControlButtonsMixin.js | 196 +- addons/point_of_sale/static/src/js/Gui.js | 115 +- .../src/js/Misc/AbstractReceiptScreen.js | 104 +- .../static/src/js/Misc/CurrencyAmount.js | 16 +- .../static/src/js/Misc/Draggable.js | 266 +- .../src/js/Misc/IndependentToOrderScreen.js | 20 +- .../static/src/js/Misc/MobileOrderWidget.js | 38 +- .../static/src/js/Misc/NotificationSound.js | 26 +- .../static/src/js/Misc/NumberBuffer.js | 615 ++-- .../static/src/js/Misc/SearchBar.js | 208 +- .../static/src/js/Notification.js | 38 +- .../src/js/Popups/AbstractAwaitablePopup.js | 114 +- .../static/src/js/Popups/CashMovePopup.js | 96 +- .../static/src/js/Popups/CashOpeningPopup.js | 100 +- .../static/src/js/Popups/ClosePosPopup.js | 344 +- .../static/src/js/Popups/ConfirmPopup.js | 32 +- .../src/js/Popups/ControlButtonPopup.js | 44 +- .../static/src/js/Popups/EditListInput.js | 44 +- .../static/src/js/Popups/EditListPopup.js | 210 +- .../static/src/js/Popups/ErrorBarcodePopup.js | 43 +- .../static/src/js/Popups/ErrorPopup.js | 46 +- .../src/js/Popups/ErrorTracebackPopup.js | 80 +- .../static/src/js/Popups/MoneyDetailsPopup.js | 94 +- .../static/src/js/Popups/NumberPopup.js | 142 +- .../static/src/js/Popups/OfflineErrorPopup.js | 48 +- .../static/src/js/Popups/OrderImportPopup.js | 44 +- .../src/js/Popups/PosPopupController.js | 233 +- .../src/js/Popups/ProductConfiguratorPopup.js | 161 +- .../static/src/js/Popups/ProductInfoPopup.js | 55 +- .../static/src/js/Popups/SelectionPopup.js | 104 +- .../static/src/js/Popups/TextAreaPopup.js | 70 +- .../static/src/js/Popups/TextInputPopup.js | 62 +- .../static/src/js/PosComponent.js | 138 +- .../point_of_sale/static/src/js/PosContext.js | 14 +- .../point_of_sale/static/src/js/Registries.js | 46 +- .../PartnerListScreen/PartnerDetailsEdit.js | 246 +- .../Screens/PartnerListScreen/PartnerLine.js | 36 +- .../PartnerListScreen/PartnerListScreen.js | 422 +-- .../PaymentScreen/PSNumpadInputButton.js | 22 +- .../js/Screens/PaymentScreen/PaymentScreen.js | 903 +++--- .../PaymentScreen/PaymentScreenNumpad.js | 24 +- .../PaymentScreenPaymentLines.js | 34 +- .../PaymentScreen/PaymentScreenStatus.js | 42 +- .../Screens/ProductScreen/ActionpadWidget.js | 38 +- .../Screens/ProductScreen/CategoryButton.js | 24 +- .../OrderlineCustomerNoteButton.js | 56 +- .../ControlButtons/ProductInfoButton.js | 84 +- .../ControlButtons/RefundButton.js | 58 +- .../ControlButtons/SetFiscalPositionButton.js | 122 +- .../ControlButtons/SetPricelistButton.js | 95 +- .../js/Screens/ProductScreen/NumpadWidget.js | 86 +- .../js/Screens/ProductScreen/OrderSummary.js | 42 +- .../js/Screens/ProductScreen/OrderWidget.js | 128 +- .../src/js/Screens/ProductScreen/Orderline.js | 44 +- .../js/Screens/ProductScreen/ProductItem.js | 84 +- .../js/Screens/ProductScreen/ProductScreen.js | 751 ++--- .../Screens/ProductScreen/ProductsWidget.js | 160 +- .../ProductsWidgetControlPanel.js | 201 +- .../js/Screens/ReceiptScreen/OrderReceipt.js | 81 +- .../js/Screens/ReceiptScreen/ReceiptScreen.js | 271 +- .../ReceiptScreen/WrappedProductNameLines.js | 16 +- .../src/js/Screens/ScaleScreen/ScaleScreen.js | 193 +- .../ControlButtons/InvoiceButton.js | 239 +- .../ControlButtons/ReprintReceiptButton.js | 36 +- .../js/Screens/TicketScreen/OrderDetails.js | 46 +- .../Screens/TicketScreen/OrderlineDetails.js | 150 +- .../TicketScreen/ReprintReceiptScreen.js | 59 +- .../js/Screens/TicketScreen/TicketScreen.js | 1293 ++++---- .../static/src/js/barcode_reader.js | 66 +- .../static/src/js/custom_hooks.js | 212 +- addons/point_of_sale/static/src/js/db.js | 386 +-- addons/point_of_sale/static/src/js/devices.js | 293 +- addons/point_of_sale/static/src/js/models.js | 2039 +++++++----- addons/point_of_sale/static/src/js/payment.js | 14 +- addons/point_of_sale/static/src/js/pos_env.js | 16 +- .../point_of_sale/static/src/js/printers.js | 92 +- addons/point_of_sale/static/src/js/utils.js | 188 +- .../static/tests/tours/Chrome.tour.js | 176 +- .../tests/tours/FixedPriceNegativeQty.tour.js | 25 + .../static/tests/tours/PaymentScreen.tour.js | 140 +- .../tests/tours/ProductConfigurator.tour.js | 114 +- .../static/tests/tours/ProductScreen.tour.js | 273 +- .../static/tests/tours/ReceiptScreen.tour.js | 122 +- .../static/tests/tours/TicketScreen.tour.js | 272 +- .../static/tests/tours/acceptance_tour.js | 244 ++ .../tests/tours/helpers/ChromeTourMethods.js | 51 +- .../tours/helpers/ErrorPopupTourMethods.js | 48 +- .../tours/helpers/NumberPopupTourMethods.js | 130 +- .../helpers/PartnerListScreenTourMethods.js | 50 +- .../tours/helpers/PaymentScreenTourMethods.js | 382 ++- .../helpers/ProductConfiguratorTourMethods.js | 142 +- .../tours/helpers/ProductScreenTourMethods.js | 572 ++-- .../tours/helpers/ReceiptScreenTourMethods.js | 172 +- .../helpers/SelectionPopupTourMethods.js | 66 +- .../tours/helpers/TextAreaPopupTourMethods.js | 68 +- .../helpers/TextInputPopupTourMethods.js | 68 +- .../tours/helpers/TicketScreenTourMethods.js | 378 +-- .../static/tests/tours/helpers/utils.js | 285 +- .../static/tests/tours/point_of_sale.js | 408 --- .../static/tests/tours/pricelist_tour.js | 265 ++ .../static/tests/unit/helpers/test_env.js | 64 +- .../tests/unit/test_ComponentRegistry.js | 820 +++-- .../static/tests/unit/test_NumberBuffer.js | 102 +- .../tests/unit/test_PosPopupController.js | 282 +- .../pos_adyen/static/src/js/PaymentScreen.js | 37 +- addons/pos_adyen/static/src/js/models.js | 52 +- .../pos_adyen/static/src/js/payment_adyen.js | 352 +- .../static/src/js/DiscountButton.js | 159 +- .../static/src/js/pos_epson_printer.js | 33 +- .../static/src/js/printers.js | 72 +- .../static/src/js/models.js | 30 +- addons/pos_hr/static/src/js/CashierName.js | 57 +- addons/pos_hr/static/src/js/Chrome.js | 55 +- .../pos_hr/static/src/js/HeaderLockButton.js | 42 +- addons/pos_hr/static/src/js/LoginScreen.js | 72 +- addons/pos_hr/static/src/js/PaymentScreen.js | 26 +- .../static/src/js/SelectCashierMixin.js | 33 +- addons/pos_hr/static/src/js/models.js | 146 +- addons/pos_hr/static/tests/tours/PosHrTour.js | 152 +- .../static/tests/tours/PosHrTourMethods.js | 122 +- .../pos_hr_restaurant/static/src/js/Chrome.js | 4 +- .../src/js/ControlButtons/PromoCodeButton.js | 26 +- .../js/ControlButtons/ResetProgramsButton.js | 14 +- .../src/js/ControlButtons/RewardButton.js | 70 +- .../src/js/ControlButtons/eWalletButton.js | 46 +- addons/pos_loyalty/static/src/js/Loyalty.js | 2864 +++++++++-------- .../pos_loyalty/static/src/js/OrderSummary.js | 8 +- .../pos_loyalty/static/src/js/OrderWidget.js | 13 +- addons/pos_loyalty/static/src/js/Orderline.js | 18 +- .../pos_loyalty/static/src/js/PartnerLine.js | 38 +- .../static/src/js/PartnerListScreen.js | 32 +- .../static/src/js/PaymentScreen.js | 60 +- .../static/src/js/ProductScreen.js | 118 +- .../pos_loyalty/static/src/js/TicketScreen.js | 19 +- .../static/src/tours/EWalletProgramTours.js | 108 +- .../static/src/tours/GiftCardProgramTours.js | 54 +- .../tours/MultipleGiftWalletProgramsTour.js | 102 +- .../src/tours/PosLoyaltyLoyaltyProgramTour.js | 138 +- .../src/tours/PosLoyaltyRewardButtonTour.js | 156 +- .../static/src/tours/PosLoyaltyTour.js | 217 +- .../static/src/tours/PosLoyaltyTourMethods.js | 354 +- .../src/tours/PosLoyaltyValidityTour.js | 26 +- .../pos_mercury/static/src/js/OrderReceipt.js | 36 +- .../static/src/js/PaymentScreen.js | 1097 ++++--- .../src/js/PaymentScreenPaymentLines.js | 50 +- .../static/src/js/PaymentTransactionPopup.js | 50 +- .../static/src/js/ProductScreen.js | 42 +- .../pos_mercury/static/src/js/pos_mercury.js | 257 +- addons/pos_restaurant/static/src/js/Chrome.js | 182 +- .../src/js/ChromeWidgets/BackToFloorButton.js | 56 +- .../src/js/ChromeWidgets/TicketButton.js | 94 +- .../src/js/Popups/PosPopupController.js | 43 +- .../static/src/js/Resizeable.js | 604 ++-- .../static/src/js/Screens/BillScreen.js | 52 +- .../src/js/Screens/FloorScreen/EditBar.js | 26 +- .../js/Screens/FloorScreen/EditableTable.js | 108 +- .../src/js/Screens/FloorScreen/FloorScreen.js | 704 ++-- .../src/js/Screens/FloorScreen/TableWidget.js | 160 +- .../static/src/js/Screens/PaymentScreen.js | 78 +- .../ControlButtons/OrderlineNoteButton.js | 66 +- .../ControlButtons/PrintBillButton.js | 56 +- .../ControlButtons/SplitBillButton.js | 60 +- .../ControlButtons/SubmitOrderButton.js | 112 +- .../ControlButtons/TableGuestsButton.js | 98 +- .../ControlButtons/TransferOrderButton.js | 50 +- .../src/js/Screens/ProductScreen/Orderline.js | 80 +- .../js/Screens/ReceiptScreen/ReceiptScreen.js | 52 +- .../SplitBillScreen/SplitBillScreen.js | 342 +- .../Screens/SplitBillScreen/SplitOrderline.js | 38 +- .../static/src/js/Screens/TicketScreen.js | 347 +- .../static/src/js/Screens/TipScreen.js | 288 +- addons/pos_restaurant/static/src/js/models.js | 1181 +++---- .../pos_restaurant/static/src/js/payment.js | 38 +- .../static/tests/tours/ControlButtons.tour.js | 104 +- .../static/tests/tours/FloorScreen.tour.js | 202 +- .../tests/tours/SplitBillScreen.tour.js | 134 +- .../static/tests/tours/TicketScreen.tour.js | 110 +- .../static/tests/tours/TipScreen.tour.js | 232 +- .../tours/helpers/BillScreenTourMethods.js | 48 +- .../tests/tours/helpers/ChromeTourMethods.js | 52 +- .../tours/helpers/FloorScreenTourMethods.js | 302 +- .../tours/helpers/ProductScreenTourMethods.js | 162 +- .../helpers/SplitBillScreenTourMethods.js | 116 +- .../tours/helpers/TipScreenTourMethods.js | 110 +- .../static/tests/tours/pos_restaurant.js | 181 +- .../static/src/js/payment_adyen.js | 76 +- .../static/src/js/payment_stripe.js | 35 +- .../MobileSaleOrderManagementScreen.js | 43 +- .../OrderManagementScreen/SaleOrderFetcher.js | 262 +- .../js/OrderManagementScreen/SaleOrderList.js | 50 +- .../SaleOrderManagementControlPanel.js | 241 +- .../SaleOrderManagementScreen.js | 446 +-- .../js/OrderManagementScreen/SaleOrderRow.js | 136 +- .../static/src/js/SetSaleOrderButton.js | 106 +- addons/pos_sale/static/src/js/models.js | 153 +- .../pos_sale_loyalty/static/src/js/models.js | 4 +- .../static/src/js/models.js | 57 +- addons/pos_six/static/src/js/BalanceButton.js | 30 +- addons/pos_six/static/src/js/Chrome.js | 24 +- addons/pos_six/static/src/js/models.js | 9 +- addons/pos_six/static/src/js/payment_six.js | 65 +- addons/pos_stripe/static/src/js/models.js | 9 +- .../static/src/js/payment_stripe.js | 172 +- addons/web/tooling/_eslintignore | 60 + addons/web/tooling/_jsconfig.json | 22 +- 229 files changed, 19802 insertions(+), 18628 deletions(-) create mode 100644 addons/point_of_sale/static/tests/tours/FixedPriceNegativeQty.tour.js create mode 100644 addons/point_of_sale/static/tests/tours/acceptance_tour.js delete mode 100644 addons/point_of_sale/static/tests/tours/point_of_sale.js create mode 100644 addons/point_of_sale/static/tests/tours/pricelist_tour.js diff --git a/addons/l10n_co_pos/static/src/js/PaymentScreen.js b/addons/l10n_co_pos/static/src/js/PaymentScreen.js index d7b90711244..f4053645395 100644 --- a/addons/l10n_co_pos/static/src/js/PaymentScreen.js +++ b/addons/l10n_co_pos/static/src/js/PaymentScreen.js @@ -1,31 +1,30 @@ -odoo.define('l10n_co_pos.PaymentScreen', function(require) { - 'use strict'; +/** @odoo-module */ - const PaymentScreen = require('point_of_sale.PaymentScreen'); - const Registries = require('point_of_sale.Registries'); - const session = require('web.session'); +import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import Registries from "@point_of_sale/js/Registries"; +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); - } - } finally { - return super._postPushOrderResolve(...arguments); +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. } - }; + return super._postPushOrderResolve(...arguments); + } + }; - Registries.Component.extend(PaymentScreen, L10nCoPosPaymentScreen); +Registries.Component.extend(PaymentScreen, L10nCoPosPaymentScreen); - return PaymentScreen; -}); +export default PaymentScreen; diff --git a/addons/l10n_co_pos/static/src/js/pos.js b/addons/l10n_co_pos/static/src/js/pos.js index b7719f4f57d..386b5aefead 100644 --- a/addons/l10n_co_pos/static/src/js/pos.js +++ b/addons/l10n_co_pos/static/src/js/pos.js @@ -1,34 +1,33 @@ -odoo.define('l10n_co_pos.pos', function (require) { -"use strict"; +/** @odoo-module */ -var { PosGlobalState, Order } = require('point_of_sale.models'); -const Registries = require('point_of_sale.Registries'); +import { PosGlobalState, Order } from "@point_of_sale/js/models"; +import Registries from "@point_of_sale/js/Registries"; -const L10nCoPosGlobalState = (PosGlobalState) => class L10nCoPosGlobalState extends PosGlobalState { - is_colombian_country() { - return this.company.country.code === 'CO'; - } -} +const L10nCoPosGlobalState = (PosGlobalState) => + class L10nCoPosGlobalState extends PosGlobalState { + is_colombian_country() { + return this.company.country.code === "CO"; + } + }; Registries.Model.extend(PosGlobalState, L10nCoPosGlobalState); -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; - } -} +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); - -}); 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 e1a2cfdd2f7..03bdafa0eff 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/Chrome.js +++ b/addons/l10n_fr_pos_cert/static/src/js/Chrome.js @@ -1,42 +1,47 @@ -odoo.define('l10n_fr_pos_cert.Chrome', function (require) { - 'use strict'; +/** @odoo-module */ - const Chrome = require('point_of_sale.Chrome'); - const Registries = require('point_of_sale.Registries'); - const { ConnectionLostError, ConnectionAbortedError } = require('@web/core/network/rpc_service') - const { identifyError } = require('point_of_sale.utils'); +import Chrome from "@point_of_sale/js/Chrome"; +import Registries from "@point_of_sale/js/Registries"; +import { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service"; +import { identifyError } from "@point_of_sale/js/utils"; - 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(); - let limitDate = new Date(this.env.pos.pos_session.start_at); - limitDate.setDate(limitDate.getDate() + 1); - if (limitDate < now) { - try { - const info = await this.env.pos.getClosePosInfo(); - this.showPopup('ClosePosPopup', { info: info }); - } catch (e) { - if (identifyError(e) instanceof ConnectionLostError||ConnectionAbortedError) { - this.showPopup('OfflineErrorPopup', { - title: this.env._t('Network Error'), - body: this.env._t('Please check your internet connection and try again.'), - }); - } else { - this.showPopup('ErrorPopup', { - title: this.env._t('Unknown Error'), - body: this.env._t('An unknown error prevents us from getting closing information.'), - }); - } +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 < now) { + try { + const info = await this.env.pos.getClosePosInfo(); + this.showPopup("ClosePosPopup", { info: info }); + } catch (e) { + if ( + identifyError(e) instanceof ConnectionLostError || + ConnectionAbortedError + ) { + this.showPopup("OfflineErrorPopup", { + title: this.env._t("Network Error"), + body: this.env._t( + "Please check your internet connection and try again." + ), + }); + } else { + this.showPopup("ErrorPopup", { + title: this.env._t("Unknown Error"), + body: this.env._t( + "An unknown error prevents us from getting closing information." + ), + }); } } } } - }; + } + }; - Registries.Component.extend(Chrome, PosFrCertChrome); +Registries.Component.extend(Chrome, PosFrCertChrome); - return Chrome; -}); +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 1ec68f9baf8..5cad1898023 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js +++ b/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js @@ -1,29 +1,30 @@ -odoo.define('l10n_fr_pos_cert.PaymentScreen', function(require) { +/** @odoo-module */ - const PaymentScreen = require('point_of_sale.PaymentScreen'); - const Registries = require('point_of_sale.Registries'); - const session = require('web.session'); +import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import Registries from "@point_of_sale/js/Registries"; +import session from "web.session"; - const PosFrPaymentScreen = PaymentScreen => class extends PaymentScreen { +const PosFrPaymentScreen = (PaymentScreen) => + class extends PaymentScreen { async _postPushOrderResolve(order, order_server_ids) { try { - if(this.env.pos.is_french_country()) { - let result = await this.rpc({ - model: 'pos.order', - method: 'search_read', - domain: [['id', 'in', order_server_ids]], - fields: ['l10n_fr_hash'], + 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); } - } finally { - return super._postPushOrderResolve(...arguments); + } catch { + // FIXME this doesn't seem correct but is equivalent to return in finally which we had before. } + return super._postPushOrderResolve(...arguments); } }; - Registries.Component.extend(PaymentScreen, PosFrPaymentScreen); +Registries.Component.extend(PaymentScreen, PosFrPaymentScreen); - return PaymentScreen; -}); +export default PaymentScreen; 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 8d5d0157e32..aa24ac24e2a 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,27 +1,25 @@ -odoo.define('l10n_fr_pos_cert.ClosePosPopup', function (require) { - 'use strict'; +/** @odoo-module */ - const ClosePosPopup = require('point_of_sale.ClosePosPopup'); - const Registries = require('point_of_sale.Registries'); +import ClosePosPopup from "@point_of_sale/js/Popups/ClosePosPopup"; +import Registries from "@point_of_sale/js/Registries"; - 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(); - let limitDate = new Date(this.env.pos.pos_session.start_at); - limitDate.setDate(limitDate.getDate() + 1); - isOutdated = limitDate < now; - } - return isOutdated; +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; } - canCancel() { - return super.canCancel() && !this.sessionIsOutdated(); - } - }; + return isOutdated; + } + canCancel() { + return super.canCancel() && !this.sessionIsOutdated(); + } + }; - Registries.Component.extend(ClosePosPopup, PosFrCertClosePopup); +Registries.Component.extend(ClosePosPopup, PosFrCertClosePopup); - return ClosePosPopup; -}); +export default ClosePosPopup; 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 0b7853c1191..2e85a7bacac 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/pos.js +++ b/addons/l10n_fr_pos_cert/static/src/js/pos.js @@ -1,84 +1,93 @@ -odoo.define('l10n_fr_pos_cert.pos', function (require) { -"use strict"; +/** @odoo-module */ -const { Gui } = require('point_of_sale.Gui'); -var { PosGlobalState, Order, Orderline } = require('point_of_sale.models'); -var core = require('web.core'); -const Registries = require('point_of_sale.Registries'); +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"; 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); - } - disallowLineQuantityChange() { - let result = super.disallowLineQuantityChange(...arguments); - return this.is_french_country() || result; - } -} +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); + } + disallowLineQuantityChange() { + const result = super.disallowLineQuantityChange(...arguments); + return this.is_french_country() || result; + } + }; Registries.Model.extend(PosGlobalState, L10nFrPosGlobalState); - -const L10nFrOrder = (Order) => class L10nFrOrder extends Order { - constructor() { - super(...arguments); - this.l10n_fr_hash = this.l10n_fr_hash || false; - this.save_to_db(); - } - 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); +const L10nFrOrder = (Order) => + class L10nFrOrder extends Order { + constructor() { + super(...arguments); + this.l10n_fr_hash = this.l10n_fr_hash || false; + this.save_to_db(); } - } -} + 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); -const L10nFrOrderline = (Orderline) => class L10nFrOrderline extends Orderline { - can_be_merged_with(orderline) { - let order = this.pos.get_order(); - let orderlines = order.orderlines; - let 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 super.can_be_merged_with(...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 e7fe99acba3..7d34022c5b1 100644 --- a/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js +++ b/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js @@ -1,20 +1,19 @@ -odoo.define('l10n_gcc_pos.OrderReceipt', function (require) { - 'use strict'; +/** @odoo-module */ - const OrderReceipt = require('point_of_sale.OrderReceipt') - const Registries = require('point_of_sale.Registries'); +import OrderReceipt from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt"; +import Registries from "@point_of_sale/js/Registries"; - const OrderReceiptGCC = OrderReceipt => - class extends OrderReceipt { - - get receiptEnv() { - let receipt_render_env = super.receiptEnv; - let 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; - } +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) - return OrderReceiptGCC -}); + }; +Registries.Component.extend(OrderReceipt, OrderReceiptGCC); +export default OrderReceiptGCC; diff --git a/addons/l10n_in_pos/static/src/js/receipt.js b/addons/l10n_in_pos/static/src/js/receipt.js index 94ef5e56130..f3218cb2195 100644 --- a/addons/l10n_in_pos/static/src/js/receipt.js +++ b/addons/l10n_in_pos/static/src/js/receipt.js @@ -1,17 +1,14 @@ -odoo.define('l10n_in_pos.receipt', function (require) { -"use strict"; +/** @odoo-module */ -var { Orderline } = require('point_of_sale.models'); -const Registries = require('point_of_sale.Registries'); +import { Orderline } from "@point_of_sale/js/models"; +import Registries from "@point_of_sale/js/Registries"; - -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; - } -} +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); - -}); diff --git a/addons/l10n_sa_pos/static/src/js/models.js b/addons/l10n_sa_pos/static/src/js/models.js index 952e616f24d..c1485b589e1 100644 --- a/addons/l10n_sa_pos/static/src/js/models.js +++ b/addons/l10n_sa_pos/static/src/js/models.js @@ -1,64 +1,79 @@ -odoo.define('l10n_sa_pos.pos', function (require) { -"use strict"; +/** @odoo-module */ -var { Order } = require('point_of_sale.models'); -var Registries = require('point_of_sale.Registries'); +import { Order } from "@point_of_sale/js/models"; +import Registries from "@point_of_sale/js/Registries"; -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() - let qr_values = this.compute_sa_qr_code(result.company.name, result.company.vat, result.date.isostring, result.total_with_tax, result.total_tax); - let 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); +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); + } } + 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 + 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 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); + 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]); + let binary = ""; + for (let i = 0; i < str_to_encode.length; i++) { + binary += String.fromCharCode(str_to_encode[i]); + } + return btoa(binary); } - 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); - } - -} + _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); - -}); diff --git a/addons/point_of_sale/static/src/backend/tours/point_of_sale.js b/addons/point_of_sale/static/src/backend/tours/point_of_sale.js index 37bc8736206..949fe4a6fd3 100644 --- a/addons/point_of_sale/static/src/backend/tours/point_of_sale.js +++ b/addons/point_of_sale/static/src/backend/tours/point_of_sale.js @@ -1,30 +1,40 @@ -odoo.define('point_of_sale.tour', function(require) { -"use strict"; +/** @odoo-module */ -const {_t} = require('web.core'); -const {Markup} = require('web.utils'); -var tour = require('web_tour.tour'); +import { _t } from "web.core"; +import { Markup } from "web.utils"; +import tour from "web_tour.tour"; -tour.register('point_of_sale_tour', { - url: "/web", - rainbowMan: false, - sequence: 45, -}, [tour.stepUtils.showAppsMenuItem(), { - trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]', - content: Markup(_t("Ready to launch your point of sale?")), - width: 215, - position: 'right', - edition: 'community' -}, { - trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]', - content: Markup(_t("Ready to launch your point of sale?")), - width: 215, - position: 'bottom', - edition: 'enterprise' -}, { - trigger: ".o_pos_kanban button.oe_kanban_action_button", - content: Markup(_t("

Ready to have a look at the POS Interface? Let's start our first session.

")), - position: "bottom" -}]); - -}); +tour.register( + "point_of_sale_tour", + { + url: "/web", + rainbowMan: false, + sequence: 45, + }, + [ + tour.stepUtils.showAppsMenuItem(), + { + trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]', + content: Markup(_t("Ready to launch your point of sale?")), + width: 215, + position: "right", + edition: "community", + }, + { + trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]', + content: Markup(_t("Ready to launch your point of sale?")), + width: 215, + position: "bottom", + edition: "enterprise", + }, + { + trigger: ".o_pos_kanban button.oe_kanban_action_button", + content: Markup( + _t( + "

Ready to have a look at the POS Interface? Let's start our first session.

" + ) + ), + position: "bottom", + }, + ] +); diff --git a/addons/point_of_sale/static/src/entry/chrome_adapter.js b/addons/point_of_sale/static/src/entry/chrome_adapter.js index f31874fbeae..e69b8be5c43 100644 --- a/addons/point_of_sale/static/src/entry/chrome_adapter.js +++ b/addons/point_of_sale/static/src/entry/chrome_adapter.js @@ -2,15 +2,15 @@ import { useService } from "@web/core/utils/hooks"; -import Chrome from "point_of_sale.Chrome"; -import ProductScreen from "point_of_sale.ProductScreen"; -import Registries from "point_of_sale.Registries"; -import { PosGlobalState } from "point_of_sale.models"; -import { configureGui } from "point_of_sale.Gui"; +import Chrome from "@point_of_sale/js/Chrome"; +import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import Registries from "@point_of_sale/js/Registries"; +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.env"; +import env from "@point_of_sale/js/pos_env"; import { debounce } from "@web/core/utils/timing"; -import { batched } from "point_of_sale.utils"; +import { batched } from "@point_of_sale/js/utils"; const { Component, reactive, markRaw, useExternalListener, useSubEnv, onWillUnmount, xml } = owl; @@ -58,8 +58,8 @@ export class ChromeAdapter extends Component { async configureAndStart(chrome) { // Add the pos error handler when the chrome component is available. - registry.category('error_handlers').add( - 'posErrorHandler', + registry.category("error_handlers").add( + "posErrorHandler", (env, ...noEnvArgs) => { if (chrome) { return chrome.errorHandler(this.env, ...noEnvArgs); diff --git a/addons/point_of_sale/static/src/entry/main.js b/addons/point_of_sale/static/src/entry/main.js index 0327bea5d45..d6b3d4c9fe1 100644 --- a/addons/point_of_sale/static/src/entry/main.js +++ b/addons/point_of_sale/static/src/entry/main.js @@ -3,7 +3,7 @@ import { startWebClient } from "@web/start"; import { ChromeAdapter } from "@point_of_sale/entry/chrome_adapter"; -import Registries from "point_of_sale.Registries"; +import Registries from "@point_of_sale/js/Registries"; import { registry } from "@web/core/registry"; const { Component, xml } = owl; diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js index ad85ffd7499..3a7c2335559 100644 --- a/addons/point_of_sale/static/src/js/Chrome.js +++ b/addons/point_of_sale/static/src/js/Chrome.js @@ -1,219 +1,236 @@ -odoo.define('point_of_sale.Chrome', function(require) { - 'use strict'; +/** @odoo-module */ - const { loadCSS } = require('@web/core/assets'); - const { useListener } = require("@web/core/utils/hooks"); - const BarcodeParser = require('barcodes.BarcodeParser'); - const PosComponent = require('point_of_sale.PosComponent'); - const NumberBuffer = require('point_of_sale.NumberBuffer'); - const Registries = require('point_of_sale.Registries'); - const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen'); - const { identifyError, batched } = require('point_of_sale.utils'); - const { odooExceptionTitleMap } = require("@web/core/errors/error_dialogs"); - const { ConnectionLostError, ConnectionAbortedError, RPCError } = require('@web/core/network/rpc_service'); - const { useBus } = require("@web/core/utils/hooks"); - const { debounce } = require("@web/core/utils/timing"); - const { Transition } = require("@web/core/transition"); +import { loadCSS } from "@web/core/assets"; +import { useListener, useBus } 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 { identifyError, batched } from "@point_of_sale/js/utils"; +import { odooExceptionTitleMap } from "@web/core/errors/error_dialogs"; +import { + ConnectionLostError, + ConnectionAbortedError, + RPCError, +} from "@web/core/network/rpc_service"; +import { debounce } from "@web/core/utils/timing"; +import { Transition } from "@web/core/transition"; - const { - onError, - onMounted, - onWillDestroy, - useExternalListener, - useRef, - useState, - useSubEnv, - reactive, - } = owl; +const { + onError, + onMounted, + onWillDestroy, + useExternalListener, + useRef, + useState, + useSubEnv, + reactive, +} = owl; + +/** + * Chrome is the root component of the PoS App. + */ +class Chrome extends PosComponent { + setup() { + super.setup(); + useExternalListener(window, "beforeunload", this._onBeforeUnload); + useListener("show-main-screen", this.__showScreen); + useListener("toggle-debug-widget", debounce(this._toggleDebugWidget, 100)); + useListener("toggle-mobile-searchbar", this._toggleMobileSearchBar); + useListener("show-temp-screen", this.__showTempScreen); + useListener("close-temp-screen", this.__closeTempScreen); + useListener("close-pos", this._closePos); + useListener("loading-skip-callback", () => this.env.proxy.stop_searching()); + useListener("play-sound", this._onPlaySound); + useListener("set-sync-status", this._onSetSyncStatus); + useListener("show-notification", this._onShowNotification); + useListener("close-notification", this._onCloseNotification); + useListener("connect-to-proxy", this.connect_to_proxy); + useBus(this.env.posbus, "start-cash-control", this.openCashControl); + NumberBuffer.activate(); + + this.state = useState({ + uiState: "LOADING", // 'LOADING' | 'READY' | 'CLOSING' + debugWidgetIsShown: true, + mobileSearchBarIsShown: false, + hasBigScrollBars: false, + sound: { src: null }, + notification: { + isShown: false, + message: "", + duration: 2000, + }, + loadingSkipButtonIsShown: false, + }); + + this.mainScreen = useState({ name: null, component: null }); + this.mainScreenProps = {}; + + this.tempScreen = useState({ isShown: false, name: null, component: null }); + this.tempScreenProps = {}; + + this.progressbar = useRef("progressbar"); + + this.previous_touch_y_coordinate = -1; + + const pos = reactive( + this.env.pos, + batched(() => this.render(true)) + ); + useSubEnv({ pos }); + + onMounted(() => { + // remove default webclient handlers that induce click delay + $(document).off(); + $(window).off(); + $("html").off(); + $("body").off(); + }); + + onWillDestroy(() => { + this.env.pos.destroy(); + }); + + onError((error) => { + // error is an OwlError object. + console.error(error.cause); + }); + + this.props.setupIsDone(this); + } + + // GETTERS // + + get customerFacingDisplayButtonIsShown() { + return this.env.pos.config.iface_customer_facing_display; + } /** - * Chrome is the root component of the PoS App. + * Used to give the `state.mobileSearchBarIsShown` value to main screen props */ - class Chrome extends PosComponent { - setup() { - super.setup(); - useExternalListener(window, 'beforeunload', this._onBeforeUnload); - useListener('show-main-screen', this.__showScreen); - useListener('toggle-debug-widget', debounce(this._toggleDebugWidget, 100)); - useListener('toggle-mobile-searchbar', this._toggleMobileSearchBar); - useListener('show-temp-screen', this.__showTempScreen); - useListener('close-temp-screen', this.__closeTempScreen); - useListener('close-pos', this._closePos); - useListener('loading-skip-callback', () => this.env.proxy.stop_searching()); - useListener('play-sound', this._onPlaySound); - useListener('set-sync-status', this._onSetSyncStatus); - useListener('show-notification', this._onShowNotification); - useListener('close-notification', this._onCloseNotification); - useListener('connect-to-proxy', this.connect_to_proxy); - useBus(this.env.posbus, 'start-cash-control', this.openCashControl); - NumberBuffer.activate(); + get mainScreenPropsFielded() { + return Object.assign({}, this.mainScreenProps, { + mobileSearchBarIsShown: this.state.mobileSearchBarIsShown, + }); + } - this.state = useState({ - uiState: 'LOADING', // 'LOADING' | 'READY' | 'CLOSING' - debugWidgetIsShown: true, - mobileSearchBarIsShown: false, - hasBigScrollBars: false, - sound: { src: null }, - notification: { - isShown: false, - message: '', - duration: 2000, - }, - loadingSkipButtonIsShown: false, - }); - - this.mainScreen = useState({ name: null, component: null }); - this.mainScreenProps = {}; - - this.tempScreen = useState({ isShown: false, name: null, component: null }); - this.tempScreenProps = {}; - - this.progressbar = useRef('progressbar'); - - this.previous_touch_y_coordinate = -1; - - const pos = reactive(this.env.pos, batched(() => this.render(true))) - useSubEnv({ pos }); - - onMounted(() => { - // remove default webclient handlers that induce click delay - $(document).off(); - $(window).off(); - $('html').off(); - $('body').off(); - }); - - onWillDestroy(() => { - this.env.pos.destroy(); - }); - - onError((error) => { - // error is an OwlError object. - console.error(error.cause); - }); - - this.props.setupIsDone(this); + /** + * Startup screen can be based on pos config so the startup screen + * is only determined after pos data is completely loaded. + * + * NOTE: Wait for pos data to be completed before calling this getter. + */ + get startScreen() { + if (this.state.uiState !== "READY") { + console.warn( + `Accessing startScreen of Chrome component before 'state.uiState' to be 'READY' is not recommended.` + ); } + return { name: "ProductScreen" }; + } - // GETTERS // + // CONTROL METHODS // - get customerFacingDisplayButtonIsShown() { - return this.env.pos.config.iface_customer_facing_display; - } - - /** - * Used to give the `state.mobileSearchBarIsShown` value to main screen props - */ - get mainScreenPropsFielded() { - return Object.assign({}, this.mainScreenProps, { - mobileSearchBarIsShown: this.state.mobileSearchBarIsShown, - }); - } - - /** - * Startup screen can be based on pos config so the startup screen - * is only determined after pos data is completely loaded. - * - * NOTE: Wait for pos data to be completed before calling this getter. - */ - get startScreen() { - if (this.state.uiState !== 'READY') { - console.warn( - `Accessing startScreen of Chrome component before 'state.uiState' to be 'READY' is not recommended.` - ); + /** + * Call this function after the Chrome component is mounted. + * This will load pos and assign it to the environment. + */ + async start() { + try { + await this.env.pos.load_server_data(); + await this.setupBarcodeParser(); + if (this.env.pos.config.use_proxy) { + await this.connect_to_proxy(); } - return { name: 'ProductScreen' }; - } - - // CONTROL METHODS // - - /** - * Call this function after the Chrome component is mounted. - * This will load pos and assign it to the environment. - */ - async start() { - try { - await this.env.pos.load_server_data(); - await this.setupBarcodeParser(); - if(this.env.pos.config.use_proxy){ - await this.connect_to_proxy(); - } - // Load the saved `env.pos.toRefundLines` from localStorage when - // the PosGlobalState is ready. - Object.assign(this.env.pos.toRefundLines, this.env.pos.db.load('TO_REFUND_LINES') || {}); - this._buildChrome(); - this._closeOtherTabs(); - this.env.pos.selectedCategoryId = this.env.pos.config.start_category && this.env.pos.config.iface_start_categ_id + // Load the saved `env.pos.toRefundLines` from localStorage when + // the PosGlobalState is ready. + Object.assign( + this.env.pos.toRefundLines, + this.env.pos.db.load("TO_REFUND_LINES") || {} + ); + this._buildChrome(); + this._closeOtherTabs(); + this.env.pos.selectedCategoryId = + this.env.pos.config.start_category && this.env.pos.config.iface_start_categ_id ? this.env.pos.config.iface_start_categ_id[0] : 0; - this.state.uiState = 'READY'; - this._showStartScreen(); - setTimeout(() => this._runBackgroundTasks()); - } catch (error) { - let title = 'Unknown Error', - body; + this.state.uiState = "READY"; + this._showStartScreen(); + setTimeout(() => this._runBackgroundTasks()); + } catch (error) { + let title = "Unknown Error", + body; - if (error.message && [100, 200, 404, -32098].includes(error.message.code)) { - // this is the signature of rpc error - if (error.message.code === -32098) { - title = 'Network Failure (XmlHttpRequestError)'; - body = - 'The Point of Sale could not be loaded due to a network problem.\n' + - 'Please check your internet connection.'; - } else if (error.message.code === 200) { - title = error.message.data.message || this.env._t('Server Error'); - body = - error.message.data.debug || - this.env._t( - 'The server encountered an error while receiving your order.' - ); - } - } else if (error instanceof Error) { - title = error.message; - body = error.stack; + if (error.message && [100, 200, 404, -32098].includes(error.message.code)) { + // this is the signature of rpc error + if (error.message.code === -32098) { + title = "Network Failure (XmlHttpRequestError)"; + body = + "The Point of Sale could not be loaded due to a network problem.\n" + + "Please check your internet connection."; + } else if (error.message.code === 200) { + title = error.message.data.message || this.env._t("Server Error"); + body = + error.message.data.debug || + this.env._t("The server encountered an error while receiving your order."); } - - await this.showPopup('ErrorTracebackPopup', { - title, - body, - exitButtonIsShown: true, - }); + } else if (error instanceof Error) { + title = error.message; + body = error.stack; } - } - _runBackgroundTasks() { - // push order in the background, no need to await - this.env.pos.push_orders(); - // Allow using the app even if not all the images are loaded. - // Basically, preload the images in the background. - this._preloadImages(); - if (this.env.pos.config.limited_partners_loading && this.env.pos.config.partner_load_background) { - this.env.pos.loadPartnersBackground().then(() => { - this.env.pos.isEveryPartnerLoaded = true; - }); - } - if (this.env.pos.config.limited_products_loading && this.env.pos.config.product_load_background) { - this.env.pos.loadProductsBackground().then(() => { - this.env.pos.isEveryProductLoaded = true; - this.render(true); - }); - } + await this.showPopup("ErrorTracebackPopup", { + title, + body, + exitButtonIsShown: true, + }); } + } - setupBarcodeParser() { - const barcode_parser = new BarcodeParser({ nomenclature_id: this.env.pos.company.nomenclature_id }); - this.env.barcode_reader.set_barcode_parser(barcode_parser); - return barcode_parser.is_loaded(); + _runBackgroundTasks() { + // push order in the background, no need to await + this.env.pos.push_orders(); + // Allow using the app even if not all the images are loaded. + // Basically, preload the images in the background. + this._preloadImages(); + if ( + this.env.pos.config.limited_partners_loading && + this.env.pos.config.partner_load_background + ) { + this.env.pos.loadPartnersBackground().then(() => { + this.env.pos.isEveryPartnerLoaded = true; + }); } + if ( + this.env.pos.config.limited_products_loading && + this.env.pos.config.product_load_background + ) { + this.env.pos.loadProductsBackground().then(() => { + this.env.pos.isEveryProductLoaded = true; + this.render(true); + }); + } + } - connect_to_proxy() { - return new Promise((resolve, reject) => { - this.env.barcode_reader.disconnect_from_proxy(); - this.state.loadingSkipButtonIsShown = true; - this.env.proxy.autoconnect({ + setupBarcodeParser() { + const barcode_parser = new BarcodeParser({ + nomenclature_id: this.env.pos.company.nomenclature_id, + }); + this.env.barcode_reader.set_barcode_parser(barcode_parser); + return barcode_parser.is_loaded(); + } + + connect_to_proxy() { + return new Promise((resolve, reject) => { + this.env.barcode_reader.disconnect_from_proxy(); + this.state.loadingSkipButtonIsShown = true; + this.env.proxy + .autoconnect({ force_ip: this.env.pos.config.proxy_ip || undefined, - progress: function(prog){}, - }).then( + progress: function (prog) {}, + }) + .then( () => { if (this.env.pos.config.iface_scan_via_proxy) { this.env.barcode_reader.connect_to_proxy(); @@ -224,308 +241,319 @@ odoo.define('point_of_sale.Chrome', function(require) { // this should reject so that it can be captured when we wait for pos.ready // in the chrome component. // then, if it got really rejected, we can show the error. - if (statusText == 'error' && window.location.protocol == 'https:') { + if (statusText == "error" && window.location.protocol == "https:") { reject({ - title: this.env._t('HTTPS connection to IoT Box failed'), + title: this.env._t("HTTPS connection to IoT Box failed"), body: _.str.sprintf( - this.env._t('Make sure you are using IoT Box v18.12 or higher. Navigate to %s to accept the certificate of your IoT Box.'), + this.env._t( + "Make sure you are using IoT Box v18.12 or higher. Navigate to %s to accept the certificate of your IoT Box." + ), url ), - popup: 'alert', + popup: "alert", }); } else { resolve(); } } ); - }); + }); + } + + openCashControl() { + if (this.shouldShowCashControl()) { + this.showPopup("CashOpeningPopup", { keepBehind: true }); + } + } + + shouldShowCashControl() { + return ( + this.env.pos.config.cash_control && this.env.pos.pos_session.state == "opening_control" + ); + } + + // EVENT HANDLERS // + + _showStartScreen() { + const { name, props } = this.startScreen; + this.showScreen(name, props); + } + _getSavedScreen(order) { + return order.get_screen_data(); + } + __showTempScreen(event) { + const { name, props, resolve } = event.detail; + this.tempScreen.isShown = true; + this.tempScreen.name = name; + this.tempScreen.component = this.constructor.components[name]; + this.tempScreenProps = Object.assign({}, props, { resolve }); + } + __closeTempScreen() { + this.tempScreen.isShown = false; + this.tempScreen.name = null; + } + __showScreen({ detail: { name, props = {} } }) { + const component = this.constructor.components[name]; + // 1. Set the information of the screen to display. + this.mainScreen.name = name; + this.mainScreen.component = component; + this.mainScreenProps = props; + + // 2. Save the screen to the order. + // - This screen is shown when the order is selected. + if ( + !(component.prototype instanceof IndependentToOrderScreen) && + name !== "ReprintReceiptScreen" + ) { + this._setScreenData(name, props); + } + } + /** + * Set the latest screen to the current order. This is done so that + * when the order is selected again, the ui returns to the latest screen + * saved in the order. + * + * @param {string} name Screen name + * @param {Object} props props for the Screen component + */ + _setScreenData(name, props) { + const order = this.env.pos.get_order(); + if (order) { + order.set_screen_data({ name, props }); + } + } + async _closePos() { + // If pos is not properly loaded, we just go back to /web without + // doing anything in the order data. + if (!this.env.pos || this.env.pos.db.get_orders().length === 0) { + window.location = "/web#action=point_of_sale.action_client_pos_menu"; } - openCashControl() { - if (this.shouldShowCashControl()) { - this.showPopup('CashOpeningPopup', { keepBehind: true }); - } - } - - shouldShowCashControl() { - return this.env.pos.config.cash_control && this.env.pos.pos_session.state == 'opening_control'; - } - - // EVENT HANDLERS // - - _showStartScreen() { - const { name, props } = this.startScreen; - this.showScreen(name, props); - } - _getSavedScreen(order) { - return order.get_screen_data(); - } - __showTempScreen(event) { - const { name, props, resolve } = event.detail; - this.tempScreen.isShown = true; - this.tempScreen.name = name; - this.tempScreen.component = this.constructor.components[name]; - this.tempScreenProps = Object.assign({}, props, { resolve }); - } - __closeTempScreen() { - this.tempScreen.isShown = false; - this.tempScreen.name = null; - } - __showScreen({ detail: { name, props = {} } }) { - const component = this.constructor.components[name]; - // 1. Set the information of the screen to display. - this.mainScreen.name = name; - this.mainScreen.component = component; - this.mainScreenProps = props; - - // 2. Save the screen to the order. - // - This screen is shown when the order is selected. - if (!(component.prototype instanceof IndependentToOrderScreen) && name !== "ReprintReceiptScreen") { - this._setScreenData(name, props); - } - } - /** - * Set the latest screen to the current order. This is done so that - * when the order is selected again, the ui returns to the latest screen - * saved in the order. - * - * @param {string} name Screen name - * @param {Object} props props for the Screen component - */ - _setScreenData(name, props) { - const order = this.env.pos.get_order(); - if (order) { - order.set_screen_data({ name, props }); - } - } - async _closePos() { - // If pos is not properly loaded, we just go back to /web without - // doing anything in the order data. - if (!this.env.pos || this.env.pos.db.get_orders().length === 0) { - window.location = '/web#action=point_of_sale.action_client_pos_menu'; - } - - if (this.env.pos.db.get_orders().length) { - // If there are orders in the db left unsynced, we try to sync. - // If sync successful, close without asking. - // Otherwise, ask again saying that some orders are not yet synced. - try { - await this.env.pos.push_orders(); - window.location = '/web#action=point_of_sale.action_client_pos_menu'; - } catch (error) { - console.warn(error); - const reason = this.env.pos.failed - ? this.env._t( - 'Some orders could not be submitted to ' + - 'the server due to configuration errors. ' + - 'You can exit the Point of Sale, but do ' + - 'not close the session before the issue ' + - 'has been resolved.' - ) - : this.env._t( - 'Some orders could not be submitted to ' + - 'the server due to internet connection issues. ' + - 'You can exit the Point of Sale, but do ' + - 'not close the session before the issue ' + - 'has been resolved.' - ); - const { confirmed } = await this.showPopup('ConfirmPopup', { - title: this.env._t('Offline Orders'), - body: reason, - }); - if (confirmed) { - this.state.uiState = 'CLOSING'; - this.state.loadingSkipButtonIsShown = false; - window.location = '/web#action=point_of_sale.action_client_pos_menu'; - } - } - } - } - _toggleDebugWidget() { - this.state.debugWidgetIsShown = !this.state.debugWidgetIsShown; - } - _toggleMobileSearchBar({ detail: isSearchBarEnabled }) { - if (isSearchBarEnabled !== null) { - this.state.mobileSearchBarIsShown = isSearchBarEnabled; - } else { - this.state.mobileSearchBarIsShown = !this.state.mobileSearchBarIsShown; - } - } - _onPlaySound({ detail: name }) { - let src; - if (name === 'error') { - src = "/point_of_sale/static/src/sounds/error.wav"; - } else if (name === 'bell') { - src = "/point_of_sale/static/src/sounds/bell.wav"; - } - this.state.sound.src = src; - } - _onSetSyncStatus({ detail: { status, pending }}) { - this.env.pos.synch.status = status; - this.env.pos.synch.pending = pending; - } - _onShowNotification({ detail: { message, duration } }) { - this.state.notification.isShown = true; - this.state.notification.message = message; - this.state.notification.duration = duration; - } - _onCloseNotification() { - this.state.notification.isShown = false; - } - /** - * Save `env.pos.toRefundLines` in localStorage on beforeunload - closing the - * browser, reloading or going to other page. - */ - _onBeforeUnload() { - this.env.pos.db.save('TO_REFUND_LINES', this.env.pos.toRefundLines); - } - - get isTicketScreenShown() { - return this.mainScreen.name === 'TicketScreen'; - } - - // MISC METHODS // - _preloadImages() { - for (let product of this.env.pos.db.get_product_by_category(0)) { - const image = new Image(); - image.src = `/web/image?model=product.product&field=image_128&id=${product.id}&unique=${product.write_date}`; - } - for (let category of Object.values(this.env.pos.db.category_by_id)) { - if (category.id == 0) continue; - const image = new Image(); - image.src = `/web/image?model=pos.category&field=image_128&id=${category.id}&unique=${category.write_date}`; - } - const staticImages = ['backspace.png', 'bc-arrow-big.png']; - for (let imageName of staticImages) { - const image = new Image(); - image.src = `/point_of_sale/static/src/img/${imageName}`; - } - } - - _buildChrome() { - if ($.browser.chrome) { - var chrome_version = $.browser.version.split('.')[0]; - if (parseInt(chrome_version, 10) >= 50) { - loadCSS('/point_of_sale/static/src/css/chrome50.css'); - } - } - - if (this.env.pos.config.iface_big_scrollbars) { - this.state.hasBigScrollBars = true; - } - - this._disableBackspaceBack(); - } - // prevent backspace from performing a 'back' navigation - _disableBackspaceBack() { - $(document).on('keydown', function (e) { - if (e.which === 8 && !$(e.target).is('input, textarea')) { - e.preventDefault(); - } - }); - } - _closeOtherTabs() { - localStorage['message'] = ''; - localStorage['message'] = JSON.stringify({ - message: 'close_tabs', - session: this.env.pos.pos_session.id, - }); - - window.addEventListener( - 'storage', - (event) => { - if (event.key === 'message' && event.newValue) { - const msg = JSON.parse(event.newValue); - if ( - msg.message === 'close_tabs' && - msg.session == this.env.pos.pos_session.id - ) { - console.info( - 'POS / Session opened in another window. EXITING POS' - ); - this._closePos(); - } - } - }, - false - ); - } - showCashMoveButton() { - return this.env.pos && this.env.pos.config && this.env.pos.config.cash_control; - } - - // UNEXPECTED ERROR HANDLING // - - /** - * This method is used to handle unexpected errors. It is registered to - * the `error_handlers` service when this component is properly mounted. - * See `onMounted` hook of the `ChromeAdapter` component. - * @param {*} env - * @param {UncaughtClientError | UncaughtPromiseError} error - * @param {*} originalError - * @returns {boolean} - */ - errorHandler(env, error, originalError) { - if (!env.pos) return false; - const errorToHandle = identifyError(originalError); - // Assume that the unhandled falsey rejections can be ignored. - if (errorToHandle) { - this._errorHandler(error, errorToHandle); - } - return true; - } - - _errorHandler(error, errorToHandle) { - if (errorToHandle instanceof RPCError) { - const { message, data } = errorToHandle; - if (odooExceptionTitleMap.has(errorToHandle.exceptionName)) { - const title = odooExceptionTitleMap.get(errorToHandle.exceptionName).toString(); - this.showPopup('ErrorPopup', { title, body: data.message }); - } else { - this.showPopup('ErrorTracebackPopup', { - title: message, - body: data.message + '\n' + data.debug + '\n', - }); - } - } else if (errorToHandle instanceof ConnectionLostError) { - this.showPopup('OfflineErrorPopup', { - title: this.env._t('Connection is lost'), - body: this.env._t('Check the internet connection then try again.'), + if (this.env.pos.db.get_orders().length) { + // If there are orders in the db left unsynced, we try to sync. + // If sync successful, close without asking. + // Otherwise, ask again saying that some orders are not yet synced. + try { + await this.env.pos.push_orders(); + window.location = "/web#action=point_of_sale.action_client_pos_menu"; + } catch (error) { + console.warn(error); + const reason = this.env.pos.failed + ? this.env._t( + "Some orders could not be submitted to " + + "the server due to configuration errors. " + + "You can exit the Point of Sale, but do " + + "not close the session before the issue " + + "has been resolved." + ) + : this.env._t( + "Some orders could not be submitted to " + + "the server due to internet connection issues. " + + "You can exit the Point of Sale, but do " + + "not close the session before the issue " + + "has been resolved." + ); + const { confirmed } = await this.showPopup("ConfirmPopup", { + title: this.env._t("Offline Orders"), + body: reason, }); - } else if (errorToHandle instanceof ConnectionAbortedError) { - this.showPopup('OfflineErrorPopup', { - title: this.env._t('Connection is aborted'), - body: this.env._t('Check the internet connection then try again.'), - }); - } else if (errorToHandle instanceof Error) { - // If `errorToHandle` is a normal Error (such as TypeError), - // the annotated traceback can be found from `error`. - this.showPopup('ErrorTracebackPopup', { - // Hopefully the message is translated. - title: `${errorToHandle.name}: ${errorToHandle.message}`, - body: error.traceback, - }); - } else { - // Hey developer. It's your fault that the error reach here. - // Please, throw an Error object in order to get stack trace of the error. - // At least we can find the file that throws the error when you look - // at the console. - this.showPopup('ErrorPopup', { - title: this.env._t('Unknown Error'), - body: this.env._t('Unable to show information about this error.'), - }); - console.error('Unknown error. Unable to show information about this error.', errorToHandle); + if (confirmed) { + this.state.uiState = "CLOSING"; + this.state.loadingSkipButtonIsShown = false; + window.location = "/web#action=point_of_sale.action_client_pos_menu"; + } } } } - Chrome.template = 'Chrome'; - Object.defineProperty(Chrome, "components", { - get () { - return Object.assign({ Transition }, PosComponent.components); + _toggleDebugWidget() { + this.state.debugWidgetIsShown = !this.state.debugWidgetIsShown; + } + _toggleMobileSearchBar({ detail: isSearchBarEnabled }) { + if (isSearchBarEnabled !== null) { + this.state.mobileSearchBarIsShown = isSearchBarEnabled; + } else { + this.state.mobileSearchBarIsShown = !this.state.mobileSearchBarIsShown; } - }) + } + _onPlaySound({ detail: name }) { + let src; + if (name === "error") { + src = "/point_of_sale/static/src/sounds/error.wav"; + } else if (name === "bell") { + src = "/point_of_sale/static/src/sounds/bell.wav"; + } + this.state.sound.src = src; + } + _onSetSyncStatus({ detail: { status, pending } }) { + this.env.pos.synch.status = status; + this.env.pos.synch.pending = pending; + } + _onShowNotification({ detail: { message, duration } }) { + this.state.notification.isShown = true; + this.state.notification.message = message; + this.state.notification.duration = duration; + } + _onCloseNotification() { + this.state.notification.isShown = false; + } + /** + * Save `env.pos.toRefundLines` in localStorage on beforeunload - closing the + * browser, reloading or going to other page. + */ + _onBeforeUnload() { + this.env.pos.db.save("TO_REFUND_LINES", this.env.pos.toRefundLines); + } - Registries.Component.add(Chrome); + get isTicketScreenShown() { + return this.mainScreen.name === "TicketScreen"; + } - return Chrome; + // MISC METHODS // + _preloadImages() { + for (const product of this.env.pos.db.get_product_by_category(0)) { + const image = new Image(); + image.src = `/web/image?model=product.product&field=image_128&id=${product.id}&unique=${product.write_date}`; + } + for (const category of Object.values(this.env.pos.db.category_by_id)) { + if (category.id == 0) { + continue; + } + const image = new Image(); + image.src = `/web/image?model=pos.category&field=image_128&id=${category.id}&unique=${category.write_date}`; + } + const staticImages = ["backspace.png", "bc-arrow-big.png"]; + for (const imageName of staticImages) { + const image = new Image(); + image.src = `/point_of_sale/static/src/img/${imageName}`; + } + } + + _buildChrome() { + if ($.browser.chrome) { + var chrome_version = $.browser.version.split(".")[0]; + if (parseInt(chrome_version, 10) >= 50) { + loadCSS("/point_of_sale/static/src/css/chrome50.css"); + } + } + + if (this.env.pos.config.iface_big_scrollbars) { + this.state.hasBigScrollBars = true; + } + + this._disableBackspaceBack(); + } + // prevent backspace from performing a 'back' navigation + _disableBackspaceBack() { + $(document).on("keydown", function (e) { + if (e.which === 8 && !$(e.target).is("input, textarea")) { + e.preventDefault(); + } + }); + } + _closeOtherTabs() { + localStorage["message"] = ""; + localStorage["message"] = JSON.stringify({ + message: "close_tabs", + session: this.env.pos.pos_session.id, + }); + + window.addEventListener( + "storage", + (event) => { + if (event.key === "message" && event.newValue) { + const msg = JSON.parse(event.newValue); + if ( + msg.message === "close_tabs" && + msg.session == this.env.pos.pos_session.id + ) { + console.info("POS / Session opened in another window. EXITING POS"); + this._closePos(); + } + } + }, + false + ); + } + showCashMoveButton() { + return this.env.pos && this.env.pos.config && this.env.pos.config.cash_control; + } + + // UNEXPECTED ERROR HANDLING // + + /** + * This method is used to handle unexpected errors. It is registered to + * the `error_handlers` service when this component is properly mounted. + * See `onMounted` hook of the `ChromeAdapter` component. + * @param {*} env + * @param {UncaughtClientError | UncaughtPromiseError} error + * @param {*} originalError + * @returns {boolean} + */ + errorHandler(env, error, originalError) { + if (!env.pos) { + return false; + } + const errorToHandle = identifyError(originalError); + // Assume that the unhandled falsey rejections can be ignored. + if (errorToHandle) { + this._errorHandler(error, errorToHandle); + } + return true; + } + + _errorHandler(error, errorToHandle) { + if (errorToHandle instanceof RPCError) { + const { message, data } = errorToHandle; + if (odooExceptionTitleMap.has(errorToHandle.exceptionName)) { + const title = odooExceptionTitleMap.get(errorToHandle.exceptionName).toString(); + this.showPopup("ErrorPopup", { title, body: data.message }); + } else { + this.showPopup("ErrorTracebackPopup", { + title: message, + body: data.message + "\n" + data.debug + "\n", + }); + } + } else if (errorToHandle instanceof ConnectionLostError) { + this.showPopup("OfflineErrorPopup", { + title: this.env._t("Connection is lost"), + body: this.env._t("Check the internet connection then try again."), + }); + } else if (errorToHandle instanceof ConnectionAbortedError) { + this.showPopup("OfflineErrorPopup", { + title: this.env._t("Connection is aborted"), + body: this.env._t("Check the internet connection then try again."), + }); + } else if (errorToHandle instanceof Error) { + // If `errorToHandle` is a normal Error (such as TypeError), + // the annotated traceback can be found from `error`. + this.showPopup("ErrorTracebackPopup", { + // Hopefully the message is translated. + title: `${errorToHandle.name}: ${errorToHandle.message}`, + body: error.traceback, + }); + } else { + // Hey developer. It's your fault that the error reach here. + // Please, throw an Error object in order to get stack trace of the error. + // At least we can find the file that throws the error when you look + // at the console. + this.showPopup("ErrorPopup", { + title: this.env._t("Unknown Error"), + body: this.env._t("Unable to show information about this error."), + }); + console.error( + "Unknown error. Unable to show information about this error.", + errorToHandle + ); + } + } +} +Chrome.template = "Chrome"; +Object.defineProperty(Chrome, "components", { + get() { + return Object.assign({ Transition }, 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 9289c0c37a1..340a443655b 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js @@ -1,59 +1,62 @@ -odoo.define('point_of_sale.CashMoveButton', function (require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); - const { _t } = require('web.core'); - const { renderToString } = require('@web/core/utils/render'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; +import { _t } from "web.core"; +import { renderToString } from "@web/core/utils/render"; - const TRANSLATED_CASH_MOVE_TYPE = { - in: _t('in'), - out: _t('out'), - }; +const TRANSLATED_CASH_MOVE_TYPE = { + in: _t("in"), + out: _t("out"), +}; - class CashMoveButton extends PosComponent { - async onClick() { - const { confirmed, payload } = await this.showPopup('CashMovePopup'); - if (!confirmed) return; - const { type, amount, reason } = payload; - const translatedType = TRANSLATED_CASH_MOVE_TYPE[type]; - const formattedAmount = this.env.pos.format_currency(amount); - if (!amount) { - return this.showNotification( - _.str.sprintf(this.env._t('Cash in/out of %s is ignored.'), formattedAmount), - 3000 - ); - } - const extras = { formattedAmount, translatedType }; - await this.rpc({ - model: 'pos.session', - method: 'try_cash_in_out', - args: [[this.env.pos.pos_session.id], type, amount, reason, extras], - }); - if (this.env.proxy.printer) { - const renderedReceipt = renderToString('point_of_sale.CashMoveReceipt', { - _receipt: this._getReceiptInfo({ ...payload, translatedType, formattedAmount }), - }); - const printResult = await this.env.proxy.printer.print_receipt(renderedReceipt); - if (!printResult.successful) { - this.showPopup('ErrorPopup', { title: printResult.message.title, body: printResult.message.body }); - } - } - this.showNotification( - _.str.sprintf(this.env._t('Successfully made a cash %s of %s.'), type, formattedAmount), +class CashMoveButton extends PosComponent { + async onClick() { + const { confirmed, payload } = await this.showPopup("CashMovePopup"); + if (!confirmed) { + return; + } + const { type, amount, reason } = payload; + const translatedType = TRANSLATED_CASH_MOVE_TYPE[type]; + const formattedAmount = this.env.pos.format_currency(amount); + if (!amount) { + return this.showNotification( + _.str.sprintf(this.env._t("Cash in/out of %s is ignored."), formattedAmount), 3000 ); } - _getReceiptInfo(payload) { - const result = { ...payload }; - result.cashier = this.env.pos.get_cashier(); - result.company = this.env.pos.company; - return result; + const extras = { formattedAmount, translatedType }; + await this.rpc({ + model: "pos.session", + method: "try_cash_in_out", + args: [[this.env.pos.pos_session.id], type, amount, reason, extras], + }); + if (this.env.proxy.printer) { + const renderedReceipt = renderToString("point_of_sale.CashMoveReceipt", { + _receipt: this._getReceiptInfo({ ...payload, translatedType, formattedAmount }), + }); + const printResult = await this.env.proxy.printer.print_receipt(renderedReceipt); + if (!printResult.successful) { + this.showPopup("ErrorPopup", { + title: printResult.message.title, + body: printResult.message.body, + }); + } } + this.showNotification( + _.str.sprintf(this.env._t("Successfully made a cash %s of %s."), type, formattedAmount), + 3000 + ); } - CashMoveButton.template = 'point_of_sale.CashMoveButton'; + _getReceiptInfo(payload) { + const result = { ...payload }; + result.cashier = this.env.pos.get_cashier(); + result.company = this.env.pos.company; + return result; + } +} +CashMoveButton.template = "point_of_sale.CashMoveButton"; - Registries.Component.add(CashMoveButton); +Registries.Component.add(CashMoveButton); - return 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 ba9489ae28b..db19c43c047 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js @@ -1,27 +1,25 @@ -odoo.define('point_of_sale.CashierName', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - // Previously UsernameWidget - class CashierName extends PosComponent { - get username() { - const { name } = this.env.pos.get_cashier(); - return name ? name : ''; - } - get avatar() { - const user_id = this.env.pos.get_cashier_user_id(); - const id = user_id ? user_id : -1; - return `/web/image/res.users/${id}/avatar_128`; - } - get cssClass() { - return {'not-clickable': true}; - } +// Previously UsernameWidget +class CashierName extends PosComponent { + get username() { + const { name } = this.env.pos.get_cashier(); + return name ? name : ""; } - CashierName.template = 'CashierName'; + get avatar() { + const user_id = this.env.pos.get_cashier_user_id(); + const id = user_id ? user_id : -1; + return `/web/image/res.users/${id}/avatar_128`; + } + get cssClass() { + return { "not-clickable": true }; + } +} +CashierName.template = "CashierName"; - Registries.Component.add(CashierName); +Registries.Component.add(CashierName); - return 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 cdd5db7c528..4fce19efca4 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js @@ -1,106 +1,108 @@ -odoo.define('point_of_sale.CustomerFacingDisplayButton', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - const { useState } = owl; +const { useState } = owl; - class CustomerFacingDisplayButton extends PosComponent { - setup() { - super.setup(); - this.local = this.env.pos.config.iface_customer_facing_display_local && !this.env.pos.config.iface_customer_facing_display_via_proxy; - this.state = useState({ status: this.local ? 'success' : 'failure' }); - this._start(); - } - get message() { - return { - success: '', - warning: this.env._t('Connected, Not Owned'), - failure: this.env._t('Disconnected'), - not_found: this.env._t('Customer Screen Unsupported. Please upgrade the IoT Box'), - }[this.state.status]; - } - onClick() { - if (this.local) { - return this.onClickLocal(); - } else { - return this.onClickProxy(); - } - } - async onClickLocal() { - this.env.pos.customer_display = window.open('', 'Customer Display', 'height=600,width=900'); - const renderedHtml = await this.env.pos.render_html_for_customer_facing_display(); - var $renderedHtml = $('
').html(renderedHtml); - $(this.env.pos.customer_display.document.body).html($renderedHtml.find('.pos-customer_facing_display')); - $(this.env.pos.customer_display.document.head).html($renderedHtml.find('.resources').html()); - } - async onClickProxy() { - try { - const renderedHtml = await this.env.pos.render_html_for_customer_facing_display(); - let ownership = await this.env.proxy.take_ownership_over_customer_screen( - renderedHtml - ); - if (typeof ownership === 'string') { - ownership = JSON.parse(ownership); - } - if (ownership.status === 'success') { - this.state.status = 'success'; - } else { - this.state.status = 'warning'; - } - if (!this.env.proxy.posbox_supports_display) { - this.env.proxy.posbox_supports_display = true; - this._start(); - } - } catch (error) { - if (typeof error == 'undefined') { - this.state.status = 'failure'; - } else { - this.state.status = 'not_found'; - } - } - } - _start() { - if (this.local) { - return; - } - - const self = this; - async function loop() { - if (self.env.proxy.posbox_supports_display) { - try { - let ownership = await self.env.proxy.test_ownership_of_customer_screen(); - if (typeof ownership === 'string') { - ownership = JSON.parse(ownership); - } - if (ownership.status === 'OWNER') { - self.state.status = 'success'; - } else { - self.state.status = 'warning'; - } - setTimeout(loop, 3000); - } catch (error) { - if (error.abort) { - // Stop the loop - return; - } - if (typeof error == 'undefined') { - self.state.status = 'failure'; - } else { - self.state.status = 'not_found'; - self.env.proxy.posbox_supports_display = false; - } - setTimeout(loop, 3000); - } - } - } - loop(); +class CustomerFacingDisplayButton extends PosComponent { + setup() { + super.setup(); + this.local = + this.env.pos.config.iface_customer_facing_display_local && + !this.env.pos.config.iface_customer_facing_display_via_proxy; + this.state = useState({ status: this.local ? "success" : "failure" }); + this._start(); + } + get message() { + return { + success: "", + warning: this.env._t("Connected, Not Owned"), + failure: this.env._t("Disconnected"), + not_found: this.env._t("Customer Screen Unsupported. Please upgrade the IoT Box"), + }[this.state.status]; + } + onClick() { + if (this.local) { + return this.onClickLocal(); + } else { + return this.onClickProxy(); } } - CustomerFacingDisplayButton.template = 'CustomerFacingDisplayButton'; + async onClickLocal() { + this.env.pos.customer_display = window.open("", "Customer Display", "height=600,width=900"); + const renderedHtml = await this.env.pos.render_html_for_customer_facing_display(); + var $renderedHtml = $("
").html(renderedHtml); + $(this.env.pos.customer_display.document.body).html( + $renderedHtml.find(".pos-customer_facing_display") + ); + $(this.env.pos.customer_display.document.head).html( + $renderedHtml.find(".resources").html() + ); + } + async onClickProxy() { + try { + const renderedHtml = await this.env.pos.render_html_for_customer_facing_display(); + let ownership = await this.env.proxy.take_ownership_over_customer_screen(renderedHtml); + if (typeof ownership === "string") { + ownership = JSON.parse(ownership); + } + if (ownership.status === "success") { + this.state.status = "success"; + } else { + this.state.status = "warning"; + } + if (!this.env.proxy.posbox_supports_display) { + this.env.proxy.posbox_supports_display = true; + this._start(); + } + } catch (error) { + if (typeof error == "undefined") { + this.state.status = "failure"; + } else { + this.state.status = "not_found"; + } + } + } + _start() { + if (this.local) { + return; + } - Registries.Component.add(CustomerFacingDisplayButton); + const self = this; + async function loop() { + if (self.env.proxy.posbox_supports_display) { + try { + let ownership = await self.env.proxy.test_ownership_of_customer_screen(); + if (typeof ownership === "string") { + ownership = JSON.parse(ownership); + } + if (ownership.status === "OWNER") { + self.state.status = "success"; + } else { + self.state.status = "warning"; + } + setTimeout(loop, 3000); + } catch (error) { + if (error.abort) { + // Stop the loop + return; + } + if (typeof error == "undefined") { + self.state.status = "failure"; + } else { + self.state.status = "not_found"; + self.env.proxy.posbox_supports_display = false; + } + setTimeout(loop, 3000); + } + } + } + loop(); + } +} +CustomerFacingDisplayButton.template = "CustomerFacingDisplayButton"; - return CustomerFacingDisplayButton; -}); +Registries.Component.add(CustomerFacingDisplayButton); + +export default CustomerFacingDisplayButton; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/DebugWidget.js b/addons/point_of_sale/static/src/js/ChromeWidgets/DebugWidget.js index de0c1a83b0b..79f874d6bac 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/DebugWidget.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/DebugWidget.js @@ -1,163 +1,161 @@ -odoo.define('point_of_sale.DebugWidget', function (require) { - 'use strict'; +/** @odoo-module */ - const { getFileAsText } = require('point_of_sale.utils'); - const { parse } = require('web.field_utils'); - const NumberBuffer = require('point_of_sale.NumberBuffer'); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import { getFileAsText } from "@point_of_sale/js/utils"; +import { parse } from "web.field_utils"; +import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - const { onMounted, onWillUnmount, useRef, useState } = owl; +const { onMounted, onWillUnmount, useRef, useState } = owl; - class DebugWidget extends PosComponent { - setup() { - super.setup(); - this.state = useState({ - barcodeInput: '', - weightInput: '', - isPaidOrdersReady: false, - isUnpaidOrdersReady: false, - buffer: NumberBuffer.get(), - }); +class DebugWidget extends PosComponent { + setup() { + super.setup(); + this.state = useState({ + barcodeInput: "", + weightInput: "", + isPaidOrdersReady: false, + isUnpaidOrdersReady: false, + buffer: NumberBuffer.get(), + }); - // NOTE: Perhaps this can still be improved. - // What we do here is loop thru the `event` elements - // then we assign animation that happens when the event is triggered - // in the proxy. E.g. if open_cashbox is sent, the open_cashbox element - // changes color from '#6CD11D' to '#1E1E1E' for a duration of 2sec. - this.eventElementsRef = {}; - this.animations = {}; - for (let eventName of ['open_cashbox', 'print_receipt', 'scale_read']) { - this.eventElementsRef[eventName] = useRef(eventName); - this.env.proxy.add_notification( - eventName, - (() => { - if (this.animations[eventName]) { - this.animations[eventName].cancel(); - } - const eventElement = this.eventElementsRef[eventName].el; - eventElement.style.backgroundColor = '#6CD11D'; - this.animations[eventName] = eventElement.animate( - { backgroundColor: ['#6CD11D', '#1E1E1E'] }, - 2000 - ); - }).bind(this) - ); - } - - onMounted(() => { - NumberBuffer.on('buffer-update', this, this._onBufferUpdate); - }); - - onWillUnmount(() => { - NumberBuffer.off('buffer-update', this, this._onBufferUpdate); - }); - } - toggleWidget() { - this.state.isShown = !this.state.isShown; - } - setWeight() { - var weightInKg = parse.float(this.state.weightInput); - if (!isNaN(weightInKg)) { - this.env.proxy.debug_set_weight(weightInKg); - } - } - resetWeight() { - this.state.weightInput = ''; - this.env.proxy.debug_reset_weight(); - } - async barcodeScan() { - await this.env.barcode_reader.scan(this.state.barcodeInput); - } - async barcodeScanEAN() { - const ean = this.env.barcode_reader.barcode_parser.sanitize_ean( - this.state.barcodeInput || '0' + // NOTE: Perhaps this can still be improved. + // What we do here is loop thru the `event` elements + // then we assign animation that happens when the event is triggered + // in the proxy. E.g. if open_cashbox is sent, the open_cashbox element + // changes color from '#6CD11D' to '#1E1E1E' for a duration of 2sec. + this.eventElementsRef = {}; + this.animations = {}; + for (const eventName of ["open_cashbox", "print_receipt", "scale_read"]) { + this.eventElementsRef[eventName] = useRef(eventName); + this.env.proxy.add_notification( + eventName, + (() => { + if (this.animations[eventName]) { + this.animations[eventName].cancel(); + } + const eventElement = this.eventElementsRef[eventName].el; + eventElement.style.backgroundColor = "#6CD11D"; + this.animations[eventName] = eventElement.animate( + { backgroundColor: ["#6CD11D", "#1E1E1E"] }, + 2000 + ); + }).bind(this) ); - this.state.barcodeInput = ean; - await this.env.barcode_reader.scan(ean); } - async deleteOrders() { - 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.' - ), - }); - if (confirmed) { - this.env.pos.db.remove_all_orders(); - this.env.pos.set_synch('connected', 0); - } - } - async deleteUnpaidOrders() { - 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.' - ), - }); - if (confirmed) { - this.env.pos.db.remove_all_unpaid_orders(); - window.location = '/'; - } - } - _createBlob(contents) { - if (typeof contents !== 'string') { - contents = JSON.stringify(contents, null, 2); - } - return new Blob([contents]); - } - // IMPROVEMENT: Duplicated codes for downloading paid and unpaid orders. - // The implementation can be better. - preparePaidOrders() { - try { - this.paidOrdersBlob = this._createBlob(this.env.pos.export_paid_orders()); - this.state.isPaidOrdersReady = true; - } catch (error) { - console.warn(error); - } - } - get paidOrdersFilename() { - return `${this.env._t('paid orders')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.json`; - } - get paidOrdersURL() { - var URL = window.URL || window.webkitURL; - return URL.createObjectURL(this.paidOrdersBlob); - } - prepareUnpaidOrders() { - try { - this.unpaidOrdersBlob = this._createBlob(this.env.pos.export_unpaid_orders()); - this.state.isUnpaidOrdersReady = true; - } catch (error) { - console.warn(error); - } - } - get unpaidOrdersFilename() { - return `${this.env._t('unpaid orders')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.json`; - } - get unpaidOrdersURL() { - var URL = window.URL || window.webkitURL; - return URL.createObjectURL(this.unpaidOrdersBlob); - } - async importOrders(event) { - const file = event.target.files[0]; - if (file) { - const report = this.env.pos.import_orders(await getFileAsText(file)); - await this.showPopup('OrderImportPopup', { report }); - } - } - refreshDisplay() { - this.env.proxy.message('display_refresh', {}); - } - _onBufferUpdate(buffer) { - this.state.buffer = buffer; - } - get bufferRepr() { - return `"${this.state.buffer}"`; + + onMounted(() => { + NumberBuffer.on("buffer-update", this, this._onBufferUpdate); + }); + + onWillUnmount(() => { + NumberBuffer.off("buffer-update", this, this._onBufferUpdate); + }); + } + toggleWidget() { + this.state.isShown = !this.state.isShown; + } + setWeight() { + var weightInKg = parse.float(this.state.weightInput); + if (!isNaN(weightInKg)) { + this.env.proxy.debug_set_weight(weightInKg); } } - DebugWidget.template = 'DebugWidget'; + resetWeight() { + this.state.weightInput = ""; + this.env.proxy.debug_reset_weight(); + } + async barcodeScan() { + await this.env.barcode_reader.scan(this.state.barcodeInput); + } + async barcodeScanEAN() { + const ean = this.env.barcode_reader.barcode_parser.sanitize_ean( + this.state.barcodeInput || "0" + ); + this.state.barcodeInput = ean; + await this.env.barcode_reader.scan(ean); + } + async deleteOrders() { + 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." + ), + }); + if (confirmed) { + this.env.pos.db.remove_all_orders(); + this.env.pos.set_synch("connected", 0); + } + } + async deleteUnpaidOrders() { + 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." + ), + }); + if (confirmed) { + this.env.pos.db.remove_all_unpaid_orders(); + window.location = "/"; + } + } + _createBlob(contents) { + if (typeof contents !== "string") { + contents = JSON.stringify(contents, null, 2); + } + return new Blob([contents]); + } + // IMPROVEMENT: Duplicated codes for downloading paid and unpaid orders. + // The implementation can be better. + preparePaidOrders() { + try { + this.paidOrdersBlob = this._createBlob(this.env.pos.export_paid_orders()); + this.state.isPaidOrdersReady = true; + } catch (error) { + console.warn(error); + } + } + get paidOrdersFilename() { + return `${this.env._t("paid orders")} ${moment().format("YYYY-MM-DD-HH-mm-ss")}.json`; + } + get paidOrdersURL() { + var URL = window.URL || window.webkitURL; + return URL.createObjectURL(this.paidOrdersBlob); + } + prepareUnpaidOrders() { + try { + this.unpaidOrdersBlob = this._createBlob(this.env.pos.export_unpaid_orders()); + this.state.isUnpaidOrdersReady = true; + } catch (error) { + console.warn(error); + } + } + get unpaidOrdersFilename() { + return `${this.env._t("unpaid orders")} ${moment().format("YYYY-MM-DD-HH-mm-ss")}.json`; + } + get unpaidOrdersURL() { + var URL = window.URL || window.webkitURL; + return URL.createObjectURL(this.unpaidOrdersBlob); + } + async importOrders(event) { + const file = event.target.files[0]; + if (file) { + const report = this.env.pos.import_orders(await getFileAsText(file)); + await this.showPopup("OrderImportPopup", { report }); + } + } + refreshDisplay() { + this.env.proxy.message("display_refresh", {}); + } + _onBufferUpdate(buffer) { + this.state.buffer = buffer; + } + get bufferRepr() { + return `"${this.state.buffer}"`; + } +} +DebugWidget.template = "DebugWidget"; - Registries.Component.add(DebugWidget); +Registries.Component.add(DebugWidget); - return DebugWidget; -}); +export default DebugWidget; 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 54ff960e695..b94b7589e19 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js @@ -1,36 +1,36 @@ -odoo.define('point_of_sale.HeaderButton', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); - const { ConnectionLostError, ConnectionAbortedError } = require('@web/core/network/rpc_service') - const { identifyError } = require('point_of_sale.utils'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; +import { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service"; +import { identifyError } from "@point_of_sale/js/utils"; - // Previously HeaderButtonWidget - // This is the close session button - class HeaderButton extends PosComponent { - async onClick() { - try { - const info = await this.env.pos.getClosePosInfo(); - this.showPopup('ClosePosPopup', { info: info, keepBehind: true }); - } catch (e) { - if (identifyError(e) instanceof ConnectionAbortedError||ConnectionLostError) { - this.showPopup('OfflineErrorPopup', { - title: this.env._t('Network Error'), - body: this.env._t('Please check your internet connection and try again.'), - }); - } else { - this.showPopup('ErrorPopup', { - title: this.env._t('Unknown Error'), - body: this.env._t('An unknown error prevents us from getting closing information.'), - }); - } +// Previously HeaderButtonWidget +// This is the close session button +class HeaderButton extends PosComponent { + async onClick() { + try { + const info = await this.env.pos.getClosePosInfo(); + this.showPopup("ClosePosPopup", { info: info, keepBehind: true }); + } catch (e) { + if (identifyError(e) instanceof ConnectionAbortedError || ConnectionLostError) { + this.showPopup("OfflineErrorPopup", { + title: this.env._t("Network Error"), + body: this.env._t("Please check your internet connection and try again."), + }); + } else { + this.showPopup("ErrorPopup", { + title: this.env._t("Unknown Error"), + body: this.env._t( + "An unknown error prevents us from getting closing information." + ), + }); } } } - HeaderButton.template = 'HeaderButton'; +} +HeaderButton.template = "HeaderButton"; - Registries.Component.add(HeaderButton); +Registries.Component.add(HeaderButton); - return 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 2dd9955be11..618749a59a8 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js @@ -1,83 +1,76 @@ -odoo.define('point_of_sale.ProxyStatus', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - const { onMounted, onWillUnmount, useState } = owl; +const { onMounted, onWillUnmount, useState } = owl; - // Previously ProxyStatusWidget - class ProxyStatus extends PosComponent { - setup() { - super.setup(); - const initialProxyStatus = this.env.proxy.get('status'); - this.state = useState({ - status: initialProxyStatus.status, - msg: initialProxyStatus.msg, - }); - this.statuses = ['connected', 'connecting', 'disconnected', 'warning']; - this.index = 0; +// Previously ProxyStatusWidget +class ProxyStatus extends PosComponent { + setup() { + super.setup(); + const initialProxyStatus = this.env.proxy.get("status"); + this.state = useState({ + status: initialProxyStatus.status, + msg: initialProxyStatus.msg, + }); + this.statuses = ["connected", "connecting", "disconnected", "warning"]; + this.index = 0; - onMounted(() => { - this.env.proxy.on('change:status', this, this._onChangeStatus); - }); + onMounted(() => { + this.env.proxy.on("change:status", this, this._onChangeStatus); + }); - onWillUnmount(() => { - this.env.proxy.off('change:status', this, this._onChangeStatus); - }); - } - _onChangeStatus(posProxy, statusChange) { - this._setStatus(statusChange.newValue); - } - _setStatus(newStatus) { - if (newStatus.status === 'connected') { - var warning = false; - var msg = ''; - if (this.env.pos.config.iface_scan_via_proxy) { - var scannerStatus = newStatus.drivers.scanner - ? newStatus.drivers.scanner.status - : false; - if (scannerStatus != 'connected' && scannerStatus != 'connecting') { - warning = true; - msg += this.env._t('Scanner'); - } + onWillUnmount(() => { + this.env.proxy.off("change:status", this, this._onChangeStatus); + }); + } + _onChangeStatus(posProxy, statusChange) { + this._setStatus(statusChange.newValue); + } + _setStatus(newStatus) { + if (newStatus.status === "connected") { + var warning = false; + var msg = ""; + if (this.env.pos.config.iface_scan_via_proxy) { + var scannerStatus = newStatus.drivers.scanner + ? newStatus.drivers.scanner.status + : false; + if (scannerStatus != "connected" && scannerStatus != "connecting") { + warning = true; + msg += this.env._t("Scanner"); } - if ( - this.env.pos.config.iface_print_via_proxy || - this.env.pos.config.iface_cashdrawer - ) { - var printerStatus = newStatus.drivers.printer - ? newStatus.drivers.printer.status - : false; - if (printerStatus != 'connected' && printerStatus != 'connecting') { - warning = true; - msg = msg ? msg + ' & ' : msg; - msg += this.env._t('Printer'); - } - } - if (this.env.pos.config.iface_electronic_scale) { - var scaleStatus = newStatus.drivers.scale - ? newStatus.drivers.scale.status - : false; - if (scaleStatus != 'connected' && scaleStatus != 'connecting') { - warning = true; - msg = msg ? msg + ' & ' : msg; - msg += this.env._t('Scale'); - } - } - msg = msg ? msg + ' ' + this.env._t('Offline') : msg; - - this.state.status = warning ? 'warning' : 'connected'; - this.state.msg = msg; - } else { - this.state.status = newStatus.status; - this.state.msg = newStatus.msg || ''; } + if (this.env.pos.config.iface_print_via_proxy || this.env.pos.config.iface_cashdrawer) { + var printerStatus = newStatus.drivers.printer + ? newStatus.drivers.printer.status + : false; + if (printerStatus != "connected" && printerStatus != "connecting") { + warning = true; + msg = msg ? msg + " & " : msg; + msg += this.env._t("Printer"); + } + } + if (this.env.pos.config.iface_electronic_scale) { + var scaleStatus = newStatus.drivers.scale ? newStatus.drivers.scale.status : false; + if (scaleStatus != "connected" && scaleStatus != "connecting") { + warning = true; + msg = msg ? msg + " & " : msg; + msg += this.env._t("Scale"); + } + } + msg = msg ? msg + " " + this.env._t("Offline") : msg; + + this.state.status = warning ? "warning" : "connected"; + this.state.msg = msg; + } else { + this.state.status = newStatus.status; + this.state.msg = newStatus.msg || ""; } } - ProxyStatus.template = 'ProxyStatus'; +} +ProxyStatus.template = "ProxyStatus"; - Registries.Component.add(ProxyStatus); +Registries.Component.add(ProxyStatus); - return 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 3e9690fab70..7f847461c28 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js @@ -1,39 +1,37 @@ -odoo.define('point_of_sale.SaleDetailsButton', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); - const { renderToString } = require('@web/core/utils/render'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; +import { renderToString } from "@web/core/utils/render"; - 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 - // component correctly triggers an event. - const saleDetails = await this.rpc({ - model: 'report.point_of_sale.report_saledetails', - method: 'get_sale_details', - args: [false, false, false, [this.env.pos.pos_session.id]], +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 + // component correctly triggers an event. + const saleDetails = await this.rpc({ + model: "report.point_of_sale.report_saledetails", + method: "get_sale_details", + args: [false, false, false, [this.env.pos.pos_session.id]], + }); + const report = renderToString( + "SaleDetailsReport", + Object.assign({}, saleDetails, { + date: new Date().toLocaleString(), + pos: this.env.pos, + }) + ); + const printResult = await this.env.proxy.printer.print_receipt(report); + if (!printResult.successful) { + await this.showPopup("ErrorPopup", { + title: printResult.message.title, + body: printResult.message.body, }); - const report = renderToString( - 'SaleDetailsReport', - Object.assign({}, saleDetails, { - date: new Date().toLocaleString(), - pos: this.env.pos, - }) - ); - const printResult = await this.env.proxy.printer.print_receipt(report); - if (!printResult.successful) { - await this.showPopup('ErrorPopup', { - title: printResult.message.title, - body: printResult.message.body, - }); - } } } - SaleDetailsButton.template = 'SaleDetailsButton'; +} +SaleDetailsButton.template = "SaleDetailsButton"; - Registries.Component.add(SaleDetailsButton); +Registries.Component.add(SaleDetailsButton); - return 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 1744b69230f..b22c1dc0557 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js @@ -1,17 +1,15 @@ -odoo.define('point_of_sale.SyncNotification', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class SyncNotification extends PosComponent { - onClick() { - this.env.pos.push_orders(null, { show_error: true }); - } +class SyncNotification extends PosComponent { + onClick() { + this.env.pos.push_orders(null, { show_error: true }); } - SyncNotification.template = 'SyncNotification'; +} +SyncNotification.template = "SyncNotification"; - Registries.Component.add(SyncNotification); +Registries.Component.add(SyncNotification); - return 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 3466536e020..de90148561c 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js @@ -1,28 +1,26 @@ -odoo.define('point_of_sale.TicketButton', function (require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class TicketButton extends PosComponent { - onClick() { - if (this.props.isTicketScreenShown) { - this.env.posbus.trigger('ticket-button-clicked'); - } else { - this.showScreen('TicketScreen'); - } - } - get count() { - if (this.env.pos) { - return this.env.pos.get_order_list().length; - } else { - return 0; - } +class TicketButton extends PosComponent { + onClick() { + if (this.props.isTicketScreenShown) { + this.env.posbus.trigger("ticket-button-clicked"); + } else { + this.showScreen("TicketScreen"); } } - TicketButton.template = 'TicketButton'; + get count() { + if (this.env.pos) { + return this.env.pos.get_order_list().length; + } else { + return 0; + } + } +} +TicketButton.template = "TicketButton"; - Registries.Component.add(TicketButton); +Registries.Component.add(TicketButton); - return 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 index eed07fe31b1..24c66e25f54 100644 --- a/addons/point_of_sale/static/src/js/ClassRegistry.js +++ b/addons/point_of_sale/static/src/js/ClassRegistry.js @@ -1,262 +1,260 @@ -odoo.define('point_of_sale.ClassRegistry', function (require) { - 'use strict'; +/** @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 (let [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 (let [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 (let [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 (let [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); - } +/** + * **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); + } - return ClassRegistry; -}); + // 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 index 1e820782e15..1547aa930e0 100644 --- a/addons/point_of_sale/static/src/js/ComponentRegistry.js +++ b/addons/point_of_sale/static/src/js/ComponentRegistry.js @@ -1,29 +1,27 @@ -odoo.define('point_of_sale.ComponentRegistry', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const ClassRegistry = require('point_of_sale.ClassRegistry'); +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 (let [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; +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; + } +} - return ComponentRegistry; -}); +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 4d221b6dba1..1ed763ce552 100644 --- a/addons/point_of_sale/static/src/js/ControlButtonsMixin.js +++ b/addons/point_of_sale/static/src/js/ControlButtonsMixin.js @@ -1,107 +1,107 @@ -odoo.define('point_of_sale.ControlButtonsMixin', function (require) { - 'use strict'; +/** @odoo-module */ - const Registries = require('point_of_sale.Registries'); +import Registries from "@point_of_sale/js/Registries"; +/** + * Component that has this mixin allows the use of `addControlButton`. + * All added control buttons that satisfies the condition can be accessed + * thru the `controlButtons` field of the Component's instance. These + * control buttons can then be rendered in the Component. + * @param {Function} x superclass + */ +const ControlButtonsMixin = (x) => { + const controlButtonsToPosition = []; + const sortedControlButtons = []; + + class Extended extends x { + get controlButtons() { + return sortedControlButtons + .filter((cb) => { + return cb.condition ? cb.condition.bind(this)() : true; + }) + .map((cb) => + Object.assign({}, cb, { component: Registries.Component.get(cb.component) }) + ); + } + } /** - * Component that has this mixin allows the use of `addControlButton`. - * All added control buttons that satisfies the condition can be accessed - * thru the `controlButtons` field of the Component's instance. These - * control buttons can then be rendered in the Component. - * @param {Function} x superclass + * @param {Object} controlButton + * @param {Function} controlButton.component + * Base class that is added in the Registries.Component. + * @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. + * @param {Array} [controlButton.position] array of two elements + * [locator, relativeTo] + * locator: string -> any of ('before', 'after', 'replace') + * relativeTo: string -> other controlButtons component name */ - const ControlButtonsMixin = (x) => { - const controlButtonsToPosition = []; - const sortedControlButtons = []; - - class Extended extends x { - get controlButtons() { - return sortedControlButtons - .filter((cb) => { - return cb.condition ? cb.condition.bind(this)() : true; - }) - .map((cb) => - Object.assign({}, cb, { component: Registries.Component.get(cb.component) }) - ); - } + Extended.addControlButton = function (controlButton) { + // We set the name first. + if (!controlButton.name) { + controlButton.name = controlButton.component.name; } - /** - * @param {Object} controlButton - * @param {Function} controlButton.component - * Base class that is added in the Registries.Component. - * @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. - * @param {Array} [controlButton.position] array of two elements - * [locator, relativeTo] - * locator: string -> any of ('before', 'after', 'replace') - * relativeTo: string -> other controlButtons component name - */ - Extended.addControlButton = function (controlButton) { - // We set the name first. - if (!controlButton.name) { - controlButton.name = controlButton.component.name; - } - // If no position is set, we just push it to the array. - if (!controlButton.position) { - sortedControlButtons.push(controlButton); - } else { - controlButtonsToPosition.push(controlButton); - } - }; - - /** - * Call this static method to make the added control buttons in proper - * order. - * NOTE: This isn't necessarily a fast algorithm. I doubt that the number - * of control buttons will exceed an order of hundreds, so for practical - * purposes, it is enough. - */ - Extended.sortControlButtons = function () { - function setControlButton(locator, index, cb) { - if (locator == 'replace') { - sortedControlButtons[index] = cb; - } else if (locator == 'before') { - sortedControlButtons.splice(index, 0, cb); - } else if (locator == 'after') { - sortedControlButtons.splice(index + 1, 0, cb); - } - } - function locate(cb) { - const [locator, reference] = cb.position; - const index = sortedControlButtons.findIndex((cb) => cb.name == reference); - return [locator, index, reference]; - } - const cbMissingReference = []; - // 1. First pass. If the reference control button isn't there, collect it for second pass. - for (let cb of controlButtonsToPosition) { - const [locator, index] = locate(cb); - if (index == -1) { - cbMissingReference.push(cb); - continue; - } - setControlButton(locator, index, cb); - } - // 2. Second pass. - // If during the first pass, 1 -> 2, 2 -> 3, 3 -> 4, 4 -> 5 and 5 is already - // in the sorted control buttons, then 1, 2, 3 & 4 are put in `cbMissingReference`. - // This only means 2 things about the objects in `cbMissingReference`: - // i) They are referencing the cb after them - // ii) They really have missing reference. - // Thus, we have to iterate the cb with missing reference in reverse. - for (let cb of cbMissingReference.reverse()) { - const [locator, index, reference] = locate(cb); - if (index == -1) { - console.warn(`'${cb.name}' is not properly position because '${reference}' is not found. Is '${reference}' spelled correctly?`); - sortedControlButtons.push(cb); - } else { - setControlButton(locator, index, cb); - } - } + // If no position is set, we just push it to the array. + if (!controlButton.position) { + sortedControlButtons.push(controlButton); + } else { + controlButtonsToPosition.push(controlButton); } - return Extended; }; - return ControlButtonsMixin; -}); + /** + * Call this static method to make the added control buttons in proper + * order. + * NOTE: This isn't necessarily a fast algorithm. I doubt that the number + * of control buttons will exceed an order of hundreds, so for practical + * purposes, it is enough. + */ + Extended.sortControlButtons = function () { + function setControlButton(locator, index, cb) { + if (locator == "replace") { + sortedControlButtons[index] = cb; + } else if (locator == "before") { + sortedControlButtons.splice(index, 0, cb); + } else if (locator == "after") { + sortedControlButtons.splice(index + 1, 0, cb); + } + } + function locate(cb) { + const [locator, reference] = cb.position; + const index = sortedControlButtons.findIndex((cb) => cb.name == reference); + return [locator, index, reference]; + } + const cbMissingReference = []; + // 1. First pass. If the reference control button isn't there, collect it for second pass. + for (const cb of controlButtonsToPosition) { + const [locator, index] = locate(cb); + if (index == -1) { + cbMissingReference.push(cb); + continue; + } + setControlButton(locator, index, cb); + } + // 2. Second pass. + // If during the first pass, 1 -> 2, 2 -> 3, 3 -> 4, 4 -> 5 and 5 is already + // in the sorted control buttons, then 1, 2, 3 & 4 are put in `cbMissingReference`. + // This only means 2 things about the objects in `cbMissingReference`: + // i) They are referencing the cb after them + // ii) They really have missing reference. + // Thus, we have to iterate the cb with missing reference in reverse. + for (const cb of cbMissingReference.reverse()) { + const [locator, index, reference] = locate(cb); + if (index == -1) { + console.warn( + `'${cb.name}' is not properly position because '${reference}' is not found. Is '${reference}' spelled correctly?` + ); + sortedControlButtons.push(cb); + } else { + setControlButton(locator, index, cb); + } + } + }; + 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 6be8f41014a..f6d3c6806a4 100644 --- a/addons/point_of_sale/static/src/js/Gui.js +++ b/addons/point_of_sale/static/src/js/Gui.js @@ -1,64 +1,61 @@ -odoo.define('point_of_sale.Gui', function (require) { - 'use strict'; +/** @odoo-module */ +const { status } = owl; - const { status } = owl; +/** + * This module bridges the data classes (such as those defined in + * models.js) to the view (Component) but not vice versa. + * + * The idea is to be able to perform side-effects to the user interface + * during calculation. Think of console.log during times we want to see + * the result of calculations. This is no different, except that instead + * of printing something in the console, we access a method in the user + * interface then the user interface reacts, e.g. calling `showPopup`. + * + * This however can be dangerous to the user interface as it can be possible + * that a rendered component is destroyed during the calculation. Because of + * this, we are going to limit external ui controls to those safe ones to + * use such as: + * - `showPopup` + * - `showTempScreen` + * + * IMPROVEMENT: After all, this Gui layer seems to be a good abstraction because + * there is a complete decoupling between data and view despite the data being + * able to use selected functionalities in the view layer. More formalized + * implementation is welcome. + */ - /** - * This module bridges the data classes (such as those defined in - * models.js) to the view (Component) but not vice versa. - * - * The idea is to be able to perform side-effects to the user interface - * during calculation. Think of console.log during times we want to see - * the result of calculations. This is no different, except that instead - * of printing something in the console, we access a method in the user - * interface then the user interface reacts, e.g. calling `showPopup`. - * - * This however can be dangerous to the user interface as it can be possible - * that a rendered component is destroyed during the calculation. Because of - * this, we are going to limit external ui controls to those safe ones to - * use such as: - * - `showPopup` - * - `showTempScreen` - * - * IMPROVEMENT: After all, this Gui layer seems to be a good abstraction because - * there is a complete decoupling between data and view despite the data being - * able to use selected functionalities in the view layer. More formalized - * implementation is welcome. - */ +const config = {}; - const config = {}; +/** + * Call this when the user interface is ready. Provide the component + * that will be used to control the ui. + * @param {component} component component having the ui methods. + */ +export const configureGui = ({ component }) => { + config.component = component; + config.availableMethods = new Set([ + "showScreen", + "showPopup", + "showTempScreen", + "playSound", + "setSyncStatus", + "showNotification", + ]); +}; - /** - * Call this when the user interface is ready. Provide the component - * that will be used to control the ui. - * @param {component} component component having the ui methods. - */ - const configureGui = ({ component }) => { - config.component = component; - config.availableMethods = new Set([ - 'showScreen', - 'showPopup', - 'showTempScreen', - 'playSound', - 'setSyncStatus', - 'showNotification', - ]); - }; - - /** - * Import this and consume like so: `Gui.showPopup(, )`. - * Like you would call `showPopup` in a component. - */ - const Gui = new Proxy(config, { - get(target, key) { - const { component, availableMethods } = target; - if (!component) throw new Error(`Call 'configureGui' before using Gui.`); - const isMounted = status(component) === 'mounted'; - if (availableMethods.has(key) && isMounted) { - return component[key].bind(component); - } - }, - }); - - return { configureGui, Gui }; +/** + * Import this and consume like so: `Gui.showPopup(, )`. + * Like you would call `showPopup` in a component. + */ +export const Gui = new Proxy(config, { + get(target, key) { + const { component, availableMethods } = target; + if (!component) { + throw new Error(`Call 'configureGui' before using Gui.`); + } + const isMounted = status(component) === "mounted"; + if (availableMethods.has(key) && isMounted) { + return component[key].bind(component); + } + }, }); 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 f88cff74d71..8355c7b449b 100644 --- a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js @@ -1,63 +1,63 @@ -odoo.define('point_of_sale.AbstractReceiptScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const { nextFrame } = require('point_of_sale.utils'); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import { nextFrame } from "@point_of_sale/js/utils"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - const { useRef } = owl; +const { useRef } = owl; - /** - * This relies on the assumption that there is a reference to - * `order-receipt` so it is important to declare a `t-ref` to - * `order-receipt` in the template of the Component that extends - * this abstract component. - */ - class AbstractReceiptScreen extends PosComponent { - setup() { - super.setup(); - this.orderReceipt = useRef('order-receipt'); - } - async _printReceipt() { - if (this.env.proxy.printer) { - const printResult = await this.env.proxy.printer.print_receipt(this.orderReceipt.el.innerHTML); - if (printResult.successful) { - return true; - } else { - const { confirmed } = await this.showPopup('ConfirmPopup', { - title: printResult.message.title, - body: 'Do you want to print using the web printer?', - }); - if (confirmed) { - // We want to call the _printWeb when the popup is fully gone - // from the screen which happens after the next animation frame. - await nextFrame(); - return await this._printWeb(); - } - return false; - } - } else { - return await this._printWeb(); - } - } - async _printWeb() { - try { - window.print(); +/** + * This relies on the assumption that there is a reference to + * `order-receipt` so it is important to declare a `t-ref` to + * `order-receipt` in the template of the Component that extends + * this abstract component. + */ +class AbstractReceiptScreen extends PosComponent { + setup() { + super.setup(); + this.orderReceipt = useRef("order-receipt"); + } + async _printReceipt() { + if (this.env.proxy.printer) { + const printResult = await this.env.proxy.printer.print_receipt( + this.orderReceipt.el.innerHTML + ); + if (printResult.successful) { return true; - } catch { - 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 ' + - 'is available. It is possible to print your tickets by making use of an IoT Box.' - ), + } else { + const { confirmed } = await this.showPopup("ConfirmPopup", { + title: printResult.message.title, + body: "Do you want to print using the web printer?", }); + if (confirmed) { + // We want to call the _printWeb when the popup is fully gone + // from the screen which happens after the next animation frame. + await nextFrame(); + return await this._printWeb(); + } return false; } + } else { + return await this._printWeb(); } } + async _printWeb() { + try { + window.print(); + return true; + } catch { + 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 " + + "is available. It is possible to print your tickets by making use of an IoT Box." + ), + }); + return false; + } + } +} - Registries.Component.add(AbstractReceiptScreen); +Registries.Component.add(AbstractReceiptScreen); - return 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 e7ced094b69..82481f61240 100644 --- a/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js +++ b/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js @@ -1,13 +1,11 @@ -odoo.define('point_of_sale.CurrencyAmount', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class CurrencyAmount extends PosComponent {} - CurrencyAmount.template = 'CurrencyAmount'; +class CurrencyAmount extends PosComponent {} +CurrencyAmount.template = "CurrencyAmount"; - Registries.Component.add(CurrencyAmount); +Registries.Component.add(CurrencyAmount); - return CurrencyAmount; -}); +export default 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 751bbbee52f..82f4ef5fce8 100644 --- a/addons/point_of_sale/static/src/js/Misc/Draggable.js +++ b/addons/point_of_sale/static/src/js/Misc/Draggable.js @@ -1,145 +1,145 @@ -odoo.define('point_of_sale.Draggable', function(require) { - 'use strict'; +/** @odoo-module */ - const { useListener } = require("@web/core/utils/hooks"); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import { useListener } from "@web/core/utils/hooks"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - const { onMounted, useExternalListener } = owl; +const { onMounted, useExternalListener } = owl; - /** - * Wrap an element or a component with { position: absolute } to make it - * draggable around the limitArea or the nearest positioned ancestor. - * - * e.g. - * ``` - *
- * - * - * - * - *
- * ``` - * - * In the above snippet, if the popup div is { position: absolute }, - * then it becomes draggable around the .limit-area element if it is dragged - * thru its Header -- because of the .drag-handle element. - * - * @trigger 'drag-end' when dragging ended with payload `{ loc: { top, left } }` - */ - class Draggable extends PosComponent { - setup() { - super.setup(); - this.isDragging = false; - this.dx = 0; - this.dy = 0; - // drag with mouse - useExternalListener(document, 'mousemove', this.move); - useExternalListener(document, 'mouseup', this.endDrag); - // drag with touch - useExternalListener(document, 'touchmove', this.move); - useExternalListener(document, 'touchend', this.endDrag); +/** + * Wrap an element or a component with { position: absolute } to make it + * draggable around the limitArea or the nearest positioned ancestor. + * + * e.g. + * ``` + *
+ * + * + * + * + *
+ * ``` + * + * In the above snippet, if the popup div is { position: absolute }, + * then it becomes draggable around the .limit-area element if it is dragged + * thru its Header -- because of the .drag-handle element. + * + * @trigger 'drag-end' when dragging ended with payload `{ loc: { top, left } }` + */ +class Draggable extends PosComponent { + setup() { + super.setup(); + this.isDragging = false; + this.dx = 0; + this.dy = 0; + // drag with mouse + useExternalListener(document, "mousemove", this.move); + useExternalListener(document, "mouseup", this.endDrag); + // drag with touch + useExternalListener(document, "touchmove", this.move); + useExternalListener(document, "touchend", this.endDrag); - useListener('mousedown', '.drag-handle', this.startDrag); - useListener('touchstart', '.drag-handle', this.startDrag); + useListener("mousedown", ".drag-handle", this.startDrag); + useListener("touchstart", ".drag-handle", this.startDrag); - onMounted(() => { - this.limitArea = this.props.limitArea - ? document.querySelector(this.props.limitArea) - : this.el.offsetParent; - if (!this.limitArea) return; - this.limitAreaBoundingRect = this.limitArea.getBoundingClientRect(); - if (this.limitArea === this.el.offsetParent) { - this.limitLeft = 0; - this.limitTop = 0; - this.limitRight = this.limitAreaBoundingRect.width; - this.limitBottom = this.limitAreaBoundingRect.height; - } else { - this.limitLeft = -this.el.offsetParent.offsetLeft; - this.limitTop = -this.el.offsetParent.offsetTop; - this.limitRight = - this.limitAreaBoundingRect.width - this.el.offsetParent.offsetLeft; - this.limitBottom = - this.limitAreaBoundingRect.height - this.el.offsetParent.offsetTop; - } - this.limitAreaWidth = this.limitAreaBoundingRect.width; - this.limitAreaHeight = this.limitAreaBoundingRect.height; - - // absolutely position the element then remove the transform. - const elBoundingRect = this.el.getBoundingClientRect(); - this.el.style.top = `${elBoundingRect.top}px`; - this.el.style.left = `${elBoundingRect.left}px`; - this.el.style.transform = 'none'; - }); - } - startDrag(event) { - let realEvent; - if (event instanceof CustomEvent) { - realEvent = event.detail; + onMounted(() => { + this.limitArea = this.props.limitArea + ? document.querySelector(this.props.limitArea) + : this.el.offsetParent; + if (!this.limitArea) { + return; + } + this.limitAreaBoundingRect = this.limitArea.getBoundingClientRect(); + if (this.limitArea === this.el.offsetParent) { + this.limitLeft = 0; + this.limitTop = 0; + this.limitRight = this.limitAreaBoundingRect.width; + this.limitBottom = this.limitAreaBoundingRect.height; } else { - realEvent = event; + this.limitLeft = -this.el.offsetParent.offsetLeft; + this.limitTop = -this.el.offsetParent.offsetTop; + this.limitRight = + this.limitAreaBoundingRect.width - this.el.offsetParent.offsetLeft; + this.limitBottom = + this.limitAreaBoundingRect.height - this.el.offsetParent.offsetTop; } - const { x, y } = this._getEventLoc(realEvent); - this.isDragging = true; - this.dx = this.el.offsetLeft - x; - this.dy = this.el.offsetTop - y; - event.stopPropagation(); + this.limitAreaWidth = this.limitAreaBoundingRect.width; + this.limitAreaHeight = this.limitAreaBoundingRect.height; + + // absolutely position the element then remove the transform. + const elBoundingRect = this.el.getBoundingClientRect(); + this.el.style.top = `${elBoundingRect.top}px`; + this.el.style.left = `${elBoundingRect.left}px`; + this.el.style.transform = "none"; + }); + } + startDrag(event) { + let realEvent; + if (event instanceof CustomEvent) { + realEvent = event.detail; + } else { + realEvent = event; } - move(event) { - if (this.isDragging) { - const { x: pointerX, y: pointerY } = this._getEventLoc(event); - const posLeft = this._getPosLeft(pointerX, this.dx); - const posTop = this._getPosTop(pointerY, this.dy); - this.el.style.left = `${posLeft}px`; - this.el.style.top = `${posTop}px`; - } - } - endDrag() { - if (this.isDragging) { - this.isDragging = false; - this.trigger('drag-end', { - loc: { top: this.el.offsetTop, left: this.el.offsetLeft }, - }); - } - } - _getEventLoc(event) { - let coordX, coordY; - if (event.touches && event.touches[0]) { - coordX = event.touches[0].clientX; - coordY = event.touches[0].clientY; - } else { - coordX = event.clientX; - coordY = event.clientY; - } - return { - x: coordX, - y: coordY, - }; - } - _getPosLeft(pointerX, dx) { - const posLeft = pointerX + dx; - if (posLeft < this.limitLeft) { - return this.limitLeft; - } else if (posLeft > this.limitRight - this.el.offsetWidth) { - return this.limitRight - this.el.offsetWidth; - } - return posLeft; - } - _getPosTop(pointerY, dy) { - const posTop = pointerY + dy; - if (posTop < this.limitTop) { - return this.limitTop; - } else if (posTop > this.limitBottom - this.el.offsetHeight) { - return this.limitBottom - this.el.offsetHeight; - } - return posTop; + const { x, y } = this._getEventLoc(realEvent); + this.isDragging = true; + this.dx = this.el.offsetLeft - x; + this.dy = this.el.offsetTop - y; + event.stopPropagation(); + } + move(event) { + if (this.isDragging) { + const { x: pointerX, y: pointerY } = this._getEventLoc(event); + const posLeft = this._getPosLeft(pointerX, this.dx); + const posTop = this._getPosTop(pointerY, this.dy); + this.el.style.left = `${posLeft}px`; + this.el.style.top = `${posTop}px`; } } - Draggable.template = 'Draggable'; + endDrag() { + if (this.isDragging) { + this.isDragging = false; + this.trigger("drag-end", { + loc: { top: this.el.offsetTop, left: this.el.offsetLeft }, + }); + } + } + _getEventLoc(event) { + let coordX, coordY; + if (event.touches && event.touches[0]) { + coordX = event.touches[0].clientX; + coordY = event.touches[0].clientY; + } else { + coordX = event.clientX; + coordY = event.clientY; + } + return { + x: coordX, + y: coordY, + }; + } + _getPosLeft(pointerX, dx) { + const posLeft = pointerX + dx; + if (posLeft < this.limitLeft) { + return this.limitLeft; + } else if (posLeft > this.limitRight - this.el.offsetWidth) { + return this.limitRight - this.el.offsetWidth; + } + return posLeft; + } + _getPosTop(pointerY, dy) { + const posTop = pointerY + dy; + if (posTop < this.limitTop) { + return this.limitTop; + } else if (posTop > this.limitBottom - this.el.offsetHeight) { + return this.limitBottom - this.el.offsetHeight; + } + return posTop; + } +} +Draggable.template = "Draggable"; - Registries.Component.add(Draggable); +Registries.Component.add(Draggable); - return 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 81640e2f5aa..c46d93f7294 100644 --- a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js +++ b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js @@ -1,15 +1,13 @@ -odoo.define('point_of_sale.IndependentToOrderScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); +import PosComponent from "@point_of_sale/js/PosComponent"; - class IndependentToOrderScreen extends PosComponent { - close() { - const order = this.env.pos.get_order(); - const { name: screenName } = order.get_screen_data(); - this.showScreen(screenName); - } +class IndependentToOrderScreen extends PosComponent { + close() { + const order = this.env.pos.get_order(); + const { name: screenName } = order.get_screen_data(); + this.showScreen(screenName); } +} - return IndependentToOrderScreen; -}); +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 ffc3424e564..ddbb2817fb0 100644 --- a/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js +++ b/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js @@ -1,25 +1,25 @@ -odoo.define('point_of_sale.MobileOrderWidget', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class MobileOrderWidget extends PosComponent { - get order() { - return this.env.pos.get_order(); - } - get total() { - const _total = this.order ? this.order.get_total_with_tax() : 0; - return this.env.pos.format_currency(_total); - } - get items_number() { - return this.order ? this.order.orderlines.reduce((items_number,line) => items_number + line.quantity, 0) : 0; - } +class MobileOrderWidget extends PosComponent { + get order() { + return this.env.pos.get_order(); } + get total() { + const _total = this.order ? this.order.get_total_with_tax() : 0; + return this.env.pos.format_currency(_total); + } + get items_number() { + return this.order + ? this.order.orderlines.reduce((items_number, line) => items_number + line.quantity, 0) + : 0; + } +} - MobileOrderWidget.template = 'MobileOrderWidget'; +MobileOrderWidget.template = "MobileOrderWidget"; - Registries.Component.add(MobileOrderWidget); +Registries.Component.add(MobileOrderWidget); - return MobileOrderWidget; -}); +export default MobileOrderWidget; diff --git a/addons/point_of_sale/static/src/js/Misc/NotificationSound.js b/addons/point_of_sale/static/src/js/Misc/NotificationSound.js index e783225e38b..3132dc2b468 100644 --- a/addons/point_of_sale/static/src/js/Misc/NotificationSound.js +++ b/addons/point_of_sale/static/src/js/Misc/NotificationSound.js @@ -1,19 +1,17 @@ -odoo.define('point_of_sale.NotificationSound', function (require) { - 'use strict'; +/** @odoo-module */ - const { useListener } = require("@web/core/utils/hooks"); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import { useListener } from "@web/core/utils/hooks"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class NotificationSound extends PosComponent { - setup() { - super.setup(); - useListener('ended', () => (this.props.sound.src = null)); - } +class NotificationSound extends PosComponent { + setup() { + super.setup(); + useListener("ended", () => (this.props.sound.src = null)); } - NotificationSound.template = 'NotificationSound'; +} +NotificationSound.template = "NotificationSound"; - Registries.Component.add(NotificationSound); +Registries.Component.add(NotificationSound); - return NotificationSound; -}); +export default NotificationSound; 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 c1a87844a44..ff3e5736ae6 100644 --- a/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js +++ b/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js @@ -1,311 +1,320 @@ -odoo.define('point_of_sale.NumberBuffer', function(require) { - 'use strict'; +/** @odoo-module */ - const { useListener } = require("@web/core/utils/hooks"); - const { parse } = require('web.field_utils'); - const { barcodeService } = require('@barcodes/barcode_service'); - const { _t } = require('web.core'); - const { Gui } = require('point_of_sale.Gui'); +import { useListener } from "@web/core/utils/hooks"; +import { parse } from "web.field_utils"; +import { barcodeService } from "@barcodes/barcode_service"; +import { _t } from "web.core"; +import { Gui } from "@point_of_sale/js/Gui"; - const { EventBus, onMounted, onWillUnmount, useComponent, useExternalListener } = owl; - const INPUT_KEYS = new Set( - ['Delete', 'Backspace', '+1', '+2', '+5', '+10', '+20', '+50'].concat('0123456789+-.,'.split('')) - ); - const CONTROL_KEYS = new Set(['Enter', 'Esc']); - const ALLOWED_KEYS = new Set([...INPUT_KEYS, ...CONTROL_KEYS]); - const getDefaultConfig = () => ({ - decimalPoint: false, - triggerAtEnter: false, - triggerAtEsc: false, - triggerAtInput: false, - nonKeyboardInputEvent: false, - useWithBarcode: false, - }); +const { EventBus, onMounted, onWillUnmount, useComponent, useExternalListener } = owl; +const INPUT_KEYS = new Set( + ["Delete", "Backspace", "+1", "+2", "+5", "+10", "+20", "+50"].concat( + "0123456789+-.,".split("") + ) +); +const CONTROL_KEYS = new Set(["Enter", "Esc"]); +const ALLOWED_KEYS = new Set([...INPUT_KEYS, ...CONTROL_KEYS]); +const getDefaultConfig = () => ({ + decimalPoint: false, + triggerAtEnter: false, + triggerAtEsc: false, + triggerAtInput: false, + nonKeyboardInputEvent: false, + useWithBarcode: false, +}); +/** + * This is a singleton. + * + * Only one component can `use` the buffer at a time. + * This is done by keeping track of each component (and its + * corresponding state and config) using a stack (bufferHolderStack). + * The component on top of the stack is the one that currently + * `holds` the buffer. + * + * When the current component is unmounted, the top of the stack + * is popped and NumberBuffer is set up again for the new component + * on top of the stack. + * + * Usage + * ===== + * - Activate in the construction of root component. `NumberBuffer.activate()` + * - Use the buffer in a child component by calling `NumberBuffer.use()` + * in the constructor of the child component. + * - The component that `uses` the buffer has access to the following instance + * methods of the NumberBuffer: + * - get() + * - set(val) + * - reset() + * - getFloat() + * - capture() + * + * Note + * ==== + * - No need to instantiate as it is a singleton created before exporting in this module. + * + * Possible Improvements + * ===================== + * - Relieve the buffer from responsibility of handling `Enter` and other control keys. + * - Make the constants (ALLOWED_KEYS, etc.) more configurable. + * - Write more integration tests. NumberPopup can be used as test component. + */ +class NumberBuffer extends EventBus { + constructor() { + super(); + this.isReset = false; + this.bufferHolderStack = []; + } /** - * This is a singleton. - * - * Only one component can `use` the buffer at a time. - * This is done by keeping track of each component (and its - * corresponding state and config) using a stack (bufferHolderStack). - * The component on top of the stack is the one that currently - * `holds` the buffer. - * - * When the current component is unmounted, the top of the stack - * is popped and NumberBuffer is set up again for the new component - * on top of the stack. - * - * Usage - * ===== - * - Activate in the construction of root component. `NumberBuffer.activate()` - * - Use the buffer in a child component by calling `NumberBuffer.use()` - * in the constructor of the child component. - * - The component that `uses` the buffer has access to the following instance - * methods of the NumberBuffer: - * - get() - * - set(val) - * - reset() - * - getFloat() - * - capture() - * - * Note - * ==== - * - No need to instantiate as it is a singleton created before exporting in this module. - * - * Possible Improvements - * ===================== - * - Relieve the buffer from responsibility of handling `Enter` and other control keys. - * - Make the constants (ALLOWED_KEYS, etc.) more configurable. - * - Write more integration tests. NumberPopup can be used as test component. + * @returns {String} value of the buffer, e.g. '-95.79' */ - class NumberBuffer extends EventBus { - constructor() { - super(); - this.isReset = false; - this.bufferHolderStack = []; - } - /** - * @returns {String} value of the buffer, e.g. '-95.79' - */ - get() { - return this.state ? this.state.buffer : null; - } - /** - * Takes a string that is convertible to float, and set it as - * value of the buffer. e.g. val = '2.99'; - * - * @param {String} val - */ - set(val) { - this.state.buffer = !isNaN(parseFloat(val)) ? val : ''; - this.trigger('buffer-update', this.state.buffer); - } - /** - * Resets the buffer to empty string. - */ - reset() { - this.isReset = true; - this.state.buffer = ''; - this.trigger('buffer-update', this.state.buffer); - } - /** - * Calling this function, we immediately invoke the `handler` method - * that handles the contents of the input events buffer (`eventsBuffer`). - * This is helpful when we don't want to wait for the timeout that - * is supposed to invoke the handler. - */ - capture() { - if (this.handler) { - clearTimeout(this._timeout); - this.handler(); - delete this.handler; - } - } - /** - * @returns {number} float equivalent of the value of buffer - */ - getFloat() { - return parse.float(this.get()); - } - /** - * Add keyup listener to window via the useExternalListener hook. - * When the component calling this is unmounted, the listener is also - * removed from window. - */ - activate() { - this.defaultDecimalPoint = _t.database.parameters.decimal_point; - useExternalListener(window, 'keyup', this._onKeyboardInput.bind(this)); - } - /** - * @param {Object} config Use to setup the buffer - * @param {String|null} config.decimalPoint The decimal character. - * @param {String|null} config.triggerAtEnter Event triggered when 'Enter' key is pressed. - * @param {String|null} config.triggerAtEsc Event triggered when 'Esc' key is pressed. - * @param {String|null} config.triggerAtInput Event triggered for every accepted input. - * @param {String|null} config.nonKeyboardInputEvent Also listen to a non-keyboard input event - * that carries a payload of { key }. The key is checked if it is a valid input. If valid, - * the number buffer is modified just as it is modified when a keyboard key is pressed. - * @param {Boolean} config.useWithBarcode Whether this buffer is used with barcode. - * @emits config.triggerAtEnter when 'Enter' key is pressed. - * @emits config.triggerAtEsc when 'Esc' key is pressed. - * @emits config.triggerAtInput when an input is accepted. - */ - use(config) { - this.eventsBuffer = []; - const currentComponent = useComponent(); - config = Object.assign(getDefaultConfig(), config); - onMounted(() => { - this.bufferHolderStack.push({ - component: currentComponent, - state: config.state ? config.state : { buffer: '', toStartOver: false }, - config, - }); - this._setUp(); - }); - onWillUnmount(() => { - this.bufferHolderStack.pop(); - this._setUp(); - }); - // Add listener that accepts non keyboard inputs - if (typeof config.nonKeyboardInputEvent === 'string') { - useListener(config.nonKeyboardInputEvent, this._onNonKeyboardInput.bind(this)); - } - } - get _currentBufferHolder() { - return this.bufferHolderStack[this.bufferHolderStack.length - 1]; - } - _setUp() { - if (!this._currentBufferHolder) return; - const { component, state, config } = this._currentBufferHolder; - this.component = component; - this.state = state; - this.config = config; - this.decimalPoint = config.decimalPoint || this.defaultDecimalPoint; - this.maxTimeBetweenKeys = this.config.useWithBarcode - ? barcodeService.maxTimeBetweenKeysInMs - : 0; - } - _onKeyboardInput(event) { - return this._bufferEvents(this._onInput(event => event.key))(event); - } - _onNonKeyboardInput(event) { - return this._bufferEvents(this._onInput(event => event.detail.key))(event); - } - _bufferEvents(handler) { - return event => { - if (['INPUT', 'TEXTAREA'].includes(event.target.tagName) || !this.eventsBuffer) return; - clearTimeout(this._timeout); - this.eventsBuffer.push(event); - this._timeout = setTimeout(handler, this.maxTimeBetweenKeys); - this.handler = handler - }; - } - _onInput(keyAccessor) { - return () => { - if (this.eventsBuffer.length <= 2) { - // Check first the buffer if its contents are all valid - // number input. - for (let event of this.eventsBuffer) { - if (!ALLOWED_KEYS.has(keyAccessor(event))) { - this.eventsBuffer = []; - return; - } - } - // At this point, all the events in buffer - // contains number input. It's now okay to handle - // each input. - for (let event of this.eventsBuffer) { - this._handleInput(keyAccessor(event)); - event.preventDefault(); - event.stopPropagation(); - } - } - this.eventsBuffer = []; - }; - } - _handleInput(key) { - if (key === 'Enter' && this.config.triggerAtEnter) { - this.component.trigger(this.config.triggerAtEnter, this.state); - } else if (key === 'Esc' && this.config.triggerAtEsc) { - this.component.trigger(this.config.triggerAtEsc, this.state); - } else if (INPUT_KEYS.has(key)) { - this._updateBuffer(key); - if (this.config.triggerAtInput) - this.component.trigger(this.config.triggerAtInput, { buffer: this.state.buffer, key }); - } - } - /** - * Updates the current buffer state using the given input. - * @param {String} input valid input - */ - _updateBuffer(input) { - const isEmpty = val => { - return val === '' || val === null; - }; - if (input === undefined || input === null) return; - let isFirstInput = isEmpty(this.state.buffer); - if (input === ',' || input === '.') { - if (this.state.toStartOver) { - this.state.buffer = ''; - } - if (isFirstInput) { - this.state.buffer = '0' + this.decimalPoint; - } else if (!this.state.buffer.length || this.state.buffer === '-') { - this.state.buffer += '0' + this.decimalPoint; - } else if (this.state.buffer.indexOf(this.decimalPoint) < 0) { - this.state.buffer = this.state.buffer + this.decimalPoint; - } - } else if (input === 'Delete') { - if (this.isReset) { - this.state.buffer = ''; - this.isReset = false; - return; - } - this.state.buffer = isEmpty(this.state.buffer) ? null : ''; - } else if (input === 'Backspace') { - if (this.isReset) { - this.state.buffer = ''; - this.isReset = false; - return; - } - if (this.state.toStartOver) { - this.state.buffer = ''; - } - const buffer = this.state.buffer; - if (isEmpty(buffer)) { - this.state.buffer = null; - } else { - const nCharToRemove = buffer[buffer.length - 1] === this.decimalPoint ? 2 : 1; - this.state.buffer = buffer.substring(0, buffer.length - nCharToRemove); - } - } else if (input === '+') { - if (this.state.buffer[0] === '-') { - this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length); - } - } else if (input === '-') { - if (isFirstInput) { - this.state.buffer = '-0'; - } else if (this.state.buffer[0] === '-') { - this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length); - } else { - this.state.buffer = '-' + this.state.buffer; - } - } else if (input[0] === '+' && !isNaN(parseFloat(input))) { - // when input is like '+10', '+50', etc - const inputValue = parse.float(input.slice(1)); - const currentBufferValue = this.state.buffer ? parse.float(this.state.buffer) : 0; - this.state.buffer = this.component.env.pos.formatFixed( - inputValue + currentBufferValue - ); - } else if (!isNaN(parseInt(input, 10))) { - if (this.state.toStartOver) { // when we want to erase the current buffer for a new value - this.state.buffer = ''; - } - if (isFirstInput) { - this.state.buffer = '' + input; - } else if (this.state.buffer.length > 12) { - Gui.playSound('bell'); - } else { - this.state.buffer += input; - } - } - if (this.state.buffer === '-') { - this.state.buffer = ''; - } - // once an input is accepted and updated the buffer, - // the buffer should not be in reset state anymore. - this.isReset = false; - // it should not be in a start the buffer over state anymore. - this.state.toStartOver = false; - - if (this.config.maxValue && this.state.buffer > this.config.maxValue) { - this.state.buffer = this.config.maxValue.toString(); - this.config.maxValueReached(); - } - - this.trigger('buffer-update', this.state.buffer); + get() { + return this.state ? this.state.buffer : null; + } + /** + * Takes a string that is convertible to float, and set it as + * value of the buffer. e.g. val = '2.99'; + * + * @param {String} val + */ + set(val) { + this.state.buffer = !isNaN(parseFloat(val)) ? val : ""; + this.trigger("buffer-update", this.state.buffer); + } + /** + * Resets the buffer to empty string. + */ + reset() { + this.isReset = true; + this.state.buffer = ""; + this.trigger("buffer-update", this.state.buffer); + } + /** + * Calling this function, we immediately invoke the `handler` method + * that handles the contents of the input events buffer (`eventsBuffer`). + * This is helpful when we don't want to wait for the timeout that + * is supposed to invoke the handler. + */ + capture() { + if (this.handler) { + clearTimeout(this._timeout); + this.handler(); + delete this.handler; } } + /** + * @returns {number} float equivalent of the value of buffer + */ + getFloat() { + return parse.float(this.get()); + } + /** + * Add keyup listener to window via the useExternalListener hook. + * When the component calling this is unmounted, the listener is also + * removed from window. + */ + activate() { + this.defaultDecimalPoint = _t.database.parameters.decimal_point; + useExternalListener(window, "keyup", this._onKeyboardInput.bind(this)); + } + /** + * @param {Object} config Use to setup the buffer + * @param {String|null} config.decimalPoint The decimal character. + * @param {String|null} config.triggerAtEnter Event triggered when 'Enter' key is pressed. + * @param {String|null} config.triggerAtEsc Event triggered when 'Esc' key is pressed. + * @param {String|null} config.triggerAtInput Event triggered for every accepted input. + * @param {String|null} config.nonKeyboardInputEvent Also listen to a non-keyboard input event + * that carries a payload of { key }. The key is checked if it is a valid input. If valid, + * the number buffer is modified just as it is modified when a keyboard key is pressed. + * @param {Boolean} config.useWithBarcode Whether this buffer is used with barcode. + * @emits config.triggerAtEnter when 'Enter' key is pressed. + * @emits config.triggerAtEsc when 'Esc' key is pressed. + * @emits config.triggerAtInput when an input is accepted. + */ + use(config) { + this.eventsBuffer = []; + const currentComponent = useComponent(); + config = Object.assign(getDefaultConfig(), config); + onMounted(() => { + this.bufferHolderStack.push({ + component: currentComponent, + state: config.state ? config.state : { buffer: "", toStartOver: false }, + config, + }); + this._setUp(); + }); + onWillUnmount(() => { + this.bufferHolderStack.pop(); + this._setUp(); + }); + // Add listener that accepts non keyboard inputs + if (typeof config.nonKeyboardInputEvent === "string") { + useListener(config.nonKeyboardInputEvent, this._onNonKeyboardInput.bind(this)); + } + } + get _currentBufferHolder() { + return this.bufferHolderStack[this.bufferHolderStack.length - 1]; + } + _setUp() { + if (!this._currentBufferHolder) { + return; + } + const { component, state, config } = this._currentBufferHolder; + this.component = component; + this.state = state; + this.config = config; + this.decimalPoint = config.decimalPoint || this.defaultDecimalPoint; + this.maxTimeBetweenKeys = this.config.useWithBarcode + ? barcodeService.maxTimeBetweenKeysInMs + : 0; + } + _onKeyboardInput(event) { + return this._bufferEvents(this._onInput((event) => event.key))(event); + } + _onNonKeyboardInput(event) { + return this._bufferEvents(this._onInput((event) => event.detail.key))(event); + } + _bufferEvents(handler) { + return (event) => { + if (["INPUT", "TEXTAREA"].includes(event.target.tagName) || !this.eventsBuffer) { + return; + } + clearTimeout(this._timeout); + this.eventsBuffer.push(event); + this._timeout = setTimeout(handler, this.maxTimeBetweenKeys); + this.handler = handler; + }; + } + _onInput(keyAccessor) { + return () => { + if (this.eventsBuffer.length <= 2) { + // Check first the buffer if its contents are all valid + // number input. + for (const event of this.eventsBuffer) { + if (!ALLOWED_KEYS.has(keyAccessor(event))) { + this.eventsBuffer = []; + return; + } + } + // At this point, all the events in buffer + // contains number input. It's now okay to handle + // each input. + for (const event of this.eventsBuffer) { + this._handleInput(keyAccessor(event)); + event.preventDefault(); + event.stopPropagation(); + } + } + this.eventsBuffer = []; + }; + } + _handleInput(key) { + if (key === "Enter" && this.config.triggerAtEnter) { + this.component.trigger(this.config.triggerAtEnter, this.state); + } else if (key === "Esc" && this.config.triggerAtEsc) { + this.component.trigger(this.config.triggerAtEsc, this.state); + } else if (INPUT_KEYS.has(key)) { + this._updateBuffer(key); + if (this.config.triggerAtInput) { + this.component.trigger(this.config.triggerAtInput, { + buffer: this.state.buffer, + key, + }); + } + } + } + /** + * Updates the current buffer state using the given input. + * @param {String} input valid input + */ + _updateBuffer(input) { + const isEmpty = (val) => { + return val === "" || val === null; + }; + if (input === undefined || input === null) { + return; + } + const isFirstInput = isEmpty(this.state.buffer); + if (input === "," || input === ".") { + if (this.state.toStartOver) { + this.state.buffer = ""; + } + if (isFirstInput) { + this.state.buffer = "0" + this.decimalPoint; + } else if (!this.state.buffer.length || this.state.buffer === "-") { + this.state.buffer += "0" + this.decimalPoint; + } else if (this.state.buffer.indexOf(this.decimalPoint) < 0) { + this.state.buffer = this.state.buffer + this.decimalPoint; + } + } else if (input === "Delete") { + if (this.isReset) { + this.state.buffer = ""; + this.isReset = false; + return; + } + this.state.buffer = isEmpty(this.state.buffer) ? null : ""; + } else if (input === "Backspace") { + if (this.isReset) { + this.state.buffer = ""; + this.isReset = false; + return; + } + if (this.state.toStartOver) { + this.state.buffer = ""; + } + const buffer = this.state.buffer; + if (isEmpty(buffer)) { + this.state.buffer = null; + } else { + const nCharToRemove = buffer[buffer.length - 1] === this.decimalPoint ? 2 : 1; + this.state.buffer = buffer.substring(0, buffer.length - nCharToRemove); + } + } else if (input === "+") { + if (this.state.buffer[0] === "-") { + this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length); + } + } else if (input === "-") { + if (isFirstInput) { + this.state.buffer = "-0"; + } else if (this.state.buffer[0] === "-") { + this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length); + } else { + this.state.buffer = "-" + this.state.buffer; + } + } else if (input[0] === "+" && !isNaN(parseFloat(input))) { + // when input is like '+10', '+50', etc + const inputValue = parse.float(input.slice(1)); + const currentBufferValue = this.state.buffer ? parse.float(this.state.buffer) : 0; + this.state.buffer = this.component.env.pos.formatFixed(inputValue + currentBufferValue); + } else if (!isNaN(parseInt(input, 10))) { + if (this.state.toStartOver) { + // when we want to erase the current buffer for a new value + this.state.buffer = ""; + } + if (isFirstInput) { + this.state.buffer = "" + input; + } else if (this.state.buffer.length > 12) { + Gui.playSound("bell"); + } else { + this.state.buffer += input; + } + } + if (this.state.buffer === "-") { + this.state.buffer = ""; + } + // once an input is accepted and updated the buffer, + // the buffer should not be in reset state anymore. + this.isReset = false; + // it should not be in a start the buffer over state anymore. + this.state.toStartOver = false; - return new NumberBuffer(); -}); + if (this.config.maxValue && this.state.buffer > this.config.maxValue) { + this.state.buffer = this.config.maxValue.toString(); + this.config.maxValueReached(); + } + + this.trigger("buffer-update", this.state.buffer); + } +} + +export default 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 09fd599ade0..4f74637dcd0 100644 --- a/addons/point_of_sale/static/src/js/Misc/SearchBar.js +++ b/addons/point_of_sale/static/src/js/Misc/SearchBar.js @@ -1,110 +1,114 @@ -odoo.define('point_of_sale.SearchBar', function (require) { - 'use strict'; +/** @odoo-module */ - const { useAutofocus, useListener } = require("@web/core/utils/hooks"); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import { useAutofocus, useListener } from "@web/core/utils/hooks"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - const { useExternalListener, useState } = owl; +const { useExternalListener, useState } = owl; +/** + * This is a simple configurable search bar component. It has search fields + * and selection filter. Search fields allow the users to specify the type + * of their searches. The filter is a dropdown menu for selection. Depending on + * user's action, this component emits corresponding event with the action + * information (payload). + * + * TODO: This component can be made more generic and be able to replace + * all the search bars across pos ui. + * + * @prop {{ + * config: { + * searchFields: Map, + * filter: { show: boolean, options: Map } + * }, + * placeholder: string, + * }} + * @emits search @payload { fieldName: string, searchTerm: '' } + * @emits filter-selected @payload { filter: string } + * + * NOTE: The payload of the emitted event is accessible via the `detail` + * field of the event. + */ +class SearchBar extends PosComponent { + setup() { + super.setup(); + useAutofocus(); + useExternalListener(window, "click", this._hideOptions); + useListener("click-search-field", this._onClickSearchField); + useListener("select-filter", this._onSelectFilter); + this.filterOptionsList = [...this.props.config.filter.options.keys()]; + this.searchFieldsList = [...this.props.config.searchFields.keys()]; + const defaultSearchFieldId = this.searchFieldsList.indexOf( + this.props.config.defaultSearchDetails.fieldName + ); + this.state = useState({ + searchInput: this.props.config.defaultSearchDetails.searchTerm || "", + selectedSearchFieldId: defaultSearchFieldId == -1 ? 0 : defaultSearchFieldId, + showSearchFields: false, + showFilterOptions: false, + selectedFilter: this.props.config.defaultFilter || this.filterOptionsList[0], + }); + } + _onSelectFilter({ detail: key }) { + this.state.selectedFilter = key; + this.trigger("filter-selected", { filter: this.state.selectedFilter }); + } /** - * This is a simple configurable search bar component. It has search fields - * and selection filter. Search fields allow the users to specify the type - * of their searches. The filter is a dropdown menu for selection. Depending on - * user's action, this component emits corresponding event with the action - * information (payload). - * - * TODO: This component can be made more generic and be able to replace - * all the search bars across pos ui. - * - * @prop {{ - * config: { - * searchFields: Map, - * filter: { show: boolean, options: Map } - * }, - * placeholder: string, - * }} - * @emits search @payload { fieldName: string, searchTerm: '' } - * @emits filter-selected @payload { filter: string } - * - * NOTE: The payload of the emitted event is accessible via the `detail` - * field of the event. + * When pressing vertical arrow keys, do not move the input cursor. */ - class SearchBar extends PosComponent { - setup() { - super.setup(); - useAutofocus(); - useExternalListener(window, 'click', this._hideOptions); - useListener('click-search-field', this._onClickSearchField); - useListener('select-filter', this._onSelectFilter); - this.filterOptionsList = [...this.props.config.filter.options.keys()]; - this.searchFieldsList = [...this.props.config.searchFields.keys()]; - const defaultSearchFieldId = this.searchFieldsList.indexOf( - this.props.config.defaultSearchDetails.fieldName - ); - this.state = useState({ - searchInput: this.props.config.defaultSearchDetails.searchTerm || '', - selectedSearchFieldId: defaultSearchFieldId == -1 ? 0 : defaultSearchFieldId, - showSearchFields: false, - showFilterOptions: false, - selectedFilter: this.props.config.defaultFilter || this.filterOptionsList[0], - }); - } - _onSelectFilter({ detail: key }) { - this.state.selectedFilter = key; - this.trigger('filter-selected', { filter: this.state.selectedFilter }); - } - /** - * When pressing vertical arrow keys, do not move the input cursor. - */ - onSearchInputKeydown(event) { - if (['ArrowUp', 'ArrowDown'].includes(event.key)) { - event.preventDefault(); - } - } - /** - * When vertical arrow keys are pressed, select fields for searching. - * When enter key is pressed, trigger search event if there is searchInput. - */ - onSearchInputKeyup(event) { - if (['ArrowUp', 'ArrowDown'].includes(event.key)) { - this.state.selectedSearchFieldId = this._fieldIdToSelect(event.key); - } else if (event.key === 'Enter' || this.state.searchInput == '') { - this._onClickSearchField({ detail: this.searchFieldsList[this.state.selectedSearchFieldId] }); - } else { - if (this.state.selectedSearchFieldId === -1 && this.searchFieldsList.length) { - this.state.selectedSearchFieldId = 0; - } - this.state.showSearchFields = true; - } - } - /** - * Called when a search field is clicked. - */ - _onClickSearchField({ detail: fieldName }) { - this.state.showSearchFields = false; - this.trigger('search', { fieldName, searchTerm: this.state.searchInput }); - } - /** - * Given an arrow key, return the next selectedSearchFieldId. - * E.g. If the selectedSearchFieldId is 1 and ArrowDown is pressed, return 2. - * - * @param {string} key vertical arrow key - */ - _fieldIdToSelect(key) { - const length = this.searchFieldsList.length; - if (!length) return null; - if (this.state.selectedSearchFieldId === -1) return 0; - const current = this.state.selectedSearchFieldId || length; - return (current + (key === 'ArrowDown' ? 1 : -1)) % length; - } - _hideOptions() { - this.state.showFilterOptions = false; - this.state.showSearchFields = false; + onSearchInputKeydown(event) { + if (["ArrowUp", "ArrowDown"].includes(event.key)) { + event.preventDefault(); } } - SearchBar.template = 'point_of_sale.SearchBar'; - Registries.Component.add(SearchBar); + /** + * When vertical arrow keys are pressed, select fields for searching. + * When enter key is pressed, trigger search event if there is searchInput. + */ + onSearchInputKeyup(event) { + if (["ArrowUp", "ArrowDown"].includes(event.key)) { + this.state.selectedSearchFieldId = this._fieldIdToSelect(event.key); + } else if (event.key === "Enter" || this.state.searchInput == "") { + this._onClickSearchField({ + detail: this.searchFieldsList[this.state.selectedSearchFieldId], + }); + } else { + if (this.state.selectedSearchFieldId === -1 && this.searchFieldsList.length) { + this.state.selectedSearchFieldId = 0; + } + this.state.showSearchFields = true; + } + } + /** + * Called when a search field is clicked. + */ + _onClickSearchField({ detail: fieldName }) { + this.state.showSearchFields = false; + this.trigger("search", { fieldName, searchTerm: this.state.searchInput }); + } + /** + * Given an arrow key, return the next selectedSearchFieldId. + * E.g. If the selectedSearchFieldId is 1 and ArrowDown is pressed, return 2. + * + * @param {string} key vertical arrow key + */ + _fieldIdToSelect(key) { + const length = this.searchFieldsList.length; + if (!length) { + return null; + } + if (this.state.selectedSearchFieldId === -1) { + return 0; + } + const current = this.state.selectedSearchFieldId || length; + return (current + (key === "ArrowDown" ? 1 : -1)) % length; + } + _hideOptions() { + this.state.showFilterOptions = false; + this.state.showSearchFields = false; + } +} +SearchBar.template = "point_of_sale.SearchBar"; +Registries.Component.add(SearchBar); - return 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 cf470676614..f0d92fca7ee 100644 --- a/addons/point_of_sale/static/src/js/Notification.js +++ b/addons/point_of_sale/static/src/js/Notification.js @@ -1,27 +1,25 @@ -odoo.define('point_of_sale.Notification', function (require) { - 'use strict'; +/** @odoo-module */ - const { useListener } = require("@web/core/utils/hooks"); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import { useListener } from "@web/core/utils/hooks"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - const { onMounted } = owl; +const { onMounted } = owl; - class Notification extends PosComponent { - setup() { - super.setup(); - useListener('click', this.closeNotification); +class Notification extends PosComponent { + setup() { + super.setup(); + useListener("click", this.closeNotification); - onMounted(() => { - setTimeout(() => { - this.closeNotification(); - }, this.props.duration) - }); - } + onMounted(() => { + setTimeout(() => { + this.closeNotification(); + }, this.props.duration); + }); } - Notification.template = 'Notification'; +} +Notification.template = "Notification"; - Registries.Component.add(Notification); +Registries.Component.add(Notification); - return 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 ce88df5fbc2..3041691a7d8 100644 --- a/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js @@ -1,64 +1,62 @@ -odoo.define('point_of_sale.AbstractAwaitablePopup', function (require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const { useBus } = require('@web/core/utils/hooks'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import { useBus } from "@web/core/utils/hooks"; - /** - * Implement this abstract class by extending it like so: - * ```js - * class ConcretePopup extends AbstractAwaitablePopup { - * async getPayload() { - * return 'result'; - * } - * } - * ConcretePopup.template = xml` - *
- * - * - *
- * ` - * ``` - * - * 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'); - * // based on the implementation above, - * // if confirmed, payload = 'result' - * // otherwise, payload = null - * ``` - */ - class AbstractAwaitablePopup extends PosComponent { - setup() { - super.setup(); - if (this.props.confirmKey) { - useBus(this.env.posbus, `confirm-popup-${this.props.id}`, this.confirm); - } - if (this.props.cancelKey) { - useBus(this.env.posbus, `cancel-popup-${this.props.id}`, this.cancel); - } +/** + * Implement this abstract class by extending it like so: + * ```js + * class ConcretePopup extends AbstractAwaitablePopup { + * async getPayload() { + * return 'result'; + * } + * } + * ConcretePopup.template = xml` + *
+ * + * + *
+ * ` + * ``` + * + * 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'); + * // based on the implementation above, + * // if confirmed, payload = 'result' + * // otherwise, payload = null + * ``` + */ +class AbstractAwaitablePopup extends PosComponent { + setup() { + super.setup(); + if (this.props.confirmKey) { + useBus(this.env.posbus, `confirm-popup-${this.props.id}`, this.confirm); } - async confirm() { - this.env.posbus.trigger('close-popup', { - popupId: this.props.id, - response: { confirmed: true, payload: await this.getPayload() }, - }); - } - cancel() { - this.env.posbus.trigger('close-popup', { - popupId: this.props.id, - response: { confirmed: false, payload: null }, - }); - } - /** - * Override this in the concrete popup implementation to set the - * payload when the popup is confirmed. - */ - async getPayload() { - return null; + if (this.props.cancelKey) { + useBus(this.env.posbus, `cancel-popup-${this.props.id}`, this.cancel); } } + async confirm() { + this.env.posbus.trigger("close-popup", { + popupId: this.props.id, + response: { confirmed: true, payload: await this.getPayload() }, + }); + } + cancel() { + this.env.posbus.trigger("close-popup", { + popupId: this.props.id, + response: { confirmed: false, payload: null }, + }); + } + /** + * Override this in the concrete popup implementation to set the + * payload when the popup is confirmed. + */ + async getPayload() { + return null; + } +} - return AbstractAwaitablePopup; -}); +export default AbstractAwaitablePopup; 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 407c37625b6..a1cafb05b6a 100644 --- a/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js @@ -1,57 +1,55 @@ -odoo.define('point_of_sale.CashMovePopup', function (require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { parse } = require('web.field_utils'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { parse } from "web.field_utils"; - const { useRef, useState, onMounted } = owl; +const { useRef, useState, onMounted } = owl; - class CashMovePopup extends AbstractAwaitablePopup { - setup() { - super.setup(); - this.state = useState({ - inputType: 'out', // '' | 'in' | 'out' - inputAmount: '', - inputReason: '', - errorMessage: '', - }); - this.inputAmountRef = useRef('input-amount-ref'); - onMounted(() => this.inputAmountRef.el.focus()); +class CashMovePopup extends AbstractAwaitablePopup { + setup() { + super.setup(); + this.state = useState({ + inputType: "out", // '' | 'in' | 'out' + inputAmount: "", + inputReason: "", + errorMessage: "", + }); + this.inputAmountRef = useRef("input-amount-ref"); + onMounted(() => this.inputAmountRef.el.focus()); + } + confirm() { + try { + parse.float(this.state.inputAmount); + } catch { + this.state.errorMessage = this.env._t("Invalid amount"); + return; } - confirm() { - try { - parse.float(this.state.inputAmount); - } catch { - this.state.errorMessage = this.env._t('Invalid amount'); - return; - } - if (this.state.inputAmount < 0) { - this.state.errorMessage = this.env._t('Insert a positive amount'); - return; - } - return super.confirm(); + if (this.state.inputAmount < 0) { + this.state.errorMessage = this.env._t("Insert a positive amount"); + return; } - _onAmountKeypress(event) { - if (['-', '+'].includes(event.key)) { - event.preventDefault(); - } - } - onClickButton(type) { - this.state.inputType = type; - this.state.errorMessage = ''; - this.inputAmountRef.el.focus(); - } - getPayload() { - return { - amount: parse.float(this.state.inputAmount), - reason: this.state.inputReason.trim(), - type: this.state.inputType, - }; + return super.confirm(); + } + _onAmountKeypress(event) { + if (["-", "+"].includes(event.key)) { + event.preventDefault(); } } - CashMovePopup.template = 'point_of_sale.CashMovePopup'; - Registries.Component.add(CashMovePopup); + onClickButton(type) { + this.state.inputType = type; + this.state.errorMessage = ""; + this.inputAmountRef.el.focus(); + } + getPayload() { + return { + amount: parse.float(this.state.inputAmount), + reason: this.state.inputReason.trim(), + type: this.state.inputType, + }; + } +} +CashMovePopup.template = "point_of_sale.CashMovePopup"; +Registries.Component.add(CashMovePopup); - return 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 f234267b34e..59c77266ba9 100644 --- a/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js @@ -1,58 +1,58 @@ -odoo.define('point_of_sale.CashOpeningPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; - const { useState } = owl; +const { useState } = owl; - class CashOpeningPopup extends AbstractAwaitablePopup { - setup() { - super.setup(); - this.manualInputCashCount = null; - this.moneyDetails = null; - this.state = useState({ - notes: "", - openingCash: this.env.pos.pos_session.cash_register_balance_start || 0, - }); - } - //@override - async confirm() { - this.env.pos.pos_session.cash_register_balance_start = this.state.openingCash; - this.env.pos.pos_session.state = 'opened'; - this.rpc({ - model: 'pos.session', - method: 'set_cashbox_pos', - args: [this.env.pos.pos_session.id, this.state.openingCash, this.state.notes], - }); - super.confirm(); - } - async openDetailsPopup() { - const { confirmed, payload } = await this.showPopup('MoneyDetailsPopup', { - moneyDetails: this.moneyDetails, total: this.manualInputCashCount ? 0 : this.state.openingCash }); - if (confirmed) { - const { total, moneyDetails, moneyDetailsNotes } = payload; - this.state.openingCash = total; - if (moneyDetailsNotes) { - this.state.notes = moneyDetailsNotes; - } - this.manualInputCashCount = false; - this.moneyDetails = moneyDetails; - } - } - handleInputChange() { - this.manualInputCashCount = true; - this.moneyDetails = null; - this.state.notes = ""; - if (typeof(this.state.openingCash) !== "number") { - this.state.openingCash = 0; +class CashOpeningPopup extends AbstractAwaitablePopup { + setup() { + super.setup(); + this.manualInputCashCount = null; + this.moneyDetails = null; + this.state = useState({ + notes: "", + openingCash: this.env.pos.pos_session.cash_register_balance_start || 0, + }); + } + //@override + async confirm() { + this.env.pos.pos_session.cash_register_balance_start = this.state.openingCash; + this.env.pos.pos_session.state = "opened"; + this.rpc({ + model: "pos.session", + method: "set_cashbox_pos", + args: [this.env.pos.pos_session.id, this.state.openingCash, this.state.notes], + }); + super.confirm(); + } + async openDetailsPopup() { + const { confirmed, payload } = await this.showPopup("MoneyDetailsPopup", { + moneyDetails: this.moneyDetails, + total: this.manualInputCashCount ? 0 : this.state.openingCash, + }); + if (confirmed) { + const { total, moneyDetails, moneyDetailsNotes } = payload; + this.state.openingCash = total; + if (moneyDetailsNotes) { + this.state.notes = moneyDetailsNotes; } + this.manualInputCashCount = false; + this.moneyDetails = moneyDetails; } } + handleInputChange() { + this.manualInputCashCount = true; + this.moneyDetails = null; + this.state.notes = ""; + if (typeof this.state.openingCash !== "number") { + this.state.openingCash = 0; + } + } +} - CashOpeningPopup.template = 'CashOpeningPopup'; - CashOpeningPopup.defaultProps = { cancelKey: false }; - Registries.Component.add(CashOpeningPopup); +CashOpeningPopup.template = "CashOpeningPopup"; +CashOpeningPopup.defaultProps = { cancelKey: false }; +Registries.Component.add(CashOpeningPopup); - return 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 4efdf008f5a..f0e9e3fe780 100644 --- a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js @@ -1,174 +1,192 @@ -odoo.define('point_of_sale.ClosePosPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { identifyError } = require('point_of_sale.utils'); - const { ConnectionLostError, ConnectionAbortedError} = require('@web/core/network/rpc_service') - const { useState } = owl; +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { identifyError } from "@point_of_sale/js/utils"; +import { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service"; +const { useState } = owl; - class ClosePosPopup extends AbstractAwaitablePopup { - setup() { - super.setup(); - this.manualInputCashCount = false; - this.cashControl = this.env.pos.config.cash_control; - this.closeSessionClicked = false; - this.moneyDetails = null; - Object.assign(this, this.props.info); - this.state = useState({ - displayMoneyDetailsPopup: false, - }); - Object.assign(this.state, this.props.info.state); - } - //@override - async confirm() { - if (!this.cashControl || !this.hasDifference()) { - this.closeSession(); - } else if (this.hasUserAuthority()) { - 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?'), - }); - if (confirmed) { - this.closeSession(); - } - } else { - await this.showPopup('ConfirmPopup', { - title: this.env._t('Payments Difference'), - body: _.str.sprintf( - this.env._t('The maximum difference allowed is %s.\n\ - Please contact your manager to accept the closing difference.'), - this.env.pos.format_currency(this.amountAuthorizedDiff) - ), - confirmText: this.env._t('OK'), - }) - } - } - //@override - async cancel() { - if (this.canCancel()) { - super.cancel(); - } - } - async openDetailsPopup() { - const { confirmed, payload } = await this.showPopup('MoneyDetailsPopup', { - moneyDetails: this.moneyDetails, - total: this.manualInputCashCount ? 0 : this.state.payments[this.defaultCashDetails.id].counted, +class ClosePosPopup extends AbstractAwaitablePopup { + setup() { + super.setup(); + this.manualInputCashCount = false; + this.cashControl = this.env.pos.config.cash_control; + this.closeSessionClicked = false; + this.moneyDetails = null; + Object.assign(this, this.props.info); + this.state = useState({ + displayMoneyDetailsPopup: false, + }); + Object.assign(this.state, this.props.info.state); + } + //@override + async confirm() { + if (!this.cashControl || !this.hasDifference()) { + this.closeSession(); + } else if (this.hasUserAuthority()) { + 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?" + ), }); if (confirmed) { - const { total, moneyDetailsNotes, moneyDetails } = payload; - this.state.payments[this.defaultCashDetails.id].counted = total; - this.state.payments[this.defaultCashDetails.id].difference = - this.env.pos.round_decimals_currency(this.state.payments[[this.defaultCashDetails.id]].counted - this.defaultCashDetails.amount); - if (moneyDetailsNotes) { - this.state.notes = moneyDetailsNotes; - } - this.manualInputCashCount = false; - this.moneyDetails = moneyDetails; + this.closeSession(); } - } - async downloadSalesReport() { - await this.env.legacyActionManager.do_action('point_of_sale.sale_details_report', { - additional_context: { - active_ids: [this.env.pos.pos_session.id], - }, + } else { + await this.showPopup("ConfirmPopup", { + title: this.env._t("Payments Difference"), + body: _.str.sprintf( + this.env._t( + "The maximum difference allowed is %s.\n\ + Please contact your manager to accept the closing difference." + ), + this.env.pos.format_currency(this.amountAuthorizedDiff) + ), + confirmText: this.env._t("OK"), }); } - handleInputChange(paymentId) { - let expectedAmount; - if (paymentId === this.defaultCashDetails.id) { - this.manualInputCashCount = true; - this.moneyDetails = null; - this.state.notes = ''; - expectedAmount = this.defaultCashDetails.amount; - } else { - expectedAmount = this.otherPaymentMethods.find(pm => paymentId === pm.id).amount; - } - this.state.payments[paymentId].difference = - this.env.pos.round_decimals_currency(this.state.payments[paymentId].counted - expectedAmount); - } - hasDifference() { - return Object.entries(this.state.payments).find(pm => pm[1].difference != 0); - } - hasUserAuthority() { - const absDifferences = Object.entries(this.state.payments).map(pm => Math.abs(pm[1].difference)); - return this.isManager || this.amountAuthorizedDiff == null || Math.max(...absDifferences) <= this.amountAuthorizedDiff; - } - canCancel() { - return true; - } - closePos() { - this.trigger('close-pos'); - } - async closeSession() { - if (!this.closeSessionClicked) { - this.closeSessionClicked = true; - let response; - if (this.cashControl) { - response = await this.rpc({ - model: 'pos.session', - method: 'post_closing_cash_details', - args: [this.env.pos.pos_session.id], - kwargs: { - counted_cash: this.state.payments[this.defaultCashDetails.id].counted, - } - }) - if (!response.successful) { - return this.handleClosingError(response); - } - } - await this.rpc({ - model: 'pos.session', - method: 'update_closing_control_state_session', - args: [this.env.pos.pos_session.id, this.state.notes] - }) - try { - const bankPaymentMethodDiffPairs = this.otherPaymentMethods - .filter((pm) => pm.type == 'bank') - .map((pm) => [pm.id, this.state.payments[pm.id].difference]); - response = await this.rpc({ - model: 'pos.session', - method: 'close_session_from_ui', - args: [this.env.pos.pos_session.id, bankPaymentMethodDiffPairs], - context: this.env.session.user_context, - }); - if (!response.successful) { - return this.handleClosingError(response); - } - window.location = '/web#action=point_of_sale.action_client_pos_menu'; - } catch (error) { - const iError = identifyError(error); - if (iError instanceof ConnectionLostError || iError instanceof ConnectionAbortedError) { - await this.showPopup('ErrorPopup', { - title: this.env._t('Network Error'), - body: this.env._t('Cannot close the session when offline.'), - }); - } else { - 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' + - 'You will be redirected to the back-end to manually close the session.') - }) - window.location = '/web#action=point_of_sale.action_client_pos_menu'; - } - } - this.closeSessionClicked = false; - } - } - async handleClosingError(response) { - await this.showPopup('ErrorPopup', {title: 'Error', body: response.message}); - if (response.redirect) { - window.location = '/web#action=point_of_sale.action_client_pos_menu'; - } - } - _getShowDiff(pm) { - return pm.type == 'bank' && pm.number !== 0; + } + //@override + async cancel() { + if (this.canCancel()) { + super.cancel(); } } + async openDetailsPopup() { + const { confirmed, payload } = await this.showPopup("MoneyDetailsPopup", { + moneyDetails: this.moneyDetails, + total: this.manualInputCashCount + ? 0 + : this.state.payments[this.defaultCashDetails.id].counted, + }); + if (confirmed) { + const { total, moneyDetailsNotes, moneyDetails } = payload; + this.state.payments[this.defaultCashDetails.id].counted = total; + this.state.payments[this.defaultCashDetails.id].difference = + this.env.pos.round_decimals_currency( + this.state.payments[[this.defaultCashDetails.id]].counted - + this.defaultCashDetails.amount + ); + if (moneyDetailsNotes) { + this.state.notes = moneyDetailsNotes; + } + this.manualInputCashCount = false; + this.moneyDetails = moneyDetails; + } + } + async downloadSalesReport() { + await this.env.legacyActionManager.do_action("point_of_sale.sale_details_report", { + additional_context: { + active_ids: [this.env.pos.pos_session.id], + }, + }); + } + handleInputChange(paymentId) { + let expectedAmount; + if (paymentId === this.defaultCashDetails.id) { + this.manualInputCashCount = true; + this.moneyDetails = null; + this.state.notes = ""; + expectedAmount = this.defaultCashDetails.amount; + } else { + expectedAmount = this.otherPaymentMethods.find((pm) => paymentId === pm.id).amount; + } + this.state.payments[paymentId].difference = this.env.pos.round_decimals_currency( + this.state.payments[paymentId].counted - expectedAmount + ); + } + hasDifference() { + return Object.entries(this.state.payments).find((pm) => pm[1].difference != 0); + } + hasUserAuthority() { + const absDifferences = Object.entries(this.state.payments).map((pm) => + Math.abs(pm[1].difference) + ); + return ( + this.isManager || + this.amountAuthorizedDiff == null || + Math.max(...absDifferences) <= this.amountAuthorizedDiff + ); + } + canCancel() { + return true; + } + closePos() { + this.trigger("close-pos"); + } + async closeSession() { + if (!this.closeSessionClicked) { + this.closeSessionClicked = true; + let response; + if (this.cashControl) { + response = await this.rpc({ + model: "pos.session", + method: "post_closing_cash_details", + args: [this.env.pos.pos_session.id], + kwargs: { + counted_cash: this.state.payments[this.defaultCashDetails.id].counted, + }, + }); + if (!response.successful) { + return this.handleClosingError(response); + } + } + await this.rpc({ + model: "pos.session", + method: "update_closing_control_state_session", + args: [this.env.pos.pos_session.id, this.state.notes], + }); + try { + const bankPaymentMethodDiffPairs = this.otherPaymentMethods + .filter((pm) => pm.type == "bank") + .map((pm) => [pm.id, this.state.payments[pm.id].difference]); + response = await this.rpc({ + model: "pos.session", + method: "close_session_from_ui", + args: [this.env.pos.pos_session.id, bankPaymentMethodDiffPairs], + context: this.env.session.user_context, + }); + if (!response.successful) { + return this.handleClosingError(response); + } + window.location = "/web#action=point_of_sale.action_client_pos_menu"; + } catch (error) { + const iError = identifyError(error); + if ( + iError instanceof ConnectionLostError || + iError instanceof ConnectionAbortedError + ) { + await this.showPopup("ErrorPopup", { + title: this.env._t("Network Error"), + body: this.env._t("Cannot close the session when offline."), + }); + } else { + 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" + + "You will be redirected to the back-end to manually close the session." + ), + }); + window.location = "/web#action=point_of_sale.action_client_pos_menu"; + } + } + this.closeSessionClicked = false; + } + } + async handleClosingError(response) { + await this.showPopup("ErrorPopup", { title: "Error", body: response.message }); + if (response.redirect) { + window.location = "/web#action=point_of_sale.action_client_pos_menu"; + } + } + _getShowDiff(pm) { + return pm.type == "bank" && pm.number !== 0; + } +} - ClosePosPopup.template = 'ClosePosPopup'; - Registries.Component.add(ClosePosPopup); +ClosePosPopup.template = "ClosePosPopup"; +Registries.Component.add(ClosePosPopup); - return 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 4536feb3e94..106ba159911 100644 --- a/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js @@ -1,21 +1,19 @@ -odoo.define('point_of_sale.ConfirmPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - // formerly ConfirmPopupWidget - class ConfirmPopup extends AbstractAwaitablePopup {} - ConfirmPopup.template = 'ConfirmPopup'; - ConfirmPopup.defaultProps = { - confirmText: _lt('Ok'), - cancelText: _lt('Cancel'), - title: _lt('Confirm ?'), - body: '', - }; +// 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); +Registries.Component.add(ConfirmPopup); - return ConfirmPopup; -}); +export default ConfirmPopup; 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 924accf4b7f..2d4eb65b541 100644 --- a/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js @@ -1,28 +1,26 @@ -odoo.define('point_of_sale.ControlButtonPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - class ControlButtonPopup extends AbstractAwaitablePopup { - /** - * @param {Object} props - * @param {string} props.startingValue - */ - setup() { - super.setup(); - this.controlButtons = this.props.controlButtons; - } +class ControlButtonPopup extends AbstractAwaitablePopup { + /** + * @param {Object} props + * @param {string} props.startingValue + */ + setup() { + super.setup(); + this.controlButtons = this.props.controlButtons; } - ControlButtonPopup.template = 'ControlButtonPopup'; - ControlButtonPopup.defaultProps = { - cancelText: _lt('Back'), - controlButtons: [], - confirmKey: false, - }; +} +ControlButtonPopup.template = "ControlButtonPopup"; +ControlButtonPopup.defaultProps = { + cancelText: _lt("Back"), + controlButtons: [], + confirmKey: false, +}; - Registries.Component.add(ControlButtonPopup); +Registries.Component.add(ControlButtonPopup); - return ControlButtonPopup; -}); +export default ControlButtonPopup; 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 e4e7769685f..3b48b008fae 100644 --- a/addons/point_of_sale/static/src/js/Popups/EditListInput.js +++ b/addons/point_of_sale/static/src/js/Popups/EditListInput.js @@ -1,29 +1,27 @@ -odoo.define('point_of_sale.EditListInput', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - /** - * props { - * createNewItem: callback, - * removeItem: callback, - * item: object, - * } - */ - class EditListInput extends PosComponent { - onKeyup(event) { - if (event.key === "Enter" && event.target.value.trim() !== '') { - this.props.createNewItem(); - } - } - onInput(event) { - this.props.onInputChange(this.props.item._id, event.target.value); +/** + * props { + * createNewItem: callback, + * removeItem: callback, + * item: object, + * } + */ +class EditListInput extends PosComponent { + onKeyup(event) { + if (event.key === "Enter" && event.target.value.trim() !== "") { + this.props.createNewItem(); } } - EditListInput.template = 'EditListInput'; + onInput(event) { + this.props.onInputChange(this.props.item._id, event.target.value); + } +} +EditListInput.template = "EditListInput"; - Registries.Component.add(EditListInput); +Registries.Component.add(EditListInput); - return 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 e5fc46198fa..c1aa8220674 100644 --- a/addons/point_of_sale/static/src/js/Popups/EditListPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/EditListPopup.js @@ -1,109 +1,117 @@ -odoo.define('point_of_sale.EditListPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { useAutoFocusToLast } = require('point_of_sale.custom_hooks'); - const { _lt } = require('@web/core/l10n/translation'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { useAutoFocusToLast } from "@point_of_sale/js/custom_hooks"; +import { _lt } from "@web/core/l10n/translation"; - const { useState } = owl; +const { useState } = owl; +/** + * Given a array of { id, text }, we show the user this popup to be able to modify this given array. + * (used to replace PackLotLinePopupWidget) + * + * The expected return of showPopup when this popup is used is an array of { _id, [id], text }. + * - _id is the assigned unique identifier for each item. + * - id is the original id. if not provided, then it means that the item is new. + * - text is the modified/unmodified text. + * + * Example: + * + * ``` + * -- perhaps inside a click handler -- + * // gather the items to edit + * const names = [{ id: 1, text: 'Joseph'}, { id: 2, text: 'Kaykay' }]; + * + * // 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', { + * title: "Can you confirm this item?", + * array: names }) + * + * // we then consume the new data. In this example, it is only logged. + * if (confirmed) { + * console.log(newNames); + * // the above might log the following: + * // [{ _id: 1, id: 1, text: 'Joseph Caburnay' }, { _id: 2, id: 2, 'Kaykay' }, { _id: 3, 'James' }] + * // The result showed that the original item with id=1 was changed to have text 'Joseph Caburnay' from 'Joseph' + * // The one with id=2 did not change. And a new item with text='James' is added. + * } + * ``` + */ +class EditListPopup extends AbstractAwaitablePopup { /** - * Given a array of { id, text }, we show the user this popup to be able to modify this given array. - * (used to replace PackLotLinePopupWidget) - * - * The expected return of showPopup when this popup is used is an array of { _id, [id], text }. - * - _id is the assigned unique identifier for each item. - * - id is the original id. if not provided, then it means that the item is new. - * - text is the modified/unmodified text. - * - * Example: - * - * ``` - * -- perhaps inside a click handler -- - * // gather the items to edit - * const names = [{ id: 1, text: 'Joseph'}, { id: 2, text: 'Kaykay' }]; - * - * // 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', { - * title: "Can you confirm this item?", - * array: names }) - * - * // we then consume the new data. In this example, it is only logged. - * if (confirmed) { - * console.log(newNames); - * // the above might log the following: - * // [{ _id: 1, id: 1, text: 'Joseph Caburnay' }, { _id: 2, id: 2, 'Kaykay' }, { _id: 3, 'James' }] - * // The result showed that the original item with id=1 was changed to have text 'Joseph Caburnay' from 'Joseph' - * // The one with id=2 did not change. And a new item with text='James' is added. - * } - * ``` + * @param {String} title required title of popup + * @param {Array} [props.array=[]] the array of { id, text } to be edited or an array of strings + * @param {Boolean} [props.isSingleItem=false] true if only allowed to edit single item (the first item) */ - class EditListPopup extends AbstractAwaitablePopup { - /** - * @param {String} title required title of popup - * @param {Array} [props.array=[]] the array of { id, text } to be edited or an array of strings - * @param {Boolean} [props.isSingleItem=false] true if only allowed to edit single item (the first item) - */ - setup() { - super.setup(); - this._id = 0; - this.state = useState({ array: this._initialize(this.props.array) }); - useAutoFocusToLast(); - } - _nextId() { - return this._id++; - } - _emptyItem() { - return { - text: '', - _id: this._nextId(), - }; - } - _initialize(array) { - // If no array is provided, we initialize with one empty item. - if (array.length === 0) return [this._emptyItem()]; - // Put _id for each item. It will serve as unique identifier of each item. - return array.map((item) => Object.assign({}, { _id: this._nextId() }, typeof item === 'object'? item: { 'text': item})); - } - _hasMoreThanOneItem() { - return this.state.array.length > 1; - } - removeItem(itemId) { - this.state.array.splice( - this.state.array.findIndex(item => item._id == itemId), - 1 - ); - } - onInputChange(itemId, text) { - const item = this.state.array.find(elem => elem._id === itemId); - item.text = text; - } - createNewItem() { - if (this.props.isSingleItem) return; - this.state.array.push(this._emptyItem()); - } - /** - * @override - */ - getPayload() { - return { - newArray: this.state.array - .filter((item) => item.text.trim() !== '') - .map((item) => Object.assign({}, item)), - }; - } + setup() { + super.setup(); + this._id = 0; + this.state = useState({ array: this._initialize(this.props.array) }); + useAutoFocusToLast(); } - EditListPopup.template = 'EditListPopup'; - EditListPopup.defaultProps = { - confirmText: _lt('Add'), - cancelText: _lt('Discard'), - array: [], - isSingleItem: false, - }; + _nextId() { + return this._id++; + } + _emptyItem() { + return { + text: "", + _id: this._nextId(), + }; + } + _initialize(array) { + // If no array is provided, we initialize with one empty item. + if (array.length === 0) { + return [this._emptyItem()]; + } + // Put _id for each item. It will serve as unique identifier of each item. + return array.map((item) => + Object.assign( + {}, + { _id: this._nextId() }, + typeof item === "object" ? item : { text: item } + ) + ); + } + _hasMoreThanOneItem() { + return this.state.array.length > 1; + } + removeItem(itemId) { + this.state.array.splice( + this.state.array.findIndex((item) => item._id == itemId), + 1 + ); + } + onInputChange(itemId, text) { + const item = this.state.array.find((elem) => elem._id === itemId); + item.text = text; + } + createNewItem() { + if (this.props.isSingleItem) { + return; + } + this.state.array.push(this._emptyItem()); + } + /** + * @override + */ + getPayload() { + return { + newArray: this.state.array + .filter((item) => item.text.trim() !== "") + .map((item) => Object.assign({}, item)), + }; + } +} +EditListPopup.template = "EditListPopup"; +EditListPopup.defaultProps = { + confirmText: _lt("Add"), + cancelText: _lt("Discard"), + array: [], + isSingleItem: false, +}; - Registries.Component.add(EditListPopup); +Registries.Component.add(EditListPopup); - return 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 ded7dbdca80..9a43be839cc 100644 --- a/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js @@ -1,27 +1,26 @@ -odoo.define('point_of_sale.ErrorBarcodePopup', function(require) { - 'use strict'; +/** @odoo-module */ - const ErrorPopup = require('point_of_sale.ErrorPopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - // formerly ErrorBarcodePopupWidget - class ErrorBarcodePopup extends ErrorPopup { - get translatedMessage() { - return this.env._t(this.props.message); - } +// formerly ErrorBarcodePopupWidget +class ErrorBarcodePopup extends ErrorPopup { + 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.'), - }; +} +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); +Registries.Component.add(ErrorBarcodePopup); - return 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 8300b6a5749..d685efa91c8 100644 --- a/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js @@ -1,29 +1,27 @@ -odoo.define('point_of_sale.ErrorPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - // formerly ErrorPopupWidget - class ErrorPopup extends AbstractAwaitablePopup { - setup() { - super.setup(); - owl.onMounted(this.onMounted); - } - onMounted() { - this.playSound('error'); - } +// formerly ErrorPopupWidget +class ErrorPopup extends AbstractAwaitablePopup { + setup() { + super.setup(); + owl.onMounted(this.onMounted); } - ErrorPopup.template = 'ErrorPopup'; - ErrorPopup.defaultProps = { - confirmText: _lt('Ok'), - title: _lt('Error'), - body: '', - cancelKey: false, - }; + onMounted() { + this.playSound("error"); + } +} +ErrorPopup.template = "ErrorPopup"; +ErrorPopup.defaultProps = { + confirmText: _lt("Ok"), + title: _lt("Error"), + body: "", + cancelKey: false, +}; - Registries.Component.add(ErrorPopup); +Registries.Component.add(ErrorPopup); - return 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 c8990ad5578..42dbb93a590 100644 --- a/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js @@ -1,46 +1,44 @@ -odoo.define('point_of_sale.ErrorTracebackPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const ErrorPopup = require('point_of_sale.ErrorPopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - // formerly ErrorTracebackPopupWidget - class ErrorTracebackPopup extends ErrorPopup { - get tracebackUrl() { - const blob = new Blob([this.props.body]); - const URL = window.URL || window.webkitURL; - return URL.createObjectURL(blob); - } - get tracebackFilename() { - return `${this.env._t('error')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.txt`; - } - emailTraceback() { - const address = this.env.pos.company.email; - const subject = this.env._t('IMPORTANT: Bug Report From Odoo Point Of Sale'); - window.open( - 'mailto:' + - address + - '?subject=' + - (subject ? window.encodeURIComponent(subject) : '') + - '&body=' + - (this.props.body ? window.encodeURIComponent(this.props.body) : '') - ); - } +// formerly ErrorTracebackPopupWidget +class ErrorTracebackPopup extends ErrorPopup { + get tracebackUrl() { + const blob = new Blob([this.props.body]); + const URL = window.URL || window.webkitURL; + return URL.createObjectURL(blob); } - 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' - }; + get tracebackFilename() { + return `${this.env._t("error")} ${moment().format("YYYY-MM-DD-HH-mm-ss")}.txt`; + } + emailTraceback() { + const address = this.env.pos.company.email; + const subject = this.env._t("IMPORTANT: Bug Report From Odoo Point Of Sale"); + window.open( + "mailto:" + + address + + "?subject=" + + (subject ? window.encodeURIComponent(subject) : "") + + "&body=" + + (this.props.body ? window.encodeURIComponent(this.props.body) : "") + ); + } +} +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); +Registries.Component.add(ErrorTracebackPopup); - return 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 9a7750d3ca0..5068a23317a 100644 --- a/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js @@ -1,48 +1,58 @@ -odoo.define('point_of_sale.MoneyDetailsPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; - const { useState } = owl; +const { useState } = owl; - class MoneyDetailsPopup extends AbstractAwaitablePopup{ - setup() { - super.setup(); - this.currency = this.env.pos.currency; - this.state = useState({ - moneyDetails: this.props.moneyDetails ? {...this.props.moneyDetails} : - Object.fromEntries(this.env.pos.bills.map(bill => ([bill.value, 0]))), - total: this.props.total ? this.props.total : 0, - }); - } - get firstHalfMoneyDetails() { - const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b); - return moneyDetailsKeys.slice(0, Math.ceil(moneyDetailsKeys.length/2)); - } - get lastHalfMoneyDetails() { - const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b); - return moneyDetailsKeys.slice(Math.ceil(moneyDetailsKeys.length/2), moneyDetailsKeys.length); - } - updateMoneyDetailsAmount() { - let total = Object.entries(this.state.moneyDetails).reduce((total, money) => total + money[0] * money[1], 0); - this.state.total = this.env.pos.round_decimals_currency(total); - } - //@override - async getPayload() { - let moneyDetailsNotes = this.state.total ? 'Money details: \n' : null; - this.env.pos.bills.forEach(bill => { - if (this.state.moneyDetails[bill.value]) { - moneyDetailsNotes += ` - ${this.state.moneyDetails[bill.value]} x ${this.env.pos.format_currency(bill.value)}\n`; - } - }) - return { total: this.state.total, moneyDetailsNotes, moneyDetails: { ...this.state.moneyDetails } }; - } +class MoneyDetailsPopup extends AbstractAwaitablePopup { + setup() { + super.setup(); + this.currency = this.env.pos.currency; + this.state = useState({ + moneyDetails: this.props.moneyDetails + ? { ...this.props.moneyDetails } + : Object.fromEntries(this.env.pos.bills.map((bill) => [bill.value, 0])), + total: this.props.total ? this.props.total : 0, + }); } + get firstHalfMoneyDetails() { + const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b); + return moneyDetailsKeys.slice(0, Math.ceil(moneyDetailsKeys.length / 2)); + } + get lastHalfMoneyDetails() { + const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b); + return moneyDetailsKeys.slice( + Math.ceil(moneyDetailsKeys.length / 2), + moneyDetailsKeys.length + ); + } + updateMoneyDetailsAmount() { + const total = Object.entries(this.state.moneyDetails).reduce( + (total, money) => total + money[0] * money[1], + 0 + ); + this.state.total = this.env.pos.round_decimals_currency(total); + } + //@override + async getPayload() { + let moneyDetailsNotes = this.state.total ? "Money details: \n" : null; + this.env.pos.bills.forEach((bill) => { + if (this.state.moneyDetails[bill.value]) { + moneyDetailsNotes += ` - ${ + this.state.moneyDetails[bill.value] + } x ${this.env.pos.format_currency(bill.value)}\n`; + } + }); + return { + total: this.state.total, + moneyDetailsNotes, + moneyDetails: { ...this.state.moneyDetails }, + }; + } +} - MoneyDetailsPopup.template = 'MoneyDetailsPopup'; - Registries.Component.add(MoneyDetailsPopup); +MoneyDetailsPopup.template = "MoneyDetailsPopup"; +Registries.Component.add(MoneyDetailsPopup); - return 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 6575cd99ee8..c20515d8839 100644 --- a/addons/point_of_sale/static/src/js/Popups/NumberPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js @@ -1,80 +1,80 @@ -odoo.define('point_of_sale.NumberPopup', function(require) { - 'use strict'; - var core = require('web.core'); - var _t = core._t; +/** @odoo-module */ +import core from "web.core"; +var _t = core._t; - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const NumberBuffer = require('point_of_sale.NumberBuffer'); - const { useListener } = require("@web/core/utils/hooks"); - const Registries = require('point_of_sale.Registries'); +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"; - const { useState } = owl; +const { useState } = owl; - // formerly NumberPopupWidget - class NumberPopup extends AbstractAwaitablePopup { - /** - * @param {Object} props - * @param {Boolean} props.isPassword Show password popup. - * @param {number|null} props.startingValue Starting value of the popup. - * @param {Boolean} props.isInputSelected Input is highlighted and will reset upon a change. - * - * Resolve to { confirmed, payload } when used with showPopup method. - * @confirmed {Boolean} - * @payload {String} - */ - setup() { - super.setup(); - useListener('accept-input', this.confirm); - useListener('close-this-popup', this.cancel); - let startingBuffer = ''; - if (typeof this.props.startingValue === 'number' && this.props.startingValue > 0) { - startingBuffer = this.props.startingValue.toString().replace('.', this.decimalSeparator); - } - this.state = useState({ buffer: startingBuffer, toStartOver: this.props.isInputSelected }); - NumberBuffer.use({ - nonKeyboardInputEvent: 'numpad-click-input', - triggerAtEnter: 'accept-input', - triggerAtEscape: 'close-this-popup', - state: this.state, - }); +// formerly NumberPopupWidget +class NumberPopup extends AbstractAwaitablePopup { + /** + * @param {Object} props + * @param {Boolean} props.isPassword Show password popup. + * @param {number|null} props.startingValue Starting value of the popup. + * @param {Boolean} props.isInputSelected Input is highlighted and will reset upon a change. + * + * Resolve to { confirmed, payload } when used with showPopup method. + * @confirmed {Boolean} + * @payload {String} + */ + setup() { + super.setup(); + useListener("accept-input", this.confirm); + useListener("close-this-popup", this.cancel); + let startingBuffer = ""; + if (typeof this.props.startingValue === "number" && this.props.startingValue > 0) { + startingBuffer = this.props.startingValue + .toString() + .replace(".", this.decimalSeparator); } - get decimalSeparator() { - return this.env._t.database.parameters.decimal_point; + this.state = useState({ buffer: startingBuffer, toStartOver: this.props.isInputSelected }); + NumberBuffer.use({ + nonKeyboardInputEvent: "numpad-click-input", + triggerAtEnter: "accept-input", + triggerAtEscape: "close-this-popup", + state: this.state, + }); + } + get decimalSeparator() { + return this.env._t.database.parameters.decimal_point; + } + get inputBuffer() { + if (this.state.buffer === null) { + return ""; } - get inputBuffer() { - if (this.state.buffer === null) { - return ''; - } - if (this.props.isPassword) { - return this.state.buffer.replace(/./g, '•'); - } else { - return this.state.buffer; - } - } - confirm(event) { - if (NumberBuffer.get()) { - super.confirm(); - } - } - sendInput(key) { - this.trigger('numpad-click-input', { key }); - } - getPayload() { - return NumberBuffer.get(); + if (this.props.isPassword) { + return this.state.buffer.replace(/./g, "•"); + } else { + return this.state.buffer; } } - NumberPopup.template = 'NumberPopup'; - NumberPopup.defaultProps = { - confirmText: _t('Confirm'), - cancelText: _t('Discard'), - title: _t('Confirm ?'), - body: '', - cheap: false, - startingValue: null, - isPassword: false, - }; + confirm(event) { + if (NumberBuffer.get()) { + super.confirm(); + } + } + sendInput(key) { + this.trigger("numpad-click-input", { key }); + } + getPayload() { + 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); +Registries.Component.add(NumberPopup); - return 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 5da85994243..e8d878a720e 100644 --- a/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js @@ -1,30 +1,28 @@ -odoo.define('point_of_sale.OfflineErrorPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const ErrorPopup = require('point_of_sale.ErrorPopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - /** - * This is a special kind of error popup as it introduces - * an option to not show it again. - */ - class OfflineErrorPopup extends ErrorPopup { - dontShowAgain() { - this.constructor.dontShow = true; - this.cancel(); - } +/** + * This is a special kind of error popup as it introduces + * an option to not show it again. + */ +class OfflineErrorPopup extends ErrorPopup { + 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.'), - }; +} +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); +Registries.Component.add(OfflineErrorPopup); - return 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 951957819a5..aa16638428b 100644 --- a/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js @@ -1,28 +1,26 @@ -odoo.define('point_of_sale.OrderImportPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - // formerly OrderImportPopupWidget - class OrderImportPopup extends AbstractAwaitablePopup { - get unpaidSkipped() { - return ( - (this.props.report.unpaid_skipped_existing || 0) + - (this.props.report.unpaid_skipped_session || 0) - ); - } - getPayload() {} +// formerly OrderImportPopupWidget +class OrderImportPopup extends AbstractAwaitablePopup { + get unpaidSkipped() { + return ( + (this.props.report.unpaid_skipped_existing || 0) + + (this.props.report.unpaid_skipped_session || 0) + ); } - OrderImportPopup.template = 'OrderImportPopup'; - OrderImportPopup.defaultProps = { - confirmText: _lt('Ok'), - cancelKey: false, - body: '', - }; + getPayload() {} +} +OrderImportPopup.template = "OrderImportPopup"; +OrderImportPopup.defaultProps = { + confirmText: _lt("Ok"), + cancelKey: false, + body: "", +}; - Registries.Component.add(OrderImportPopup); +Registries.Component.add(OrderImportPopup); - return 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 dc47d155ab5..9e01e95f02e 100644 --- a/addons/point_of_sale/static/src/js/Popups/PosPopupController.js +++ b/addons/point_of_sale/static/src/js/Popups/PosPopupController.js @@ -1,123 +1,126 @@ -odoo.define('point_of_sale.PosPopupController', function(require) { - 'use strict'; +/** @odoo-module */ - const Registries = require('point_of_sale.Registries'); - const PosComponent = require('point_of_sale.PosComponent'); - const { useBus } = require('@web/core/utils/hooks'); +import Registries from "@point_of_sale/js/Registries"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import { useBus } from "@web/core/utils/hooks"; - /** - * This component is responsible in controlling the popups. It does so - * by coordinating with them thru the `env.posbus`. The basic steps follow: - * 1. `showPopup` method triggers `show-popup` event resulting to the - * mounting of the requested popup. - * 2. When the popup is shown, the `confirm`/`cancel` method of the popup - * will be called after the popup is used. `confirming`/`cancelling` - * will trigger the `close-popup`, which this component also listens to, - * resulting to closing of the popup. - * - * Furthermore, Pressing `confirmKey`/`cancelKey` which defaults to - * 'Enter'/'Escape', will automatically `confirm`/`cancel` the `topPopup`. - * This behavior is accomplished by listening to `keyup` event of the window. - * When the `confirmKey`/`cancelKey` of the `topPopup` is pressed, - * 'cancel-popup-{top-popup-id}'/'confirm-popup-{top-popup-id}' will be triggered - * and since the popup is listening to that event (@see AbstractAwaitablePopup), - * it will result to the call of `confirm`/`cancel` method. - * - * @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 { - setup() { - super.setup(); - useBus(this.env.posbus, 'show-popup', this._showPopup); - useBus(this.env.posbus, 'close-popup', this._closePopup); - owl.useExternalListener(window, 'keyup', this._onWindowKeyup); - this.popups = owl.useState([]); - } - _showPopup(event) { - let { id, name, 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}`, - }); - } - _closePopup(event) { - const { popupId, response } = event.detail; - const index = this.popups.findIndex((popup) => popup.props.id == popupId); - if (index != -1) { - const popup = this.popups[index]; - popup.props.resolve(response); - this.popups.splice(index, 1); - } - } - _onWindowKeyup(event) { - const eventIsFromInputField = event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA'; - const shouldHandleKey = this.topPopup && !eventIsFromInputField; - if (!shouldHandleKey) return; - - if (event.key === this.topPopup.props.cancelKey) { - this.env.posbus.trigger(`cancel-popup-${this.topPopup.props.id}`); - } else if (event.key === this.topPopup.props.confirmKey) { - 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 +/** + * This component is responsible in controlling the popups. It does so + * by coordinating with them thru the `env.posbus`. The basic steps follow: + * 1. `showPopup` method triggers `show-popup` event resulting to the + * mounting of the requested popup. + * 2. When the popup is shown, the `confirm`/`cancel` method of the popup + * will be called after the popup is used. `confirming`/`cancelling` + * will trigger the `close-popup`, which this component also listens to, + * resulting to closing of the popup. + * + * Furthermore, Pressing `confirmKey`/`cancelKey` which defaults to + * 'Enter'/'Escape', will automatically `confirm`/`cancel` the `topPopup`. + * This behavior is accomplished by listening to `keyup` event of the window. + * When the `confirmKey`/`cancelKey` of the `topPopup` is pressed, + * 'cancel-popup-{top-popup-id}'/'confirm-popup-{top-popup-id}' will be triggered + * and since the popup is listening to that event (@see AbstractAwaitablePopup), + * it will result to the call of `confirm`/`cancel` method. + * + * @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 { + setup() { + super.setup(); + useBus(this.env.posbus, "show-popup", this._showPopup); + useBus(this.env.posbus, "close-popup", this._closePopup); + owl.useExternalListener(window, "keyup", this._onWindowKeyup); + this.popups = owl.useState([]); + } + _showPopup(event) { + let { id, name, 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'.` ); } - /** - * @returns {boolean} Hide the element of this component when this returns false. - */ - isShown() { - return this.popups.length > 0; + if (component.dontShow) { + resolve(); + return; } - get topPopup() { - return this.popups[this.popups.length - 1]; - } - /** - * By default, only show the top popup. But always show a popup if - * `keepBehind` props is true. Meaning, if you have 2 popups, and - * the bottom popup has `keepBehind = true`, then the bottom popup - * will be visible if it's not blocked in the view by the top popup. - * - * @param {Popup} popup - * @returns {boolean} - */ - shouldShow(popup) { - return this.topPopup === popup || popup.props.keepBehind; + this.popups.push({ + name, + component, + props: this._constructPopupProps(component, props), + key: `${name}-${id}`, + }); + } + _closePopup(event) { + const { popupId, response } = event.detail; + const index = this.popups.findIndex((popup) => popup.props.id == popupId); + if (index != -1) { + const popup = this.popups[index]; + popup.props.resolve(response); + this.popups.splice(index, 1); } } - PosPopupController.template = 'point_of_sale.PosPopupController'; - Registries.Component.add(PosPopupController); + _onWindowKeyup(event) { + const eventIsFromInputField = + event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA"; + const shouldHandleKey = this.topPopup && !eventIsFromInputField; + if (!shouldHandleKey) { + return; + } - return PosPopupController; -}); + if (event.key === this.topPopup.props.cancelKey) { + this.env.posbus.trigger(`cancel-popup-${this.topPopup.props.id}`); + } else if (event.key === this.topPopup.props.confirmKey) { + 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. + */ + isShown() { + return this.popups.length > 0; + } + get topPopup() { + return this.popups[this.popups.length - 1]; + } + /** + * By default, only show the top popup. But always show a popup if + * `keepBehind` props is true. Meaning, if you have 2 popups, and + * the bottom popup has `keepBehind = true`, then the bottom popup + * will be visible if it's not blocked in the view by the top popup. + * + * @param {Popup} popup + * @returns {boolean} + */ + shouldShow(popup) { + 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 ab9928a1ae5..cb00c72f03f 100644 --- a/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js @@ -1,93 +1,84 @@ -odoo.define('point_of_sale.ProductConfiguratorPopup', function(require) { - 'use strict'; +/** @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"; - const PosComponent = require('point_of_sale.PosComponent'); - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); +const { useState, useSubEnv } = owl; - const { useState, useSubEnv } = owl; - - 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) => { - let { value, extra } = attribute_component.getValue(); - selected_attributes.push(value); - price_extra += extra; - }); - - return { - selected_attributes, - price_extra, - }; - } - } - ProductConfiguratorPopup.template = 'ProductConfiguratorPopup'; - Registries.Component.add(ProductConfiguratorPopup); - - class BaseProductAttribute extends PosComponent { - setup() { - super.setup(); - this.env.attribute_components.push(this); - - this.attribute = this.props.attribute; - this.values = this.attribute.values; - this.state = useState({ - selected_value: parseFloat(this.values[0].id), - custom_value: '', - }); - } - - getValue() { - let selected_value = this.values.find((val) => val.id === parseFloat(this.state.selected_value)); - let value = selected_value.name; - if (selected_value.is_custom && this.state.custom_value) { - value += `: ${this.state.custom_value}`; - } - - return { - value, - extra: selected_value.price_extra - }; - } +export class ProductConfiguratorPopup extends AbstractAwaitablePopup { + setup() { + super.setup(); + useSubEnv({ attribute_components: [] }); } - class RadioProductAttribute extends BaseProductAttribute { - setup() { - super.setup(); - owl.onMounted(this.onMounted); - } - onMounted() { - // With radio buttons `t-model` selects the default input by searching for inputs with - // a matching `value` attribute. In our case, we use `t-att-value` so `value` is - // not found yet and no radio is selected by default. - // We then manually select the first input of each radio attribute. - $(this.el).find('input[type="radio"]:first').prop('checked', true); - } + 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, + }; } - RadioProductAttribute.template = 'RadioProductAttribute'; - Registries.Component.add(RadioProductAttribute); +} +ProductConfiguratorPopup.template = "ProductConfiguratorPopup"; +Registries.Component.add(ProductConfiguratorPopup); - class SelectProductAttribute extends BaseProductAttribute { } - SelectProductAttribute.template = 'SelectProductAttribute'; - Registries.Component.add(SelectProductAttribute); +export class BaseProductAttribute extends PosComponent { + setup() { + super.setup(); + this.env.attribute_components.push(this); - class ColorProductAttribute extends BaseProductAttribute {} - ColorProductAttribute.template = 'ColorProductAttribute'; - Registries.Component.add(ColorProductAttribute); + this.attribute = this.props.attribute; + this.values = this.attribute.values; + this.state = useState({ + selected_value: parseFloat(this.values[0].id), + custom_value: "", + }); + } - return { - ProductConfiguratorPopup, - BaseProductAttribute, - RadioProductAttribute, - SelectProductAttribute, - ColorProductAttribute, - }; -}); + getValue() { + const selected_value = this.values.find( + (val) => val.id === parseFloat(this.state.selected_value) + ); + let value = selected_value.name; + if (selected_value.is_custom && this.state.custom_value) { + value += `: ${this.state.custom_value}`; + } + + return { + value, + extra: selected_value.price_extra, + }; + } +} + +export class RadioProductAttribute extends BaseProductAttribute { + setup() { + super.setup(); + owl.onMounted(this.onMounted); + } + onMounted() { + // With radio buttons `t-model` selects the default input by searching for inputs with + // a matching `value` attribute. In our case, we use `t-att-value` so `value` is + // not found yet and no radio is selected by default. + // We then manually select the first input of each radio attribute. + $(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 ColorProductAttribute extends BaseProductAttribute {} +ColorProductAttribute.template = "ColorProductAttribute"; +Registries.Component.add(ColorProductAttribute); 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 ac04fc4dc05..9e7b9ce05c3 100644 --- a/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js @@ -1,32 +1,31 @@ -odoo.define('point_of_sale.ProductInfoPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; - /** - * Props: - * { - * info: {object of data} - * } - */ - class ProductInfoPopup extends AbstractAwaitablePopup { - setup() { - super.setup(); - Object.assign(this, this.props.info); - } - searchProduct(productName) { - this.env.posbus.trigger('search-product-from-info-popup', productName); - this.cancel() - } - _hasMarginsCostsAccessRights() { - const isAccessibleToEveryUser = this.env.pos.config.is_margins_costs_accessible_to_every_user; - const isCashierManager = this.env.pos.get_cashier().role === 'manager'; - return isAccessibleToEveryUser || isCashierManager; - } +/** + * Props: + * { + * info: {object of data} + * } + */ +class ProductInfoPopup extends AbstractAwaitablePopup { + setup() { + super.setup(); + Object.assign(this, this.props.info); } + searchProduct(productName) { + this.env.posbus.trigger("search-product-from-info-popup", productName); + this.cancel(); + } + _hasMarginsCostsAccessRights() { + const isAccessibleToEveryUser = + this.env.pos.config.is_margins_costs_accessible_to_every_user; + const isCashierManager = this.env.pos.get_cashier().role === "manager"; + return isAccessibleToEveryUser || isCashierManager; + } +} - ProductInfoPopup.template = 'ProductInfoPopup'; - ProductInfoPopup.defaultProps= { confirmKey: false }; - Registries.Component.add(ProductInfoPopup); -}); +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 c74e0fc9c90..124b53ec395 100644 --- a/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js @@ -1,59 +1,57 @@ -odoo.define('point_of_sale.SelectionPopup', function (require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - const { useState } = owl; +const { useState } = owl; - // formerly SelectionPopupWidget - class SelectionPopup extends AbstractAwaitablePopup { - /** - * Value of the `item` key of the selected element in the Selection - * Array is the payload of this popup. - * - * @param {Object} props - * @param {String} [props.confirmText='Confirm'] - * @param {String} [props.cancelText='Cancel'] - * @param {String} [props.title='Select'] - * @param {String} [props.body=''] - * @param {Array} [props.list=[]] - * Selection { - * id: integer, - * label: string, - * isSelected: boolean, - * item: any, - * } - */ - setup() { - super.setup(); - this.state = useState({ selectedId: this.props.list.find((item) => item.isSelected) }); - } - selectItem(itemId) { - this.state.selectedId = itemId; - this.confirm(); - } - /** - * We send as payload of the response the selected item. - * - * @override - */ - getPayload() { - const selected = this.props.list.find((item) => this.state.selectedId === item.id); - return selected && selected.item; - } +// formerly SelectionPopupWidget +class SelectionPopup extends AbstractAwaitablePopup { + /** + * Value of the `item` key of the selected element in the Selection + * Array is the payload of this popup. + * + * @param {Object} props + * @param {String} [props.confirmText='Confirm'] + * @param {String} [props.cancelText='Cancel'] + * @param {String} [props.title='Select'] + * @param {String} [props.body=''] + * @param {Array} [props.list=[]] + * Selection { + * id: integer, + * label: string, + * isSelected: boolean, + * item: any, + * } + */ + setup() { + super.setup(); + this.state = useState({ selectedId: this.props.list.find((item) => item.isSelected) }); } - SelectionPopup.template = 'SelectionPopup'; - SelectionPopup.defaultProps = { - cancelText: _lt('Cancel'), - title: _lt('Select'), - body: '', - list: [], - confirmKey: false, - }; + selectItem(itemId) { + this.state.selectedId = itemId; + this.confirm(); + } + /** + * We send as payload of the response the selected item. + * + * @override + */ + getPayload() { + const selected = this.props.list.find((item) => this.state.selectedId === item.id); + return selected && selected.item; + } +} +SelectionPopup.template = "SelectionPopup"; +SelectionPopup.defaultProps = { + cancelText: _lt("Cancel"), + title: _lt("Select"), + body: "", + list: [], + confirmKey: false, +}; - Registries.Component.add(SelectionPopup); +Registries.Component.add(SelectionPopup); - return 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 10e1efe376a..42123d93933 100644 --- a/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js @@ -1,42 +1,40 @@ -odoo.define('point_of_sale.TextAreaPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - const { onMounted, useRef, useState } = owl; +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 { - /** - * @param {Object} props - * @param {string} props.startingValue - */ - setup() { - super.setup(); - this.state = useState({ inputValue: this.props.startingValue }); - this.inputRef = useRef('input'); - onMounted(this.onMounted); - } - onMounted() { - this.inputRef.el.focus(); - } - getPayload() { - return this.state.inputValue; - } +// formerly TextAreaPopupWidget +// IMPROVEMENT: This code is very similar to TextInputPopup. +// Combining them would reduce the code. +class TextAreaPopup extends AbstractAwaitablePopup { + /** + * @param {Object} props + * @param {string} props.startingValue + */ + setup() { + super.setup(); + this.state = useState({ inputValue: this.props.startingValue }); + this.inputRef = useRef("input"); + onMounted(this.onMounted); } - TextAreaPopup.template = 'TextAreaPopup'; - TextAreaPopup.defaultProps = { - confirmText: _lt('Add'), - cancelText: _lt('Discard'), - title: '', - body: '', - }; + onMounted() { + this.inputRef.el.focus(); + } + getPayload() { + return this.state.inputValue; + } +} +TextAreaPopup.template = "TextAreaPopup"; +TextAreaPopup.defaultProps = { + confirmText: _lt("Add"), + cancelText: _lt("Discard"), + title: "", + body: "", +}; - Registries.Component.add(TextAreaPopup); +Registries.Component.add(TextAreaPopup); - return 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 d7188c69f60..26aa028f40d 100644 --- a/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js @@ -1,38 +1,36 @@ -odoo.define('point_of_sale.TextInputPopup', function(require) { - 'use strict'; +/** @odoo-module */ - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const Registries = require('point_of_sale.Registries'); - const { _lt } = require('@web/core/l10n/translation'); +import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import Registries from "@point_of_sale/js/Registries"; +import { _lt } from "@web/core/l10n/translation"; - const { onMounted, useRef, useState } = owl; +const { onMounted, useRef, useState } = owl; - // formerly TextInputPopupWidget - class TextInputPopup extends AbstractAwaitablePopup { - setup() { - super.setup(); - this.state = useState({ inputValue: this.props.startingValue }); - this.inputRef = useRef('input'); - onMounted(this.onMounted); - } - onMounted() { - this.inputRef.el.focus(); - } - getPayload() { - return this.state.inputValue; - } +// formerly TextInputPopupWidget +class TextInputPopup extends AbstractAwaitablePopup { + setup() { + super.setup(); + this.state = useState({ inputValue: this.props.startingValue }); + this.inputRef = useRef("input"); + onMounted(this.onMounted); } - TextInputPopup.template = 'TextInputPopup'; - TextInputPopup.defaultProps = { - confirmText: _lt('Confirm'), - cancelText: _lt('Discard'), - title: '', - body: '', - startingValue: '', - placeholder: '', - }; + onMounted() { + this.inputRef.el.focus(); + } + getPayload() { + return this.state.inputValue; + } +} +TextInputPopup.template = "TextInputPopup"; +TextInputPopup.defaultProps = { + confirmText: _lt("Confirm"), + cancelText: _lt("Discard"), + title: "", + body: "", + startingValue: "", + placeholder: "", +}; - Registries.Component.add(TextInputPopup); +Registries.Component.add(TextInputPopup); - return 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 2f969a687ad..26b5fc3fbe1 100644 --- a/addons/point_of_sale/static/src/js/PosComponent.js +++ b/addons/point_of_sale/static/src/js/PosComponent.js @@ -1,75 +1,73 @@ -odoo.define('point_of_sale.PosComponent', function (require) { - 'use strict'; +/** @odoo-module */ - const { LegacyComponent } = require("@web/legacy/legacy_component"); - const { onRendered } = owl; +import { LegacyComponent } from "@web/legacy/legacy_component"; +const { onRendered } = owl; - let nextId = 0; +let nextId = 0; - class PosComponent extends LegacyComponent { - setup() { - onRendered(() => { - if (this.env.isDebug()) { - console.log('Rendered:', this.constructor.name); - } - }); - } - /** - * This function is available to all Components that inherit this class. - * The goal of this function is to show an awaitable dialog (popup) that - * returns a response after user interaction. See the following for quick - * demonstration: - * - * ``` - * async getUserName() { - * 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`. - * console.log(userResponse); // logs { confirmed: true, payload: } - * } - * ``` - * - * @param {String} name Name of the popup component - * @param {Object} props Object that will be used to render to popup - */ - showPopup(name, props) { - return new Promise((resolve) => { - this.env.posbus.trigger('show-popup', { name, props, resolve, id: nextId++ }); - }); - } - showTempScreen(name, props) { - return new Promise((resolve) => { - this.trigger('show-temp-screen', { name, props, resolve }); - }); - } - showScreen(name, props) { - this.trigger('show-main-screen', { name, props }); - } - /** - * @param {String} name 'bell' | 'error' - */ - playSound(name) { - this.trigger('play-sound', name); - } - /** - * Control the SyncNotification component. - * @param {String} status 'connected' | 'connecting' | 'disconnected' | 'error' - * @param {String} pending number of pending orders to sync - */ - setSyncStatus(status, pending) { - this.trigger('set-sync-status', { status, pending }); - } - showNotification(message, duration = 2000) { - this.trigger('show-notification', { message, duration }); - } - closeNotification() { - this.trigger('close-notification'); - } +class PosComponent extends LegacyComponent { + setup() { + onRendered(() => { + if (this.env.isDebug()) { + console.log("Rendered:", this.constructor.name); + } + }); } + /** + * This function is available to all Components that inherit this class. + * The goal of this function is to show an awaitable dialog (popup) that + * returns a response after user interaction. See the following for quick + * demonstration: + * + * ``` + * async getUserName() { + * 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`. + * console.log(userResponse); // logs { confirmed: true, payload: } + * } + * ``` + * + * @param {String} name Name of the popup component + * @param {Object} props Object that will be used to render to popup + */ + showPopup(name, props) { + return new Promise((resolve) => { + this.env.posbus.trigger("show-popup", { name, props, resolve, id: nextId++ }); + }); + } + showTempScreen(name, props) { + return new Promise((resolve) => { + this.trigger("show-temp-screen", { name, props, resolve }); + }); + } + showScreen(name, props) { + this.trigger("show-main-screen", { name, props }); + } + /** + * @param {String} name 'bell' | 'error' + */ + playSound(name) { + this.trigger("play-sound", name); + } + /** + * Control the SyncNotification component. + * @param {String} status 'connected' | 'connecting' | 'disconnected' | 'error' + * @param {String} pending number of pending orders to sync + */ + setSyncStatus(status, pending) { + this.trigger("set-sync-status", { status, pending }); + } + showNotification(message, duration = 2000) { + this.trigger("show-notification", { message, duration }); + } + closeNotification() { + this.trigger("close-notification"); + } +} - return PosComponent; -}); +export default PosComponent; diff --git a/addons/point_of_sale/static/src/js/PosContext.js b/addons/point_of_sale/static/src/js/PosContext.js index cf6ce355160..677ebdad04e 100644 --- a/addons/point_of_sale/static/src/js/PosContext.js +++ b/addons/point_of_sale/static/src/js/PosContext.js @@ -1,10 +1,6 @@ -odoo.define('point_of_sale.PosContext', function (require) { - 'use strict'; - const { reactive } = owl; +/** @odoo-module */ +const { reactive } = owl; - // Create global context objects - // e.g. component.env.device = new Context({ isMobile: false }); - return { - orderManagement: reactive({ searchString: '', selectedOrder: null }), - }; -}); +// Create global context objects +// e.g. component.env.device = new Context({ isMobile: false }); +export const orderManagement = reactive({ searchString: "", selectedOrder: null }); diff --git a/addons/point_of_sale/static/src/js/Registries.js b/addons/point_of_sale/static/src/js/Registries.js index e88d23d6c57..b55a3424f9a 100644 --- a/addons/point_of_sale/static/src/js/Registries.js +++ b/addons/point_of_sale/static/src/js/Registries.js @@ -1,27 +1,27 @@ -odoo.define('point_of_sale.Registries', function(require) { - 'use strict'; +/** @odoo-module */ +/** + * This definition contains all the instances of ClassRegistry. + */ - /** - * This definition contains all the instances of ClassRegistry. - */ +import ComponentRegistry from "@point_of_sale/js/ComponentRegistry"; +import ClassRegistry from "@point_of_sale/js/ClassRegistry"; - const ComponentRegistry = require('point_of_sale.ComponentRegistry'); - const ClassRegistry = require('point_of_sale.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); - } - } +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); + }; } +} - return { Component: new ComponentRegistry(), Model: new ModelRegistry() }; -}); +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 78403b1721c..24e969f0604 100644 --- a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js +++ b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js @@ -1,135 +1,131 @@ -odoo.define('point_of_sale.PartnerDetailsEdit', function(require) { - 'use strict'; +/** @odoo-module */ - const { _t } = require('web.core'); - const { getDataURLFromFile } = require('web.utils'); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +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"; - const { onMounted, onWillUnmount } = owl; +const { onMounted, onWillUnmount } = owl; - class PartnerDetailsEdit extends PosComponent { - setup() { - super.setup(); - this.intFields = ['country_id', 'state_id', 'property_product_pricelist']; - const partner = this.props.partner; - this.changes = { - 'country_id': partner.country_id && partner.country_id[0], - 'state_id': partner.state_id && partner.state_id[0], - }; - if (!partner.property_product_pricelist) - this.changes['property_product_pricelist'] = this.env.pos.default_pricelist.id; - - onMounted(() => { - this.env.bus.on('save-partner', this, this.saveChanges); - }); - - onWillUnmount(() => { - this.env.bus.off('save-partner', this); - }); +class PartnerDetailsEdit extends PosComponent { + setup() { + super.setup(); + this.intFields = ["country_id", "state_id", "property_product_pricelist"]; + const partner = this.props.partner; + this.changes = { + country_id: partner.country_id && partner.country_id[0], + state_id: partner.state_id && partner.state_id[0], + }; + if (!partner.property_product_pricelist) { + this.changes["property_product_pricelist"] = this.env.pos.default_pricelist.id; } - get partnerImageUrl() { - // We prioritize image_1920 in the `changes` field because we want - // to show the uploaded image without fetching new data from the server. - const partner = this.props.partner; - if (this.changes.image_1920) { - return this.changes.image_1920; - } else if (partner.id) { - return `/web/image?model=res.partner&id=${partner.id}&field=avatar_128&unique=${partner.write_date}`; - } else { - return false; - } - } - /** - * Save to field `changes` all input changes from the form fields. - */ - captureChange(event) { - this.changes[event.target.name] = event.target.value; - } - saveChanges() { - let processedChanges = {}; - for (let [key, value] of Object.entries(this.changes)) { - if (this.intFields.includes(key)) { - processedChanges[key] = parseInt(value) || false; - } else { - processedChanges[key] = value; - } - } - if ((!this.props.partner.name && !processedChanges.name) || - processedChanges.name === '' ){ - return this.showPopup('ErrorPopup', { - title: _t('A Customer Name Is Required'), - }); - } - processedChanges.id = this.props.partner.id || false; - this.trigger('save-changes', { processedChanges }); - } - async uploadImage(event) { - const file = event.target.files[0]; - if (!file.type.match(/image.*/)) { - 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.' - ), - }); - } else { - const imageUrl = await getDataURLFromFile(file); - const loadedImage = await this._loadImage(imageUrl); - if (loadedImage) { - const resizedImage = await this._resizeImage(loadedImage, 800, 600); - this.changes.image_1920 = resizedImage.toDataURL(); - // Rerender to reflect the changes in the screen - this.render(true); - } - } - } - _resizeImage(img, maxwidth, maxheight) { - var canvas = document.createElement('canvas'); - var ctx = canvas.getContext('2d'); - var ratio = 1; - if (img.width > maxwidth) { - ratio = maxwidth / img.width; - } - if (img.height * ratio > maxheight) { - ratio = maxheight / img.height; - } - var width = Math.floor(img.width * ratio); - var height = Math.floor(img.height * ratio); + onMounted(() => { + this.env.bus.on("save-partner", this, this.saveChanges); + }); - canvas.width = width; - canvas.height = height; - ctx.drawImage(img, 0, 0, width, height); - return canvas; - } - /** - * Loading image is converted to a Promise to allow await when - * loading an image. It resolves to the loaded image if succesful, - * else, resolves to false. - * - * [Source](https://stackoverflow.com/questions/45788934/how-to-turn-this-callback-into-a-promise-using-async-await) - */ - _loadImage(url) { - return new Promise((resolve) => { - const img = new Image(); - img.addEventListener('load', () => resolve(img)); - img.addEventListener('error', () => { - this.showPopup('ErrorPopup', { - title: this.env._t('Loading Image Error'), - body: this.env._t( - 'Encountered error when loading image. Please try again.' - ), - }); - resolve(false); - }); - img.src = url; - }); + onWillUnmount(() => { + this.env.bus.off("save-partner", this); + }); + } + get partnerImageUrl() { + // We prioritize image_1920 in the `changes` field because we want + // to show the uploaded image without fetching new data from the server. + const partner = this.props.partner; + if (this.changes.image_1920) { + return this.changes.image_1920; + } else if (partner.id) { + return `/web/image?model=res.partner&id=${partner.id}&field=avatar_128&unique=${partner.write_date}`; + } else { + return false; } } - PartnerDetailsEdit.template = 'PartnerDetailsEdit'; + /** + * Save to field `changes` all input changes from the form fields. + */ + captureChange(event) { + this.changes[event.target.name] = event.target.value; + } + saveChanges() { + const processedChanges = {}; + for (const [key, value] of Object.entries(this.changes)) { + if (this.intFields.includes(key)) { + processedChanges[key] = parseInt(value) || false; + } else { + processedChanges[key] = value; + } + } + if ((!this.props.partner.name && !processedChanges.name) || processedChanges.name === "") { + return this.showPopup("ErrorPopup", { + title: _t("A Customer Name Is Required"), + }); + } + processedChanges.id = this.props.partner.id || false; + this.trigger("save-changes", { processedChanges }); + } + async uploadImage(event) { + const file = event.target.files[0]; + if (!file.type.match(/image.*/)) { + 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." + ), + }); + } else { + const imageUrl = await getDataURLFromFile(file); + const loadedImage = await this._loadImage(imageUrl); + if (loadedImage) { + const resizedImage = await this._resizeImage(loadedImage, 800, 600); + this.changes.image_1920 = resizedImage.toDataURL(); + // Rerender to reflect the changes in the screen + this.render(true); + } + } + } + _resizeImage(img, maxwidth, maxheight) { + var canvas = document.createElement("canvas"); + var ctx = canvas.getContext("2d"); + var ratio = 1; - Registries.Component.add(PartnerDetailsEdit); + if (img.width > maxwidth) { + ratio = maxwidth / img.width; + } + if (img.height * ratio > maxheight) { + ratio = maxheight / img.height; + } + var width = Math.floor(img.width * ratio); + var height = Math.floor(img.height * ratio); - return PartnerDetailsEdit; -}); + canvas.width = width; + canvas.height = height; + ctx.drawImage(img, 0, 0, width, height); + return canvas; + } + /** + * Loading image is converted to a Promise to allow await when + * loading an image. It resolves to the loaded image if succesful, + * else, resolves to false. + * + * [Source](https://stackoverflow.com/questions/45788934/how-to-turn-this-callback-into-a-promise-using-async-await) + */ + _loadImage(url) { + return new Promise((resolve) => { + const img = new Image(); + img.addEventListener("load", () => resolve(img)); + img.addEventListener("error", () => { + this.showPopup("ErrorPopup", { + title: this.env._t("Loading Image Error"), + body: this.env._t("Encountered error when loading image. Please try again."), + }); + resolve(false); + }); + img.src = url; + }); + } +} +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 17a01a548ad..cd45d0e4660 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,24 +1,22 @@ -odoo.define('point_of_sale.PartnerLine', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class PartnerLine extends PosComponent { - get highlight() { - return this._isPartnerSelected ? 'highlight' : ''; - } - get shortAddress() { - const { partner } = this.props; - return [partner.zip, partner.city, partner.state_id[1]].filter(field => field).join(', '); - } - get _isPartnerSelected() { - return this.props.partner === this.props.selectedPartner; - } +class PartnerLine extends PosComponent { + get highlight() { + return this._isPartnerSelected ? "highlight" : ""; } - PartnerLine.template = 'PartnerLine'; + get shortAddress() { + const { partner } = this.props; + return [partner.zip, partner.city, partner.state_id[1]].filter((field) => field).join(", "); + } + get _isPartnerSelected() { + return this.props.partner === this.props.selectedPartner; + } +} +PartnerLine.template = "PartnerLine"; - Registries.Component.add(PartnerLine); +Registries.Component.add(PartnerLine); - return 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 dd16a666d81..fc4a87ac6f2 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,216 +1,228 @@ -odoo.define('point_of_sale.PartnerListScreen', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); - const { isConnectionError } = require('point_of_sale.utils'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; +import { isConnectionError } from "@point_of_sale/js/utils"; - const { debounce } = require("@web/core/utils/timing"); - const { useListener } = require("@web/core/utils/hooks"); +import { debounce } from "@web/core/utils/timing"; +import { useListener } from "@web/core/utils/hooks"; - const { onWillUnmount, useRef, onMounted } = owl; +const { onWillUnmount, useRef, onMounted } = owl; - /** - * Render this screen using `showTempScreen` to select partner. - * When the shown screen is confirmed ('Set Customer' or 'Deselect Customer' - * button is clicked), the call to `showTempScreen` resolves to the - * selected partner. E.g. - * - * ```js - * const { confirmed, payload: selectedPartner } = await showTempScreen('PartnerListScreen'); - * if (confirmed) { - * // do something with the selectedPartner - * } - * ``` - * - * @props partner - originally selected partner - */ - class PartnerListScreen extends PosComponent { - setup() { - super.setup(); - useListener('click-save', () => this.env.bus.trigger('save-partner')); - useListener('save-changes', this.saveChanges); - this.searchWordInputRef = useRef('search-word-input-partner'); +/** + * Render this screen using `showTempScreen` to select partner. + * When the shown screen is confirmed ('Set Customer' or 'Deselect Customer' + * button is clicked), the call to `showTempScreen` resolves to the + * selected partner. E.g. + * + * ```js + * const { confirmed, payload: selectedPartner } = await showTempScreen('PartnerListScreen'); + * if (confirmed) { + * // do something with the selectedPartner + * } + * ``` + * + * @props partner - originally selected partner + */ +class PartnerListScreen extends PosComponent { + setup() { + super.setup(); + useListener("click-save", () => this.env.bus.trigger("save-partner")); + useListener("save-changes", this.saveChanges); + this.searchWordInputRef = useRef("search-word-input-partner"); - // We are not using useState here because the object - // passed to useState converts the object and its contents - // to Observer proxy. Not sure of the side-effects of making - // a persistent object, such as pos, into Observer. But it - // is better to be safe. - this.state = { - query: null, - selectedPartner: this.props.partner, - detailIsShown: false, - editModeProps: { - partner: null, - }, - }; - this.updatePartnerList = debounce(this.updatePartnerList, 70); - onWillUnmount(this.updatePartnerList.cancel); - onMounted(() => { - if(!this.env.pos.config.limited_partners_loading) - this.env.pos.isEveryPartnerLoaded = true; - }); - - } - // Lifecycle hooks - back() { - if(this.state.detailIsShown) { - this.state.detailIsShown = false; - this.render(true); - } else { - this.props.resolve({ confirmed: false, payload: false }); - this.trigger('close-temp-screen'); + // We are not using useState here because the object + // passed to useState converts the object and its contents + // to Observer proxy. Not sure of the side-effects of making + // a persistent object, such as pos, into Observer. But it + // is better to be safe. + this.state = { + query: null, + selectedPartner: this.props.partner, + detailIsShown: false, + editModeProps: { + partner: null, + }, + }; + this.updatePartnerList = debounce(this.updatePartnerList, 70); + onWillUnmount(this.updatePartnerList.cancel); + onMounted(() => { + if (!this.env.pos.config.limited_partners_loading) { + this.env.pos.isEveryPartnerLoaded = true; } - } - confirm() { - this.props.resolve({ confirmed: true, payload: this.state.selectedPartner }); - this.trigger('close-temp-screen'); - } - activateEditMode() { - this.state.detailIsShown = true; + }); + } + // Lifecycle hooks + back() { + if (this.state.detailIsShown) { + this.state.detailIsShown = false; this.render(true); - } - // Getters - - get currentOrder() { - return this.env.pos.get_order(); - } - - get partners() { - let res; - if (this.state.query && this.state.query.trim() !== '') { - res = this.env.pos.db.search_partner(this.state.query.trim()); - } else { - res = this.env.pos.db.get_partners_sorted(1000); - } - res.sort(function (a, b) { return (a.name || '').localeCompare(b.name || '') }); - // the selected partner (if any) is displayed at the top of the list - if (this.state.selectedPartner) { - let indexOfSelectedPartner = res.findIndex( partner => - partner.id === this.state.selectedPartner.id - ); - if (indexOfSelectedPartner !== -1) { - res.splice(indexOfSelectedPartner, 1); - res.unshift(this.state.selectedPartner); - } - } - return res - } - get isBalanceDisplayed() { - return false; - } - get partnerLink() { - return `/web#model=res.partner&id=${this.state.editModeProps.partner.id}`; - } - - // Methods - - async _onPressEnterKey() { - if (!this.state.query) return; - if (!this.env.pos.isEveryPartnerLoaded) { - const result = await this.searchPartner(); - this.showNotification( - _.str.sprintf(this.env._t('%s customer(s) found for "%s".'), - result.length, - this.state.query) - , 3000); - if(!result.length) this._clearSearch(); - } - } - _clearSearch() { - this.searchWordInputRef.el.value = ''; - this.state.query = ''; - this.render(true); - } - // We declare this event handler as a debounce function in - // order to lower its trigger rate. - async updatePartnerList(event) { - this.state.query = event.target.value; - if (event.code === 'Enter') { - this._onPressEnterKey(); - } else { - this.render(true); - } - } - clickPartner(partner) { - if (this.state.selectedPartner && this.state.selectedPartner.id === partner.id) { - this.state.selectedPartner = null; - } else { - this.state.selectedPartner = partner; - } - this.confirm(); - } - editPartner(partner) { - this.state.editModeProps.partner = partner; - this.activateEditMode(); - } - createPartner() { - // initialize the edit screen with default details about country & state - this.state.editModeProps.partner = { - country_id: this.env.pos.company.country_id, - state_id: this.env.pos.company.state_id, - } - this.activateEditMode(); - } - async saveChanges(event) { - try { - let partnerId = await this.rpc({ - model: 'res.partner', - method: 'create_from_ui', - args: [event.detail.processedChanges], - }); - await this.env.pos.load_new_partners(); - this.state.selectedPartner = this.env.pos.db.get_partner_by_id(partnerId); - this.confirm(); - } catch (error) { - if (isConnectionError(error)) { - await this.showPopup('OfflineErrorPopup', { - title: this.env._t('Offline'), - body: this.env._t('Unable to save changes.'), - }); - } else { - throw error; - } - } - } - async searchPartner() { - let result = await this.getNewPartners(); - this.env.pos.addPartners(result); - if (!this.env.pos.isEveryPartnerLoaded) await this.env.pos.updateIsEveryPartnerLoaded(); - this.render(true); - return result; - } - async getNewPartners() { - let domain = []; - if(this.state.query) { - domain = ['|', ["name", "ilike", this.state.query + "%"], - ["parent_name", "ilike", this.state.query + "%"]]; - } - const result = await this.env.services.rpc( - { - model: 'pos.session', - method: 'get_pos_ui_res_partner_by_params', - args: [ - [odoo.pos_session_id], - { - domain, - limit: 10, - }, - ], - context: this.env.session.user_context, - }, - { - timeout: 3000, - shadow: true, - } - ); - return result; + } else { + this.props.resolve({ confirmed: false, payload: false }); + this.trigger("close-temp-screen"); } } - PartnerListScreen.template = 'PartnerListScreen'; + confirm() { + this.props.resolve({ confirmed: true, payload: this.state.selectedPartner }); + this.trigger("close-temp-screen"); + } + activateEditMode() { + this.state.detailIsShown = true; + this.render(true); + } + // Getters - Registries.Component.add(PartnerListScreen); + get currentOrder() { + return this.env.pos.get_order(); + } - return PartnerListScreen; -}); + get partners() { + let res; + if (this.state.query && this.state.query.trim() !== "") { + res = this.env.pos.db.search_partner(this.state.query.trim()); + } else { + res = this.env.pos.db.get_partners_sorted(1000); + } + res.sort(function (a, b) { + return (a.name || "").localeCompare(b.name || ""); + }); + // the selected partner (if any) is displayed at the top of the list + if (this.state.selectedPartner) { + const indexOfSelectedPartner = res.findIndex( + (partner) => partner.id === this.state.selectedPartner.id + ); + if (indexOfSelectedPartner !== -1) { + res.splice(indexOfSelectedPartner, 1); + res.unshift(this.state.selectedPartner); + } + } + return res; + } + get isBalanceDisplayed() { + return false; + } + get partnerLink() { + return `/web#model=res.partner&id=${this.state.editModeProps.partner.id}`; + } + + // Methods + + async _onPressEnterKey() { + if (!this.state.query) { + return; + } + if (!this.env.pos.isEveryPartnerLoaded) { + const result = await this.searchPartner(); + this.showNotification( + _.str.sprintf( + this.env._t('%s customer(s) found for "%s".'), + result.length, + this.state.query + ), + 3000 + ); + if (!result.length) { + this._clearSearch(); + } + } + } + _clearSearch() { + this.searchWordInputRef.el.value = ""; + this.state.query = ""; + this.render(true); + } + // We declare this event handler as a debounce function in + // order to lower its trigger rate. + async updatePartnerList(event) { + this.state.query = event.target.value; + if (event.code === "Enter") { + this._onPressEnterKey(); + } else { + this.render(true); + } + } + clickPartner(partner) { + if (this.state.selectedPartner && this.state.selectedPartner.id === partner.id) { + this.state.selectedPartner = null; + } else { + this.state.selectedPartner = partner; + } + this.confirm(); + } + editPartner(partner) { + this.state.editModeProps.partner = partner; + this.activateEditMode(); + } + createPartner() { + // initialize the edit screen with default details about country & state + this.state.editModeProps.partner = { + country_id: this.env.pos.company.country_id, + state_id: this.env.pos.company.state_id, + }; + this.activateEditMode(); + } + async saveChanges(event) { + try { + const partnerId = await this.rpc({ + model: "res.partner", + method: "create_from_ui", + args: [event.detail.processedChanges], + }); + await this.env.pos.load_new_partners(); + this.state.selectedPartner = this.env.pos.db.get_partner_by_id(partnerId); + this.confirm(); + } catch (error) { + if (isConnectionError(error)) { + await this.showPopup("OfflineErrorPopup", { + title: this.env._t("Offline"), + body: this.env._t("Unable to save changes."), + }); + } else { + throw error; + } + } + } + async searchPartner() { + const result = await this.getNewPartners(); + this.env.pos.addPartners(result); + if (!this.env.pos.isEveryPartnerLoaded) { + await this.env.pos.updateIsEveryPartnerLoaded(); + } + this.render(true); + return result; + } + async getNewPartners() { + let domain = []; + if (this.state.query) { + domain = [ + "|", + ["name", "ilike", this.state.query + "%"], + ["parent_name", "ilike", this.state.query + "%"], + ]; + } + const result = await this.env.services.rpc( + { + model: "pos.session", + method: "get_pos_ui_res_partner_by_params", + args: [ + [odoo.pos_session_id], + { + domain, + limit: 10, + }, + ], + context: this.env.session.user_context, + }, + { + timeout: 3000, + shadow: true, + } + ); + return result; + } +} +PartnerListScreen.template = "PartnerListScreen"; + +Registries.Component.add(PartnerListScreen); + +export default 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 b5dc6a7bd54..c5146c7146e 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,17 +1,15 @@ -odoo.define('point_of_sale.PSNumpadInputButton', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class PSNumpadInputButton extends PosComponent { - get _class() { - return this.props.changeClassTo || 'input-button number-char'; - } +class PSNumpadInputButton extends PosComponent { + get _class() { + return this.props.changeClassTo || "input-button number-char"; } - PSNumpadInputButton.template = 'PSNumpadInputButton'; +} +PSNumpadInputButton.template = "PSNumpadInputButton"; - Registries.Component.add(PSNumpadInputButton); +Registries.Component.add(PSNumpadInputButton); - return 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 2827ad28086..01e28665cb9 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,456 +1,499 @@ -odoo.define('point_of_sale.PaymentScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const { parse } = require('web.field_utils'); - const PosComponent = require('point_of_sale.PosComponent'); - const { useErrorHandlers } = require('point_of_sale.custom_hooks'); - const NumberBuffer = require('point_of_sale.NumberBuffer'); - const { useListener } = require("@web/core/utils/hooks"); - const Registries = require('point_of_sale.Registries'); - const { isConnectionError } = require('point_of_sale.utils'); - const utils = require('web.utils'); +import { parse } from "web.field_utils"; +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 { useListener } from "@web/core/utils/hooks"; +import Registries from "@point_of_sale/js/Registries"; +import { isConnectionError } from "@point_of_sale/js/utils"; +import utils from "web.utils"; - class PaymentScreen extends PosComponent { - setup() { - super.setup(); - useListener('delete-payment-line', this.deletePaymentLine); - useListener('select-payment-line', this.selectPaymentLine); - useListener('new-payment-line', this.addNewPaymentLine); - useListener('update-selected-paymentline', this._updateSelectedPaymentline); - useListener('send-payment-request', this._sendPaymentRequest); - useListener('send-payment-cancel', this._sendPaymentCancel); - useListener('send-payment-reverse', this._sendPaymentReverse); - useListener('send-force-done', this._sendForceDone); - useListener('validate-order', () => this.validateOrder(false)); - 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); - useErrorHandlers(); - this.payment_interface = null; - this.error = false; +class PaymentScreen extends PosComponent { + setup() { + super.setup(); + useListener("delete-payment-line", this.deletePaymentLine); + useListener("select-payment-line", this.selectPaymentLine); + useListener("new-payment-line", this.addNewPaymentLine); + useListener("update-selected-paymentline", this._updateSelectedPaymentline); + useListener("send-payment-request", this._sendPaymentRequest); + useListener("send-payment-cancel", this._sendPaymentCancel); + useListener("send-payment-reverse", this._sendPaymentReverse); + useListener("send-force-done", this._sendForceDone); + useListener("validate-order", () => this.validateOrder(false)); + 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); + useErrorHandlers(); + this.payment_interface = null; + this.error = false; + } + + showMaxValueError() { + 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." + ), + }); + } + get _getNumberBufferConfig() { + const config = { + // The numberBuffer listens to this event to update its state. + // Basically means 'update the buffer when this event is triggered' + nonKeyboardInputEvent: "input-from-numpad", + // When the buffer is updated, trigger this event. + // Note that the component listens to it. + triggerAtInput: "update-selected-paymentline", + }; + // Check if pos has a cash payment method + const hasCashPaymentMethod = this.payment_methods_from_config.some( + (method) => method.type === "cash" + ); + + if (!hasCashPaymentMethod) { + config["maxValue"] = this.currentOrder.get_due(); + config["maxValueReached"] = this.showMaxValueError.bind(this); } - showMaxValueError() { - 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.') + return config; + } + get currentOrder() { + return this.env.pos.get_order(); + } + get paymentLines() { + return this.currentOrder.get_paymentlines(); + } + get selectedPaymentLine() { + return this.currentOrder.selected_paymentline; + } + async selectPartner() { + // IMPROVEMENT: This code snippet is repeated multiple times. + // Maybe it's better to create a function for it. + const currentPartner = this.currentOrder.get_partner(); + const { confirmed, payload: newPartner } = await this.showTempScreen("PartnerListScreen", { + partner: currentPartner, + }); + if (confirmed) { + this.currentOrder.set_partner(newPartner); + this.currentOrder.updatePricelist(newPartner); + } + } + addNewPaymentLine({ detail: paymentMethod }) { + // original function: click_paymentmethods + const result = this.currentOrder.add_paymentline(paymentMethod); + if (result) { + NumberBuffer.reset(); + return true; + } else { + this.showPopup("ErrorPopup", { + title: this.env._t("Error"), + body: this.env._t("There is already an electronic payment in progress."), }); + return false; } - get _getNumberBufferConfig() { - let config = { - // The numberBuffer listens to this event to update its state. - // Basically means 'update the buffer when this event is triggered' - nonKeyboardInputEvent: 'input-from-numpad', - // When the buffer is updated, trigger this event. - // Note that the component listens to it. - triggerAtInput: 'update-selected-paymentline', - }; - // Check if pos has a cash payment method - const hasCashPaymentMethod = this.payment_methods_from_config.some( - (method) => method.type === 'cash' - ); + } + _updateSelectedPaymentline() { + if (this.paymentLines.every((line) => line.paid)) { + this.currentOrder.add_paymentline(this.payment_methods_from_config[0]); + } + if (!this.selectedPaymentLine) { + return; + } // do nothing if no selected payment line + // disable changing amount on paymentlines with running or done payments on a payment terminal + const payment_terminal = this.selectedPaymentLine.payment_method.payment_terminal; + if ( + payment_terminal && + !["pending", "retry"].includes(this.selectedPaymentLine.get_payment_status()) + ) { + return; + } + if (NumberBuffer.get() === null) { + this.deletePaymentLine({ detail: { cid: this.selectedPaymentLine.cid } }); + } else { + this.selectedPaymentLine.set_amount(NumberBuffer.getFloat()); + } + } + toggleIsToInvoice() { + // click_invoice + this.currentOrder.set_to_invoice(!this.currentOrder.is_to_invoice()); + this.render(true); + } + openCashbox() { + this.env.proxy.printer.open_cashbox(); + } + async addTip() { + // click_tip + const tip = this.currentOrder.get_tip(); + const change = this.currentOrder.get_change(); + const value = tip === 0 && change > 0 ? change : tip; - if (!hasCashPaymentMethod) { - config['maxValue'] = this.currentOrder.get_due(); - config['maxValueReached'] = this.showMaxValueError.bind(this); - } + const { confirmed, payload } = await this.showPopup("NumberPopup", { + title: tip ? this.env._t("Change Tip") : this.env._t("Add Tip"), + startingValue: value, + isInputSelected: true, + }); - return config; + if (confirmed) { + this.currentOrder.set_tip(parse.float(payload)); } - get currentOrder() { - return this.env.pos.get_order(); - } - get paymentLines() { - return this.currentOrder.get_paymentlines(); - } - get selectedPaymentLine() { - return this.currentOrder.selected_paymentline; - } - async selectPartner() { - // IMPROVEMENT: This code snippet is repeated multiple times. - // Maybe it's better to create a function for it. - const currentPartner = this.currentOrder.get_partner(); - const { confirmed, payload: newPartner } = await this.showTempScreen( - 'PartnerListScreen', - { partner: currentPartner } - ); - if (confirmed) { - this.currentOrder.set_partner(newPartner); - this.currentOrder.updatePricelist(newPartner); - } - } - addNewPaymentLine({ detail: paymentMethod }) { - // original function: click_paymentmethods - let result = this.currentOrder.add_paymentline(paymentMethod); - if (result){ - NumberBuffer.reset(); - return true; - } - else{ - this.showPopup('ErrorPopup', { - title: this.env._t('Error'), - body: this.env._t('There is already an electronic payment in progress.'), - }); - return false; - } - } - _updateSelectedPaymentline() { - if (this.paymentLines.every((line) => line.paid)) { - this.currentOrder.add_paymentline(this.payment_methods_from_config[0]); - } - if (!this.selectedPaymentLine) return; // do nothing if no selected payment line - // disable changing amount on paymentlines with running or done payments on a payment terminal - const payment_terminal = this.selectedPaymentLine.payment_method.payment_terminal; - if ( - payment_terminal && - !['pending', 'retry'].includes(this.selectedPaymentLine.get_payment_status()) - ) { - return; - } - if (NumberBuffer.get() === null) { - this.deletePaymentLine({ detail: { cid: this.selectedPaymentLine.cid } }); - } else { - this.selectedPaymentLine.set_amount(NumberBuffer.getFloat()); - } - } - toggleIsToInvoice() { - // click_invoice - this.currentOrder.set_to_invoice(!this.currentOrder.is_to_invoice()); - this.render(true); - } - openCashbox() { - this.env.proxy.printer.open_cashbox(); - } - async addTip() { - // click_tip - const tip = this.currentOrder.get_tip(); - const change = this.currentOrder.get_change(); - let value = tip === 0 && change > 0 ? change : tip; + } + toggleIsToShip() { + // click_ship + this.currentOrder.set_to_ship(!this.currentOrder.is_to_ship()); + this.render(true); + } + deletePaymentLine(event) { + var self = this; + const { cid } = event.detail; + const line = this.paymentLines.find((line) => line.cid === cid); - const { confirmed, payload } = await this.showPopup('NumberPopup', { - title: tip ? this.env._t('Change Tip') : this.env._t('Add Tip'), - startingValue: value, - isInputSelected: true, - }); - - if (confirmed) { - this.currentOrder.set_tip(parse.float(payload)); - } - } - toggleIsToShip() { - // click_ship - this.currentOrder.set_to_ship(!this.currentOrder.is_to_ship()); - this.render(true); - } - deletePaymentLine(event) { - var self = this; - const { cid } = event.detail; - const line = this.paymentLines.find((line) => line.cid === cid); - - // If a paymentline with a payment terminal linked to - // it is removed, the terminal should get a cancel - // request. - if (['waiting', 'waitingCard', 'timeout'].includes(line.get_payment_status())) { - line.set_payment_status('waitingCancel'); - line.payment_method.payment_terminal.send_payment_cancel(this.currentOrder, cid).then(function() { + // If a paymentline with a payment terminal linked to + // it is removed, the terminal should get a cancel + // request. + if (["waiting", "waitingCard", "timeout"].includes(line.get_payment_status())) { + line.set_payment_status("waitingCancel"); + line.payment_method.payment_terminal + .send_payment_cancel(this.currentOrder, cid) + .then(function () { self.currentOrder.remove_paymentline(line); NumberBuffer.reset(); self.render(true); - }) - } - else if (line.get_payment_status() !== 'waitingCancel') { - this.currentOrder.remove_paymentline(line); - NumberBuffer.reset(); - this.render(true); - } - } - selectPaymentLine(event) { - const { cid } = event.detail; - const line = this.paymentLines.find((line) => line.cid === cid); - this.currentOrder.select_paymentline(line); + }); + } else if (line.get_payment_status() !== "waitingCancel") { + this.currentOrder.remove_paymentline(line); 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', { - 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."), - }); - return; - } - } - if (await this._isOrderValid(isForceValidate)) { - // remove pending payments before finalizing the validation - for (let line of this.paymentLines) { - if (!line.is_done()) this.currentOrder.remove_paymentline(line); - } - await this._finalizeValidation(); - } - } - async _finalizeValidation() { - if ((this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) && this.env.pos.config.iface_cashdrawer) { - this.env.proxy.printer.open_cashbox(); - } - - this.currentOrder.initialize_validation_date(); - this.currentOrder.finalized = true; - - let syncOrderResult, hasError; - - try { - // 1. Save order to server. - syncOrderResult = await this.env.pos.push_single_order(this.currentOrder); - - // 2. Invoice. - if (this.currentOrder.is_to_invoice()) { - if (syncOrderResult.length) { - await this.env.legacyActionManager.do_action('account.account_invoices', { - additional_context: { - active_ids: [syncOrderResult[0].account_move], - }, - }); - } else { - throw { code: 401, message: 'Backend Invoice', data: { order: this.currentOrder } }; - } - } - - // 3. Post process. - if (syncOrderResult.length && this.currentOrder.wait_for_push_order()) { - const postPushResult = await this._postPushOrderResolve( - this.currentOrder, - syncOrderResult.map((res) => res.id) - ); - if (!postPushResult) { - 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.'), - }); - } - } - } catch (error) { - if (error.code == 700 || error.code == 701) - this.error = true; - - if ('code' in error) { - // We started putting `code` in the rejected object for invoicing error. - // We can continue with that convention such that when the error has `code`, - // then it is an error when invoicing. Besides, _handlePushOrderError was - // introduce to handle invoicing error logic. - await this._handlePushOrderError(error); - } else { - // We don't block for connection error. But we rethrow for any other errors. - if (isConnectionError(error)) { - this.showPopup('OfflineErrorPopup', { - title: this.env._t('Connection Error'), - body: this.env._t('Order is not synced. Check your internet connection'), - }); - } else { - throw error; - } - } - } finally { - // Always show the next screen regardless of error since pos has to - // continue working even offline. - this.showScreen(this.nextScreen); - // Remove the order from the local storage so that when we refresh the page, the order - // won't be there - this.env.pos.db.remove_unpaid_order(this.currentOrder); - - // 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', { - title: this.env._t('Remaining unsynced orders'), - body: this.env._t( - 'There are unsynced orders. Do you want to sync these orders?' - ), - }); - if (confirmed) { - // NOTE: Not yet sure if this should be awaited or not. - // If awaited, some operations like changing screen - // might not work. - this.env.pos.push_orders(); - } - } - } - } - get nextScreen() { - return !this.error? 'ReceiptScreen' : 'ProductScreen'; - } - async _isOrderValid(isForceValidate) { - if (this.currentOrder.get_orderlines().length === 0 && this.currentOrder.is_to_invoice()) { - this.showPopup('ErrorPopup', { - title: this.env._t('Empty Order'), + } + selectPaymentLine(event) { + const { cid } = event.detail; + const line = this.paymentLines.find((line) => line.cid === cid); + this.currentOrder.select_paymentline(line); + 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", { + title: this.env._t("Rounding error in payment lines"), body: this.env._t( - 'There must be at least one product in your order before it can be validated and invoiced.' + "The amount of your payment lines must be rounded to validate the transaction." ), }); - return false; - } - - const splitPayments = this.paymentLines.filter(payment => payment.payment_method.split_transactions) - if (splitPayments.length && !this.currentOrder.get_partner()) { - const paymentMethod = splitPayments[0].payment_method - 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.'), paymentMethod.name), - }); - if (confirmed) { - this.selectPartner(); - } - return false; - } - - if ((this.currentOrder.is_to_invoice() || this.currentOrder.is_to_ship()) && !this.currentOrder.get_partner()) { - 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.' - ), - }); - if (confirmed) { - this.selectPartner(); - } - return false; - } - - let partner = this.currentOrder.get_partner() - if (this.currentOrder.is_to_ship() && !(partner.name && partner.street && partner.city && partner.country_id)) { - this.showPopup('ErrorPopup', { - title: this.env._t('Incorrect address for shipping'), - body: this.env._t('The selected customer needs an address.'), - }); - return false; - } - - if (this.currentOrder.get_total_with_tax() != 0 && this.currentOrder.get_paymentlines().length === 0) { - this.showNotification(this.env._t('Select a payment method to validate the order.')); - return false; - } - - if (!this.currentOrder.is_paid() || this.invoicing) { - return false; - } - - if (this.currentOrder.has_not_valid_rounding()) { - var line = this.currentOrder.has_not_valid_rounding(); - 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.' - ), - }); - return false; - } - - // The exact amount must be paid if there is no cash payment method defined. - if ( - Math.abs( - this.currentOrder.get_total_with_tax() - this.currentOrder.get_total_paid() + this.currentOrder.get_rounding_applied() - ) > 0.00001 - ) { - var cash = false; - for (var i = 0; i < this.env.pos.payment_methods.length; i++) { - cash = cash || this.env.pos.payment_methods[i].is_cash_count; - } - if (!cash) { - 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' - ), - }); - return false; - } - } - - // if the change is too large, it's probably an input error, make the user confirm. - if ( - !isForceValidate && - this.currentOrder.get_total_with_tax() > 0 && - this.currentOrder.get_total_with_tax() * 1000 < this.currentOrder.get_total_paid() - ) { - this.showPopup('ConfirmPopup', { - title: this.env._t('Please Confirm Large Amount'), - body: - this.env._t('Are you sure that the customer wants to pay') + - ' ' + - this.env.pos.format_currency(this.currentOrder.get_total_paid()) + - ' ' + - this.env._t('for an order of') + - ' ' + - this.env.pos.format_currency(this.currentOrder.get_total_with_tax()) + - ' ' + - this.env._t('? Clicking "Confirm" will validate the payment.'), - }).then(({ confirmed }) => { - if (confirmed) this.validateOrder(true); - }); - return false; - } - - if (!this.currentOrder._isValidEmptyOrder()) return false; - - return true; - } - async _postPushOrderResolve(order, order_server_ids) { - return true; - } - async _sendPaymentRequest({ detail: line }) { - // Other payment lines can not be reversed anymore - this.paymentLines.forEach(function (line) { - line.can_be_reversed = false; - }); - - const payment_terminal = line.payment_method.payment_terminal; - line.set_payment_status('waiting'); - - const isPaymentSuccessful = await payment_terminal.send_payment_request(line.cid); - if (isPaymentSuccessful) { - line.set_payment_status('done'); - line.can_be_reversed = payment_terminal.supports_reversals; - // Automatically validate the order when after an electronic payment, - // the current order is fully paid and due is zero. - if ( - this.currentOrder.is_paid() && - utils.float_is_zero(this.currentOrder.get_due(), this.env.pos.currency.decimal_places) && - this.env.pos.config.auto_validate_terminal_payment - ) { - this.trigger('validate-order'); - } - } else { - line.set_payment_status('retry'); + return; } } - async _sendPaymentCancel({ detail: line }) { - const payment_terminal = line.payment_method.payment_terminal; - line.set_payment_status('waitingCancel'); - const isCancelSuccessful = await payment_terminal.send_payment_cancel(this.currentOrder, line.cid); - if (isCancelSuccessful) { - line.set_payment_status('retry'); - } else { - line.set_payment_status('waitingCard'); + if (await this._isOrderValid(isForceValidate)) { + // remove pending payments before finalizing the validation + for (const line of this.paymentLines) { + if (!line.is_done()) { + this.currentOrder.remove_paymentline(line); + } } - } - async _sendPaymentReverse({ detail: line }) { - const payment_terminal = line.payment_method.payment_terminal; - line.set_payment_status('reversing'); - - const isReversalSuccessful = await payment_terminal.send_payment_reversal(line.cid); - if (isReversalSuccessful) { - line.set_amount(0); - line.set_payment_status('reversed'); - } else { - line.can_be_reversed = false; - line.set_payment_status('done'); - } - } - async _sendForceDone({ detail: line }) { - line.set_payment_status('done'); + await this._finalizeValidation(); } } - PaymentScreen.template = 'PaymentScreen'; + async _finalizeValidation() { + if ( + (this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) && + this.env.pos.config.iface_cashdrawer + ) { + this.env.proxy.printer.open_cashbox(); + } - Registries.Component.add(PaymentScreen); + this.currentOrder.initialize_validation_date(); + this.currentOrder.finalized = true; - return PaymentScreen; -}); + let syncOrderResult, hasError; + + try { + // 1. Save order to server. + syncOrderResult = await this.env.pos.push_single_order(this.currentOrder); + + // 2. Invoice. + if (this.currentOrder.is_to_invoice()) { + if (syncOrderResult.length) { + await this.env.legacyActionManager.do_action("account.account_invoices", { + additional_context: { + active_ids: [syncOrderResult[0].account_move], + }, + }); + } else { + throw { + code: 401, + message: "Backend Invoice", + data: { order: this.currentOrder }, + }; + } + } + + // 3. Post process. + if (syncOrderResult.length && this.currentOrder.wait_for_push_order()) { + const postPushResult = await this._postPushOrderResolve( + this.currentOrder, + syncOrderResult.map((res) => res.id) + ); + if (!postPushResult) { + 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." + ), + }); + } + } + } catch (error) { + if (error.code == 700 || error.code == 701) { + this.error = true; + } + + if ("code" in error) { + // We started putting `code` in the rejected object for invoicing error. + // We can continue with that convention such that when the error has `code`, + // then it is an error when invoicing. Besides, _handlePushOrderError was + // introduce to handle invoicing error logic. + await this._handlePushOrderError(error); + } else { + // We don't block for connection error. But we rethrow for any other errors. + if (isConnectionError(error)) { + this.showPopup("OfflineErrorPopup", { + title: this.env._t("Connection Error"), + body: this.env._t("Order is not synced. Check your internet connection"), + }); + } else { + throw error; + } + } + } finally { + // Always show the next screen regardless of error since pos has to + // continue working even offline. + this.showScreen(this.nextScreen); + // Remove the order from the local storage so that when we refresh the page, the order + // won't be there + this.env.pos.db.remove_unpaid_order(this.currentOrder); + + // 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", { + title: this.env._t("Remaining unsynced orders"), + body: this.env._t( + "There are unsynced orders. Do you want to sync these orders?" + ), + }); + if (confirmed) { + // NOTE: Not yet sure if this should be awaited or not. + // If awaited, some operations like changing screen + // might not work. + this.env.pos.push_orders(); + } + } + } + } + get nextScreen() { + return !this.error ? "ReceiptScreen" : "ProductScreen"; + } + async _isOrderValid(isForceValidate) { + if (this.currentOrder.get_orderlines().length === 0 && this.currentOrder.is_to_invoice()) { + 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." + ), + }); + return false; + } + + const splitPayments = this.paymentLines.filter( + (payment) => payment.payment_method.split_transactions + ); + if (splitPayments.length && !this.currentOrder.get_partner()) { + const paymentMethod = splitPayments[0].payment_method; + 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."), + paymentMethod.name + ), + }); + if (confirmed) { + this.selectPartner(); + } + return false; + } + + if ( + (this.currentOrder.is_to_invoice() || this.currentOrder.is_to_ship()) && + !this.currentOrder.get_partner() + ) { + 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." + ), + }); + if (confirmed) { + this.selectPartner(); + } + return false; + } + + const partner = this.currentOrder.get_partner(); + if ( + this.currentOrder.is_to_ship() && + !(partner.name && partner.street && partner.city && partner.country_id) + ) { + this.showPopup("ErrorPopup", { + title: this.env._t("Incorrect address for shipping"), + body: this.env._t("The selected customer needs an address."), + }); + return false; + } + + if ( + this.currentOrder.get_total_with_tax() != 0 && + this.currentOrder.get_paymentlines().length === 0 + ) { + this.showNotification(this.env._t("Select a payment method to validate the order.")); + return false; + } + + if (!this.currentOrder.is_paid() || this.invoicing) { + return false; + } + + if (this.currentOrder.has_not_valid_rounding()) { + var line = this.currentOrder.has_not_valid_rounding(); + 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." + ), + }); + return false; + } + + // The exact amount must be paid if there is no cash payment method defined. + if ( + Math.abs( + this.currentOrder.get_total_with_tax() - + this.currentOrder.get_total_paid() + + this.currentOrder.get_rounding_applied() + ) > 0.00001 + ) { + var cash = false; + for (var i = 0; i < this.env.pos.payment_methods.length; i++) { + cash = cash || this.env.pos.payment_methods[i].is_cash_count; + } + if (!cash) { + 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" + ), + }); + return false; + } + } + + // if the change is too large, it's probably an input error, make the user confirm. + if ( + !isForceValidate && + this.currentOrder.get_total_with_tax() > 0 && + this.currentOrder.get_total_with_tax() * 1000 < this.currentOrder.get_total_paid() + ) { + this.showPopup("ConfirmPopup", { + title: this.env._t("Please Confirm Large Amount"), + body: + this.env._t("Are you sure that the customer wants to pay") + + " " + + this.env.pos.format_currency(this.currentOrder.get_total_paid()) + + " " + + this.env._t("for an order of") + + " " + + this.env.pos.format_currency(this.currentOrder.get_total_with_tax()) + + " " + + this.env._t('? Clicking "Confirm" will validate the payment.'), + }).then(({ confirmed }) => { + if (confirmed) { + this.validateOrder(true); + } + }); + return false; + } + + if (!this.currentOrder._isValidEmptyOrder()) { + return false; + } + + return true; + } + async _postPushOrderResolve(order, order_server_ids) { + return true; + } + async _sendPaymentRequest({ detail: line }) { + // Other payment lines can not be reversed anymore + this.paymentLines.forEach(function (line) { + line.can_be_reversed = false; + }); + + const payment_terminal = line.payment_method.payment_terminal; + line.set_payment_status("waiting"); + + const isPaymentSuccessful = await payment_terminal.send_payment_request(line.cid); + if (isPaymentSuccessful) { + line.set_payment_status("done"); + line.can_be_reversed = payment_terminal.supports_reversals; + // Automatically validate the order when after an electronic payment, + // the current order is fully paid and due is zero. + if ( + this.currentOrder.is_paid() && + utils.float_is_zero( + this.currentOrder.get_due(), + this.env.pos.currency.decimal_places + ) && + this.env.pos.config.auto_validate_terminal_payment + ) { + this.trigger("validate-order"); + } + } else { + line.set_payment_status("retry"); + } + } + async _sendPaymentCancel({ detail: line }) { + const payment_terminal = line.payment_method.payment_terminal; + line.set_payment_status("waitingCancel"); + const isCancelSuccessful = await payment_terminal.send_payment_cancel( + this.currentOrder, + line.cid + ); + if (isCancelSuccessful) { + line.set_payment_status("retry"); + } else { + line.set_payment_status("waitingCard"); + } + } + async _sendPaymentReverse({ detail: line }) { + const payment_terminal = line.payment_method.payment_terminal; + line.set_payment_status("reversing"); + + const isReversalSuccessful = await payment_terminal.send_payment_reversal(line.cid); + if (isReversalSuccessful) { + line.set_amount(0); + line.set_payment_status("reversed"); + } else { + line.can_be_reversed = false; + line.set_payment_status("done"); + } + } + async _sendForceDone({ detail: line }) { + line.set_payment_status("done"); + } +} +PaymentScreen.template = "PaymentScreen"; + +Registries.Component.add(PaymentScreen); + +export default 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 4ec3536734f..8db09d3f16f 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,18 +1,16 @@ -odoo.define('point_of_sale.PaymentScreenNumpad', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class PaymentScreenNumpad extends PosComponent { - setup() { - super.setup(); - this.decimalPoint = this.env._t.database.parameters.decimal_point; - } +class PaymentScreenNumpad extends PosComponent { + setup() { + super.setup(); + this.decimalPoint = this.env._t.database.parameters.decimal_point; } - PaymentScreenNumpad.template = 'PaymentScreenNumpad'; +} +PaymentScreenNumpad.template = "PaymentScreenNumpad"; - Registries.Component.add(PaymentScreenNumpad); +Registries.Component.add(PaymentScreenNumpad); - return 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 8f2311468f3..7a0b4185284 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,23 +1,21 @@ -odoo.define('point_of_sale.PaymentScreenPaymentLines', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class PaymentScreenPaymentLines extends PosComponent { - formatLineAmount(paymentline) { - return this.env.pos.format_currency_no_symbol(paymentline.get_amount()); - } - selectedLineClass(line) { - return { 'payment-terminal': line.get_payment_status() }; - } - unselectedLineClass(line) { - return {}; - } +class PaymentScreenPaymentLines extends PosComponent { + formatLineAmount(paymentline) { + return this.env.pos.format_currency_no_symbol(paymentline.get_amount()); } - PaymentScreenPaymentLines.template = 'PaymentScreenPaymentLines'; + selectedLineClass(line) { + return { "payment-terminal": line.get_payment_status() }; + } + unselectedLineClass(line) { + return {}; + } +} +PaymentScreenPaymentLines.template = "PaymentScreenPaymentLines"; - Registries.Component.add(PaymentScreenPaymentLines); +Registries.Component.add(PaymentScreenPaymentLines); - return 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 afcb08ed303..2cdc39e7081 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,27 +1,25 @@ -odoo.define('point_of_sale.PaymentScreenStatus', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class PaymentScreenStatus extends PosComponent { - get changeText() { - return this.env.pos.format_currency(this.props.order.get_change()); - } - get totalDueText() { - return this.env.pos.format_currency( - this.props.order.get_total_with_tax() + this.props.order.get_rounding_applied() - ); - } - get remainingText() { - return this.env.pos.format_currency( - this.props.order.get_due() > 0 ? this.props.order.get_due() : 0 - ); - } +class PaymentScreenStatus extends PosComponent { + get changeText() { + return this.env.pos.format_currency(this.props.order.get_change()); } - PaymentScreenStatus.template = 'PaymentScreenStatus'; + get totalDueText() { + return this.env.pos.format_currency( + this.props.order.get_total_with_tax() + this.props.order.get_rounding_applied() + ); + } + get remainingText() { + return this.env.pos.format_currency( + this.props.order.get_due() > 0 ? this.props.order.get_due() : 0 + ); + } +} +PaymentScreenStatus.template = "PaymentScreenStatus"; - Registries.Component.add(PaymentScreenStatus); +Registries.Component.add(PaymentScreenStatus); - return 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 fec19079d7c..b7b8ece117e 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,25 +1,23 @@ -odoo.define('point_of_sale.ActionpadWidget', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - /** - * @props partner - * @emits click-partner - * @emits click-pay - */ - class ActionpadWidget extends PosComponent { - get isLongName() { - return this.props.partner && this.props.partner.name.length > 10; - } - } - ActionpadWidget.template = 'ActionpadWidget'; - ActionpadWidget.defaultProps = { - isActionButtonHighlighted: false, +/** + * @props partner + * @emits click-partner + * @emits click-pay + */ +class ActionpadWidget extends PosComponent { + get isLongName() { + return this.props.partner && this.props.partner.name.length > 10; } +} +ActionpadWidget.template = "ActionpadWidget"; +ActionpadWidget.defaultProps = { + isActionButtonHighlighted: false, +}; - Registries.Component.add(ActionpadWidget); +Registries.Component.add(ActionpadWidget); - return 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 9aa6914e087..af034bf1d1a 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,18 +1,16 @@ -odoo.define('point_of_sale.CategoryButton', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - 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}`; - } +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'; +} +CategoryButton.template = "CategoryButton"; - Registries.Component.add(CategoryButton); +Registries.Component.add(CategoryButton); - return 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 36f3fefd9f0..2445f3e3d67 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,37 +1,37 @@ -odoo.define('point_of_sale.OrderlineCustomerNoteButton', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const ProductScreen = require('point_of_sale.ProductScreen'); - const { useListener } = require("@web/core/utils/hooks"); - const Registries = require('point_of_sale.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 Registries from "@point_of_sale/js/Registries"; - class OrderlineCustomerNoteButton extends PosComponent { - setup() { - super.setup(); - useListener('click', this.onClick); +class OrderlineCustomerNoteButton extends PosComponent { + setup() { + super.setup(); + useListener("click", this.onClick); + } + async onClick() { + const selectedOrderline = this.env.pos.get_order().get_selected_orderline(); + if (!selectedOrderline) { + return; } - async onClick() { - const selectedOrderline = this.env.pos.get_order().get_selected_orderline(); - if (!selectedOrderline) return; - const { confirmed, payload: inputNote } = await this.showPopup('TextAreaPopup', { - startingValue: selectedOrderline.get_customer_note(), - title: this.env._t('Add Customer Note'), - }); + const { confirmed, payload: inputNote } = await this.showPopup("TextAreaPopup", { + startingValue: selectedOrderline.get_customer_note(), + title: this.env._t("Add Customer Note"), + }); - if (confirmed) { - selectedOrderline.set_customer_note(inputNote); - } + if (confirmed) { + selectedOrderline.set_customer_note(inputNote); } } - OrderlineCustomerNoteButton.template = 'OrderlineCustomerNoteButton'; +} +OrderlineCustomerNoteButton.template = "OrderlineCustomerNoteButton"; - ProductScreen.addControlButton({ - component: OrderlineCustomerNoteButton, - }); - - Registries.Component.add(OrderlineCustomerNoteButton); - - return 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 635d5660880..38cafb7ada2 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,50 +1,50 @@ -odoo.define('point_of_sale.ProductInfoButton', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const ProductScreen = require('point_of_sale.ProductScreen'); - const { useListener } = require("@web/core/utils/hooks"); - const Registries = require('point_of_sale.Registries'); - const { ConnectionLostError, ConnectionAbortedError } = require('@web/core/network/rpc_service') - const { identifyError } = require('point_of_sale.utils'); +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 { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service"; +import { identifyError } from "@point_of_sale/js/utils"; - class ProductInfoButton extends PosComponent { - setup() { - super.setup(); - useListener('click', this.onClick); - } - async onClick() { - const orderline = this.env.pos.get_order().get_selected_orderline(); - if (orderline) { - const product = orderline.get_product(); - const quantity = orderline.get_quantity(); - try { - const info = await this.env.pos.getProductInfo(product, quantity); - this.showPopup('ProductInfoPopup', { info: info , product: product }); - } catch (e) { - if (identifyError(e) instanceof ConnectionLostError||ConnectionAbortedError) { - this.showPopup('OfflineErrorPopup', { - title: this.env._t('Network Error'), - body: this.env._t('Cannot access product information screen if offline.'), - }); - } else { - this.showPopup('ErrorPopup', { - title: this.env._t('Unknown error'), - body: this.env._t('An unknown error prevents us from loading product information.'), - }); - } +class ProductInfoButton extends PosComponent { + setup() { + super.setup(); + useListener("click", this.onClick); + } + async onClick() { + const orderline = this.env.pos.get_order().get_selected_orderline(); + if (orderline) { + const product = orderline.get_product(); + const quantity = orderline.get_quantity(); + try { + const info = await this.env.pos.getProductInfo(product, quantity); + this.showPopup("ProductInfoPopup", { info: info, product: product }); + } catch (e) { + if (identifyError(e) instanceof ConnectionLostError || ConnectionAbortedError) { + this.showPopup("OfflineErrorPopup", { + title: this.env._t("Network Error"), + body: this.env._t("Cannot access product information screen if offline."), + }); + } else { + this.showPopup("ErrorPopup", { + title: this.env._t("Unknown error"), + body: this.env._t( + "An unknown error prevents us from loading product information." + ), + }); } } } } - ProductInfoButton.template = 'ProductInfoButton'; +} +ProductInfoButton.template = "ProductInfoButton"; - ProductScreen.addControlButton({ - component: ProductInfoButton, - position: ['before', 'SetFiscalPositionButton'], - }); - - Registries.Component.add(ProductInfoButton); - - return 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 0c0799fdffa..b21861ff640 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,35 +1,33 @@ -odoo.define('point_of_sale.RefundButton', function (require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const ProductScreen = require('point_of_sale.ProductScreen'); - const Registries = require('point_of_sale.Registries'); - const { useListener } = require("@web/core/utils/hooks"); +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 { useListener } from "@web/core/utils/hooks"; - class RefundButton extends PosComponent { - setup() { - super.setup(); - useListener('click', this._onClick); - } - _onClick() { - const partner = this.env.pos.get_order().get_partner(); - const searchDetails = partner ? { fieldName: 'PARTNER', searchTerm: partner.name } : {}; - this.showScreen('TicketScreen', { - ui: { filter: 'SYNCED', searchDetails }, - destinationOrder: this.env.pos.get_order(), - }); - } +class RefundButton extends PosComponent { + setup() { + super.setup(); + useListener("click", this._onClick); } - RefundButton.template = 'point_of_sale.RefundButton'; + _onClick() { + const partner = this.env.pos.get_order().get_partner(); + const searchDetails = partner ? { fieldName: "PARTNER", searchTerm: partner.name } : {}; + this.showScreen("TicketScreen", { + ui: { filter: "SYNCED", searchDetails }, + destinationOrder: this.env.pos.get_order(), + }); + } +} +RefundButton.template = "point_of_sale.RefundButton"; - ProductScreen.addControlButton({ - component: RefundButton, - condition: function () { - return true; - }, - }); - - Registries.Component.add(RefundButton); - - return RefundButton; +ProductScreen.addControlButton({ + component: RefundButton, + condition: function () { + 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 226ff0971cc..0a0a55f2aec 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,71 +1,69 @@ -odoo.define('point_of_sale.SetFiscalPositionButton', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const ProductScreen = require('point_of_sale.ProductScreen'); - const { useListener } = require("@web/core/utils/hooks"); - const Registries = require('point_of_sale.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 Registries from "@point_of_sale/js/Registries"; - class SetFiscalPositionButton extends PosComponent { - setup() { - super.setup(); - useListener('click', this.onClick); +class SetFiscalPositionButton extends PosComponent { + setup() { + super.setup(); + useListener("click", this.onClick); + } + get currentOrder() { + return this.env.pos.get_order(); + } + get currentFiscalPositionName() { + return this.currentOrder && this.currentOrder.fiscal_position + ? this.currentOrder.fiscal_position.display_name + : this.env._t("Tax"); + } + async onClick() { + const currentFiscalPosition = this.currentOrder.fiscal_position; + const fiscalPosList = [ + { + id: -1, + label: this.env._t("None"), + isSelected: !currentFiscalPosition, + }, + ]; + for (const fiscalPos of this.env.pos.fiscal_positions) { + fiscalPosList.push({ + id: fiscalPos.id, + label: fiscalPos.name, + isSelected: currentFiscalPosition + ? fiscalPos.id === currentFiscalPosition.id + : false, + item: fiscalPos, + }); } - get currentOrder() { - return this.env.pos.get_order(); - } - get currentFiscalPositionName() { - return this.currentOrder && this.currentOrder.fiscal_position - ? this.currentOrder.fiscal_position.display_name - : this.env._t('Tax'); - } - async onClick() { - const currentFiscalPosition = this.currentOrder.fiscal_position; - const fiscalPosList = [ - { - id: -1, - label: this.env._t('None'), - isSelected: !currentFiscalPosition, - }, - ]; - for (let fiscalPos of this.env.pos.fiscal_positions) { - fiscalPosList.push({ - id: fiscalPos.id, - label: fiscalPos.name, - isSelected: currentFiscalPosition - ? fiscalPos.id === currentFiscalPosition.id - : false, - item: fiscalPos, - }); + const { confirmed, payload: selectedFiscalPosition } = await this.showPopup( + "SelectionPopup", + { + title: this.env._t("Select Fiscal Position"), + list: fiscalPosList, } - const { confirmed, payload: selectedFiscalPosition } = await this.showPopup( - 'SelectionPopup', - { - title: this.env._t('Select Fiscal Position'), - list: fiscalPosList, - } - ); - if (confirmed) { - this.currentOrder.set_fiscal_position(selectedFiscalPosition); - // IMPROVEMENT: The following is the old implementation and I believe - // there could be a better way of doing it. - for (let line of this.currentOrder.orderlines) { - line.set_quantity(line.quantity); - } + ); + if (confirmed) { + this.currentOrder.set_fiscal_position(selectedFiscalPosition); + // IMPROVEMENT: The following is the old implementation and I believe + // there could be a better way of doing it. + for (const line of this.currentOrder.orderlines) { + line.set_quantity(line.quantity); } } } - SetFiscalPositionButton.template = 'SetFiscalPositionButton'; +} +SetFiscalPositionButton.template = "SetFiscalPositionButton"; - ProductScreen.addControlButton({ - component: SetFiscalPositionButton, - condition: function() { - return this.env.pos.fiscal_positions.length > 0; - }, - position: ['before', 'SetPricelistButton'], - }); - - Registries.Component.add(SetFiscalPositionButton); - - return SetFiscalPositionButton; +ProductScreen.addControlButton({ + component: SetFiscalPositionButton, + condition: function () { + return this.env.pos.fiscal_positions.length > 0; + }, + 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 0370543a387..80fa78c6295 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,59 +1,52 @@ -odoo.define('point_of_sale.SetPricelistButton', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const ProductScreen = require('point_of_sale.ProductScreen'); - const { useListener } = require("@web/core/utils/hooks"); - const Registries = require('point_of_sale.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 Registries from "@point_of_sale/js/Registries"; - class SetPricelistButton extends PosComponent { - setup() { - super.setup(); - useListener('click', this.onClick); - } - get currentOrder() { - return this.env.pos.get_order(); - } - get currentPricelistName() { - const order = this.currentOrder; - return order && order.pricelist - ? order.pricelist.display_name - : this.env._t('Pricelist'); - } - async onClick() { - // Create the list to be passed to the SelectionPopup. - // Pricelist object is passed as item in the list because it - // is the object that will be returned when the popup is confirmed. - const selectionList = this.env.pos.pricelists.map(pricelist => ({ - id: pricelist.id, - label: pricelist.name, - isSelected: pricelist.id === this.currentOrder.pricelist.id, - item: pricelist, - })); +class SetPricelistButton extends PosComponent { + setup() { + super.setup(); + useListener("click", this.onClick); + } + get currentOrder() { + return this.env.pos.get_order(); + } + get currentPricelistName() { + const order = this.currentOrder; + return order && order.pricelist ? order.pricelist.display_name : this.env._t("Pricelist"); + } + async onClick() { + // Create the list to be passed to the SelectionPopup. + // Pricelist object is passed as item in the list because it + // is the object that will be returned when the popup is confirmed. + const selectionList = this.env.pos.pricelists.map((pricelist) => ({ + id: pricelist.id, + label: pricelist.name, + isSelected: pricelist.id === this.currentOrder.pricelist.id, + item: pricelist, + })); - const { confirmed, payload: selectedPricelist } = await this.showPopup( - 'SelectionPopup', - { - title: this.env._t('Select the pricelist'), - list: selectionList, - } - ); + const { confirmed, payload: selectedPricelist } = await this.showPopup("SelectionPopup", { + title: this.env._t("Select the pricelist"), + list: selectionList, + }); - if (confirmed) { - this.currentOrder.set_pricelist(selectedPricelist); - } + if (confirmed) { + this.currentOrder.set_pricelist(selectedPricelist); } } - SetPricelistButton.template = 'SetPricelistButton'; +} +SetPricelistButton.template = "SetPricelistButton"; - ProductScreen.addControlButton({ - component: SetPricelistButton, - condition: function() { - return this.env.pos.config.use_pricelist && this.env.pos.pricelists.length > 1; - }, - }); - - Registries.Component.add(SetPricelistButton); - - return SetPricelistButton; +ProductScreen.addControlButton({ + component: SetPricelistButton, + condition: function () { + return this.env.pos.config.use_pricelist && this.env.pos.pricelists.length > 1; + }, }); + +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 eb0e0c53826..933fccd1983 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,49 +1,49 @@ -odoo.define('point_of_sale.NumpadWidget', function (require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - /** - * @prop {'quantity' | 'price' | 'discount'} activeMode - * @prop {Array<'quantity' | 'price' | 'discount'>} disabledModes - * @prop {boolean} disableSign - * @event set-numpad-mode - triggered when mode button is clicked - * @event numpad-click-input - triggered when numpad button is clicked - */ - class NumpadWidget extends PosComponent { - get hasPriceControlRights() { - return ( - this.env.pos.cashierHasPriceControlRights() && - !this.props.disabledModes.includes('price') - ); - } - get hasManualDiscount() { - return this.env.pos.config.manual_discount && !this.props.disabledModes.includes('discount'); - } - changeMode(mode) { - if (!this.hasPriceControlRights && mode === 'price') { - return; - } - if (!this.hasManualDiscount && mode === 'discount') { - return; - } - this.trigger('set-numpad-mode', { mode }); - } - sendInput(key) { - this.trigger('numpad-click-input', { key }); - } - get decimalSeparator() { - return this.env._t.database.parameters.decimal_point; - } +/** + * @prop {'quantity' | 'price' | 'discount'} activeMode + * @prop {Array<'quantity' | 'price' | 'discount'>} disabledModes + * @prop {boolean} disableSign + * @event set-numpad-mode - triggered when mode button is clicked + * @event numpad-click-input - triggered when numpad button is clicked + */ +class NumpadWidget extends PosComponent { + get hasPriceControlRights() { + return ( + this.env.pos.cashierHasPriceControlRights() && + !this.props.disabledModes.includes("price") + ); } - NumpadWidget.template = 'NumpadWidget'; - NumpadWidget.defaultProps = { - disabledModes: [], - disableSign: false, + get hasManualDiscount() { + return ( + this.env.pos.config.manual_discount && !this.props.disabledModes.includes("discount") + ); } + changeMode(mode) { + if (!this.hasPriceControlRights && mode === "price") { + return; + } + if (!this.hasManualDiscount && mode === "discount") { + return; + } + this.trigger("set-numpad-mode", { mode }); + } + sendInput(key) { + this.trigger("numpad-click-input", { key }); + } + get decimalSeparator() { + return this.env._t.database.parameters.decimal_point; + } +} +NumpadWidget.template = "NumpadWidget"; +NumpadWidget.defaultProps = { + disabledModes: [], + disableSign: false, +}; - Registries.Component.add(NumpadWidget); +Registries.Component.add(NumpadWidget); - return 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 854e71e9274..b77f9060b10 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,27 +1,25 @@ -odoo.define('point_of_sale.OrderSummary', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); - const { float_is_zero } = require('web.utils'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; +import { float_is_zero } from "web.utils"; - class OrderSummary extends PosComponent { - getTotal() { - return this.env.pos.format_currency(this.props.order.get_total_with_tax()); - } - getTax() { - const total = this.props.order.get_total_with_tax(); - const totalWithoutTax = this.props.order.get_total_without_tax(); - const taxAmount = total - totalWithoutTax; - return { - hasTax: !float_is_zero(taxAmount, this.env.pos.currency.decimal_places), - displayAmount: this.env.pos.format_currency(taxAmount), - }; - } +class OrderSummary extends PosComponent { + getTotal() { + return this.env.pos.format_currency(this.props.order.get_total_with_tax()); } - OrderSummary.template = 'OrderSummary'; + getTax() { + const total = this.props.order.get_total_with_tax(); + const totalWithoutTax = this.props.order.get_total_without_tax(); + const taxAmount = total - totalWithoutTax; + return { + hasTax: !float_is_zero(taxAmount, this.env.pos.currency.decimal_places), + displayAmount: this.env.pos.format_currency(taxAmount), + }; + } +} +OrderSummary.template = "OrderSummary"; - Registries.Component.add(OrderSummary); +Registries.Component.add(OrderSummary); - return 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 c6b241fc878..4eb0edb2b6c 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,72 +1,70 @@ -odoo.define('point_of_sale.OrderWidget', function(require) { - 'use strict'; +/** @odoo-module */ - const { useListener } = require("@web/core/utils/hooks"); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import { useListener } from "@web/core/utils/hooks"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - const { onPatched, useRef } = owl; +const { onPatched, useRef } = owl; - class OrderWidget extends PosComponent { - setup() { - super.setup(); - useListener('select-line', this._selectLine); - useListener('edit-pack-lot-lines', this._editPackLotLines); - this.scrollableRef = useRef('scrollable'); - this.scrollToBottom = false; - onPatched(() => { - // IMPROVEMENT - // This one just stays at the bottom of the orderlines list. - // Perhaps it is better to scroll to the added or modified orderline. - if (this.scrollToBottom) { - this.scrollableRef.el.scrollTop = this.scrollableRef.el.scrollHeight; - this.scrollToBottom = false; - } - }); - } - get order() { - return this.env.pos.get_order(); - } - get orderlinesArray() { - return this.order ? this.order.get_orderlines() : []; - } - _selectLine(event) { - this.order.select_orderline(event.detail.orderline); - } - // IMPROVEMENT: Might be better to lift this to ProductScreen - // because there is similar operation when clicking a product. - // - // Furthermore, what if a number different from 1 (or -1) is specified - // to an orderline that has product tracked by lot. Lot tracking (based - // on the current implementation) requires that 1 item per orderline is - // allowed. - async _editPackLotLines(event) { - const orderline = event.detail.orderline; - const isAllowOnlyOneLot = orderline.product.isAllowOnlyOneLot(); - const packLotLinesToEdit = orderline.getPackLotLinesToEdit(isAllowOnlyOneLot); - const { confirmed, payload } = await this.showPopup('EditListPopup', { - title: this.env._t('Lot/Serial Number(s) Required'), - name: orderline.product.display_name, - isSingleItem: isAllowOnlyOneLot, - array: packLotLinesToEdit, - }); - if (confirmed) { - // Segregate the old and new packlot lines - const modifiedPackLotLines = Object.fromEntries( - payload.newArray.filter(item => item.id).map(item => [item.id, item.text]) - ); - const newPackLotLines = payload.newArray - .filter(item => !item.id) - .map(item => ({ lot_name: item.text })); - - orderline.setPackLotLines({ modifiedPackLotLines, newPackLotLines }); +class OrderWidget extends PosComponent { + setup() { + super.setup(); + useListener("select-line", this._selectLine); + useListener("edit-pack-lot-lines", this._editPackLotLines); + this.scrollableRef = useRef("scrollable"); + this.scrollToBottom = false; + onPatched(() => { + // IMPROVEMENT + // This one just stays at the bottom of the orderlines list. + // Perhaps it is better to scroll to the added or modified orderline. + if (this.scrollToBottom) { + this.scrollableRef.el.scrollTop = this.scrollableRef.el.scrollHeight; + this.scrollToBottom = false; } - this.order.select_orderline(event.detail.orderline); - } + }); } - OrderWidget.template = 'OrderWidget'; + get order() { + return this.env.pos.get_order(); + } + get orderlinesArray() { + return this.order ? this.order.get_orderlines() : []; + } + _selectLine(event) { + this.order.select_orderline(event.detail.orderline); + } + // IMPROVEMENT: Might be better to lift this to ProductScreen + // because there is similar operation when clicking a product. + // + // Furthermore, what if a number different from 1 (or -1) is specified + // to an orderline that has product tracked by lot. Lot tracking (based + // on the current implementation) requires that 1 item per orderline is + // allowed. + async _editPackLotLines(event) { + const orderline = event.detail.orderline; + const isAllowOnlyOneLot = orderline.product.isAllowOnlyOneLot(); + const packLotLinesToEdit = orderline.getPackLotLinesToEdit(isAllowOnlyOneLot); + const { confirmed, payload } = await this.showPopup("EditListPopup", { + title: this.env._t("Lot/Serial Number(s) Required"), + name: orderline.product.display_name, + isSingleItem: isAllowOnlyOneLot, + array: packLotLinesToEdit, + }); + if (confirmed) { + // Segregate the old and new packlot lines + const modifiedPackLotLines = Object.fromEntries( + payload.newArray.filter((item) => item.id).map((item) => [item.id, item.text]) + ); + const newPackLotLines = payload.newArray + .filter((item) => !item.id) + .map((item) => ({ lot_name: item.text })); - Registries.Component.add(OrderWidget); + orderline.setPackLotLines({ modifiedPackLotLines, newPackLotLines }); + } + this.order.select_orderline(event.detail.orderline); + } +} +OrderWidget.template = "OrderWidget"; - return 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 44213f0b956..29aa8ee0126 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,28 +1,26 @@ -odoo.define('point_of_sale.Orderline', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class Orderline extends PosComponent { - selectLine() { - this.trigger('select-line', { orderline: this.props.line }); - } - lotIconClicked() { - this.trigger('edit-pack-lot-lines', { orderline: this.props.line }); - } - get addedClasses() { - return { - selected: this.props.line.selected, - }; - } - get customerNote() { - return this.props.line.get_customer_note(); - } +class Orderline extends PosComponent { + selectLine() { + this.trigger("select-line", { orderline: this.props.line }); } - Orderline.template = 'Orderline'; + lotIconClicked() { + this.trigger("edit-pack-lot-lines", { orderline: this.props.line }); + } + get addedClasses() { + return { + selected: this.props.line.selected, + }; + } + get customerNote() { + return this.props.line.get_customer_note(); + } +} +Orderline.template = "Orderline"; - Registries.Component.add(Orderline); +Registries.Component.add(Orderline); - return 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 17f9f2734b8..87a42cf4bc4 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,49 +1,47 @@ -odoo.define('point_of_sale.ProductItem', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class ProductItem extends PosComponent { - /** - * For accessibility, pressing should be like clicking the product. - * is not considered because it conflicts with the barcode. - * - * @param {KeyPressEvent} event - */ - spaceClickProduct(event) { - if (event.which === 32) { - this.trigger('click-product', this.props.product); - } - } - get imageUrl() { - const product = this.props.product; - return `/web/image?model=product.product&field=image_128&id=${product.id}&unique=${product.write_date}`; - } - get pricelist() { - const current_order = this.env.pos.get_order(); - if (current_order) { - return current_order.pricelist; - } - return this.env.pos.default_pricelist; - } - get price() { - const formattedUnitPrice = this.env.pos.format_currency( - this.props.product.get_display_price(this.pricelist, 1), - 'Product Price' - ); - if (this.props.product.to_weight) { - return `${formattedUnitPrice}/${ - this.env.pos.units_by_id[this.props.product.uom_id[0]].name - }`; - } else { - return formattedUnitPrice; - } +class ProductItem extends PosComponent { + /** + * For accessibility, pressing should be like clicking the product. + * is not considered because it conflicts with the barcode. + * + * @param {KeyPressEvent} event + */ + spaceClickProduct(event) { + if (event.which === 32) { + this.trigger("click-product", this.props.product); } } - ProductItem.template = 'ProductItem'; + get imageUrl() { + const product = this.props.product; + return `/web/image?model=product.product&field=image_128&id=${product.id}&unique=${product.write_date}`; + } + get pricelist() { + const current_order = this.env.pos.get_order(); + if (current_order) { + return current_order.pricelist; + } + return this.env.pos.default_pricelist; + } + get price() { + const formattedUnitPrice = this.env.pos.format_currency( + this.props.product.get_display_price(this.pricelist, 1), + "Product Price" + ); + if (this.props.product.to_weight) { + return `${formattedUnitPrice}/${ + this.env.pos.units_by_id[this.props.product.uom_id[0]].name + }`; + } else { + return formattedUnitPrice; + } + } +} +ProductItem.template = "ProductItem"; - Registries.Component.add(ProductItem); +Registries.Component.add(ProductItem); - return 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 1cba86074e5..21da9bb4321 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,390 +1,421 @@ -odoo.define('point_of_sale.ProductScreen', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const ControlButtonsMixin = require('point_of_sale.ControlButtonsMixin'); - const NumberBuffer = require('point_of_sale.NumberBuffer'); - const { useListener } = require("@web/core/utils/hooks"); - const Registries = require('point_of_sale.Registries'); - const { useBarcodeReader } = require('point_of_sale.custom_hooks'); - const { isConnectionError } = require('point_of_sale.utils'); - const { parse } = require('web.field_utils'); +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 { useListener } from "@web/core/utils/hooks"; +import Registries from "@point_of_sale/js/Registries"; +import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; +import { isConnectionError } from "@point_of_sale/js/utils"; +import { parse } from "web.field_utils"; - const { onMounted, useState } = owl; +const { onMounted, useState } = owl; - class ProductScreen extends ControlButtonsMixin(PosComponent) { - setup() { - super.setup(); - useListener('update-selected-orderline', this._updateSelectedOrderline); - useListener('select-line', this._selectLine); - useListener('set-numpad-mode', this._setNumpadMode); - useListener('click-product', this._clickProduct); - useListener('click-partner', this.onClickPartner); - useListener('click-pay', this._onClickPay); - useBarcodeReader({ - product: this._barcodeProductAction, - weight: this._barcodeProductAction, - price: this._barcodeProductAction, - client: this._barcodePartnerAction, - discount: this._barcodeDiscountAction, - error: this._barcodeErrorAction, +class ProductScreen extends ControlButtonsMixin(PosComponent) { + setup() { + super.setup(); + useListener("update-selected-orderline", this._updateSelectedOrderline); + useListener("select-line", this._selectLine); + useListener("set-numpad-mode", this._setNumpadMode); + useListener("click-product", this._clickProduct); + useListener("click-partner", this.onClickPartner); + useListener("click-pay", this._onClickPay); + useBarcodeReader({ + product: this._barcodeProductAction, + weight: this._barcodeProductAction, + price: this._barcodeProductAction, + client: this._barcodePartnerAction, + discount: this._barcodeDiscountAction, + error: this._barcodeErrorAction, + }); + 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. + // We don't do this in the `mounted` lifecycle method because it is called before + // the callbacks in `onMounted` hook. + onMounted(() => NumberBuffer.reset()); + this.state = useState({ + mobile_pane: this.props.mobile_pane || "right", + }); + } + onMounted() { + this.env.posbus.trigger("start-cash-control"); + } + /** + * To be overridden by modules that checks availability of + * connected scale. + * @see _onScaleNotAvailable + */ + get isScaleAvailable() { + return true; + } + get partner() { + return this.currentOrder ? this.currentOrder.get_partner() : null; + } + get currentOrder() { + return this.env.pos.get_order(); + } + async _getAddProductOptions(product, base_code) { + let price_extra = 0.0; + let draftPackLotLines, weight, description, packLotLinesToEdit; + + if (_.some(product.attribute_line_ids, (id) => id in this.env.pos.attributes_by_ptal_id)) { + const attributes = _.map( + product.attribute_line_ids, + (id) => this.env.pos.attributes_by_ptal_id[id] + ).filter((attr) => attr !== undefined); + const { confirmed, payload } = await this.showPopup("ProductConfiguratorPopup", { + product: product, + attributes: attributes, }); - 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. - // We don't do this in the `mounted` lifecycle method because it is called before - // the callbacks in `onMounted` hook. - onMounted(() => NumberBuffer.reset()); - this.state = useState({ - mobile_pane: this.props.mobile_pane || 'right', - }); - } - onMounted() { - this.env.posbus.trigger('start-cash-control'); - } - /** - * To be overridden by modules that checks availability of - * connected scale. - * @see _onScaleNotAvailable - */ - get isScaleAvailable() { - return true; - } - get partner() { - return this.currentOrder ? this.currentOrder.get_partner() : null; - } - get currentOrder() { - return this.env.pos.get_order(); - } - async _getAddProductOptions(product, base_code) { - let price_extra = 0.0; - let draftPackLotLines, weight, description, packLotLinesToEdit; - if (_.some(product.attribute_line_ids, (id) => id in this.env.pos.attributes_by_ptal_id)) { - let attributes = _.map(product.attribute_line_ids, (id) => this.env.pos.attributes_by_ptal_id[id]) - .filter((attr) => attr !== undefined); - let { confirmed, payload } = await this.showPopup('ProductConfiguratorPopup', { - product: product, - attributes: attributes, - }); - - if (confirmed) { - description = payload.selected_attributes.join(', '); - price_extra += payload.price_extra; - } else { - return; - } + if (confirmed) { + description = payload.selected_attributes.join(", "); + price_extra += payload.price_extra; + } else { + return; } + } - // Gather lot information if required. - if (['serial', 'lot'].includes(product.tracking) && (this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots)) { - const isAllowOnlyOneLot = product.isAllowOnlyOneLot(); - if (isAllowOnlyOneLot) { + // Gather lot information if required. + if ( + ["serial", "lot"].includes(product.tracking) && + (this.env.pos.picking_type.use_create_lots || + this.env.pos.picking_type.use_existing_lots) + ) { + const isAllowOnlyOneLot = product.isAllowOnlyOneLot(); + if (isAllowOnlyOneLot) { + packLotLinesToEdit = []; + } else { + const orderline = this.currentOrder + .get_orderlines() + .filter((line) => !line.get_discount()) + .find((line) => line.product.id === product.id); + if (orderline) { + packLotLinesToEdit = orderline.getPackLotLinesToEdit(); + } else { packLotLinesToEdit = []; - } else { - const orderline = this.currentOrder - .get_orderlines() - .filter(line => !line.get_discount()) - .find(line => line.product.id === product.id); - if (orderline) { - packLotLinesToEdit = orderline.getPackLotLinesToEdit(); - } else { - packLotLinesToEdit = []; - } } - const { confirmed, payload } = await this.showPopup('EditListPopup', { - title: this.env._t('Lot/Serial Number(s) Required'), - name: product.display_name, - isSingleItem: isAllowOnlyOneLot, - array: packLotLinesToEdit, + } + const { confirmed, payload } = await this.showPopup("EditListPopup", { + title: this.env._t("Lot/Serial Number(s) Required"), + name: product.display_name, + isSingleItem: isAllowOnlyOneLot, + array: packLotLinesToEdit, + }); + if (confirmed) { + // Segregate the old and new packlot lines + const modifiedPackLotLines = Object.fromEntries( + payload.newArray.filter((item) => item.id).map((item) => [item.id, item.text]) + ); + const newPackLotLines = payload.newArray + .filter((item) => !item.id) + .map((item) => ({ lot_name: item.text })); + + draftPackLotLines = { modifiedPackLotLines, newPackLotLines }; + } else { + // We don't proceed on adding product. + return; + } + } + + // Take the weight if necessary. + if (product.to_weight && this.env.pos.config.iface_electronic_scale) { + // Show the ScaleScreen to weigh the product. + if (this.isScaleAvailable) { + const { confirmed, payload } = await this.showTempScreen("ScaleScreen", { + product, }); if (confirmed) { - // Segregate the old and new packlot lines - const modifiedPackLotLines = Object.fromEntries( - payload.newArray.filter(item => item.id).map(item => [item.id, item.text]) - ); - const newPackLotLines = payload.newArray - .filter(item => !item.id) - .map(item => ({ lot_name: item.text })); - - draftPackLotLines = { modifiedPackLotLines, newPackLotLines }; + weight = payload.weight; } else { - // We don't proceed on adding product. + // do not add the product; return; } - } - - // Take the weight if necessary. - if (product.to_weight && this.env.pos.config.iface_electronic_scale) { - // Show the ScaleScreen to weigh the product. - if (this.isScaleAvailable) { - const { confirmed, payload } = await this.showTempScreen('ScaleScreen', { - product, - }); - if (confirmed) { - weight = payload.weight; - } else { - // do not add the product; - return; - } - } else { - await this._onScaleNotAvailable(); - } - } - - if (base_code && this.env.pos.db.product_packaging_by_barcode[base_code.code]) { - weight = this.env.pos.db.product_packaging_by_barcode[base_code.code].qty; - } - - return { draftPackLotLines, quantity: weight, description, price_extra }; - } - async _addProduct(product, options) { - this.currentOrder.add_product(product, options); - } - async _clickProduct(event) { - if (!this.currentOrder) { - this.env.pos.add_new_order(); - } - const product = event.detail; - const options = await this._getAddProductOptions(product); - // Do not add product if options is undefined. - if (!options) return; - // Add the product after having the extra information. - await this._addProduct(product, options); - NumberBuffer.reset(); - } - _setNumpadMode(event) { - const { mode } = event.detail; - NumberBuffer.capture(); - NumberBuffer.reset(); - this.env.pos.numpadMode = mode; - } - _selectLine() { - NumberBuffer.reset(); - } - async _updateSelectedOrderline(event) { - if (this.env.pos.numpadMode === 'quantity' && this.env.pos.disallowLineQuantityChange()) { - let order = this.env.pos.get_order(); - if(!order.orderlines.length) - return; - let selectedLine = order.get_selected_orderline(); - let orderlines = order.orderlines; - let lastId = orderlines.length !== 0 && orderlines.at(orderlines.length - 1).cid; - let currentQuantity = this.env.pos.get_order().get_selected_orderline().get_quantity(); - - if(selectedLine.noDecrease) { - this.showPopup('ErrorPopup', { - title: this.env._t('Invalid action'), - body: this.env._t('You are not allowed to change this quantity'), - }); - return; - } - const parsedInput = event.detail.buffer && parse.float(event.detail.buffer) || 0; - if(lastId != selectedLine.cid) - this._showDecreaseQuantityPopup(); - else if(currentQuantity < parsedInput) - this._setValue(event.detail.buffer); - else if(parsedInput < currentQuantity) - this._showDecreaseQuantityPopup(); } else { - let { buffer } = event.detail; - let val = buffer === null ? 'remove' : buffer; - this._setValue(val); - if (val == 'remove') { - NumberBuffer.reset(); - this.env.pos.numpadMode = 'quantity'; - } + await this._onScaleNotAvailable(); } } - _setValue(val) { - if (this.currentOrder.get_selected_orderline()) { - if (this.env.pos.numpadMode === 'quantity') { - const result = this.currentOrder.get_selected_orderline().set_quantity(val); - if (!result) NumberBuffer.reset(); - } else if (this.env.pos.numpadMode === 'discount') { - this.currentOrder.get_selected_orderline().set_discount(val); - } else if (this.env.pos.numpadMode === 'price') { - var selected_orderline = this.currentOrder.get_selected_orderline(); - selected_orderline.price_manually_set = true; - selected_orderline.set_unit_price(val); - } - } - } - async _barcodeProductAction(code) { - let product = this.env.pos.db.get_product_by_barcode(code.base_code); - if (!product) { - // find the barcode in the backend - let foundProductIds = []; - try { - foundProductIds = await this.rpc({ - model: 'product.product', - method: 'search', - args: [[['barcode', '=', code.base_code]]], - context: this.env.session.user_context, - }); - } catch (error) { - if (isConnectionError(error)) { - return this.showPopup('OfflineErrorPopup', { - title: this.env._t('Network Error'), - body: this.env._t("Product is not loaded. Tried loading the product from the server but there is a network error."), - }); - } else { - throw error; - } - } - if (foundProductIds.length) { - await this.env.pos._addProducts(foundProductIds); - // assume that the result is unique. - product = this.env.pos.db.get_product_by_id(foundProductIds[0]); - } else { - return this._barcodeErrorAction(code); - } - } - const options = await this._getAddProductOptions(product, code); - // Do not proceed on adding the product when no options is returned. - // This is consistent with _clickProduct. - if (!options) return; - // update the options depending on the type of the scanned code - if (code.type === 'price') { - Object.assign(options, { - price: code.value, - extras: { - price_manually_set: true, - }, - }); - } else if (code.type === 'weight') { - Object.assign(options, { - quantity: code.value, - merge: false, - }); - } else if (code.type === 'discount') { - Object.assign(options, { - discount: code.value, - merge: false, - }); - } - this.currentOrder.add_product(product, options) + if (base_code && this.env.pos.db.product_packaging_by_barcode[base_code.code]) { + weight = this.env.pos.db.product_packaging_by_barcode[base_code.code].qty; } - _barcodePartnerAction(code) { - const partner = this.env.pos.db.get_partner_by_barcode(code.code); - if (partner) { - if (this.currentOrder.get_partner() !== partner) { - this.currentOrder.set_partner(partner); - this.currentOrder.set_pricelist( - _.findWhere(this.env.pos.pricelists, { - id: partner.property_product_pricelist[0], - }) || this.env.pos.default_pricelist - ); - } - return true; - } - this._barcodeErrorAction(code); - return false; - } - _barcodeDiscountAction(code) { - var last_orderline = this.currentOrder.get_last_orderline(); - if (last_orderline) { - last_orderline.set_discount(code.value); - } - } - // IMPROVEMENT: The following two methods should be in PosScreenComponent? - // 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) }); - } - _codeRepr(code) { - if (code.code.length > 32) { - return code.code.substring(0, 29) + '...'; - } else { - return code.code; - } - } - async _displayAllControlPopup() { - await this.showPopup('ControlButtonPopup', { - controlButtons: this.controlButtons - }); - } - /** - * override this method to perform procedure if the scale is not available. - * @see isScaleAvailable - */ - async _onScaleNotAvailable() {} - async _showDecreaseQuantityPopup() { - const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', { - startingValue: 0, - title: this.env._t('Set the new quantity'), - }); - let newQuantity = inputNumber && inputNumber !== "" ? parse.float(inputNumber) : null; - if (confirmed && newQuantity !== null) { - let order = this.env.pos.get_order(); - let selectedLine = this.env.pos.get_order().get_selected_orderline(); - let currentQuantity = selectedLine.get_quantity() - if(selectedLine.is_last_line() && currentQuantity === 1 && newQuantity < currentQuantity) - selectedLine.set_quantity(newQuantity); - else if(newQuantity >= currentQuantity) - selectedLine.set_quantity(newQuantity); - else { - let newLine = selectedLine.clone(); - let decreasedQuantity = currentQuantity - newQuantity - newLine.order = order; - newLine.set_quantity( - decreasedQuantity, true); - order.add_orderline(newLine); - } - } + return { draftPackLotLines, quantity: weight, description, price_extra }; + } + async _addProduct(product, options) { + this.currentOrder.add_product(product, options); + } + async _clickProduct(event) { + if (!this.currentOrder) { + this.env.pos.add_new_order(); } - async onClickPartner() { - // 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', { - title: this.env._t("Can't change customer"), - body: _.str.sprintf( - this.env._t( - "This order already has refund lines for %s. We can't change the customer associated to it. Create a new order for the new customer." - ), - currentPartner.name - ), + const product = event.detail; + const options = await this._getAddProductOptions(product); + // Do not add product if options is undefined. + if (!options) { + return; + } + // Add the product after having the extra information. + await this._addProduct(product, options); + NumberBuffer.reset(); + } + _setNumpadMode(event) { + const { mode } = event.detail; + NumberBuffer.capture(); + NumberBuffer.reset(); + this.env.pos.numpadMode = mode; + } + _selectLine() { + NumberBuffer.reset(); + } + async _updateSelectedOrderline(event) { + if (this.env.pos.numpadMode === "quantity" && this.env.pos.disallowLineQuantityChange()) { + const order = this.env.pos.get_order(); + if (!order.orderlines.length) { + return; + } + const selectedLine = order.get_selected_orderline(); + const orderlines = order.orderlines; + const lastId = orderlines.length !== 0 && orderlines.at(orderlines.length - 1).cid; + const currentQuantity = this.env.pos + .get_order() + .get_selected_orderline() + .get_quantity(); + + if (selectedLine.noDecrease) { + this.showPopup("ErrorPopup", { + title: this.env._t("Invalid action"), + body: this.env._t("You are not allowed to change this quantity"), }); return; } - const { confirmed, payload: newPartner } = await this.showTempScreen( - 'PartnerListScreen', - { partner: currentPartner } - ); - if (confirmed) { - this.currentOrder.set_partner(newPartner); - this.currentOrder.updatePricelist(newPartner); + const parsedInput = (event.detail.buffer && parse.float(event.detail.buffer)) || 0; + if (lastId != selectedLine.cid) { + this._showDecreaseQuantityPopup(); + } else if (currentQuantity < parsedInput) { + this._setValue(event.detail.buffer); + } else if (parsedInput < currentQuantity) { + this._showDecreaseQuantityPopup(); } - } - async _onClickPay() { - if (this.env.pos.get_order().orderlines.some(line => line.get_product().tracking !== 'none' && !line.has_valid_product_lot()) && (this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots)) { - 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?'), - confirmText: this.env._t('Yes'), - cancelText: this.env._t('No') - }); - if (confirmed) { - this.showScreen('PaymentScreen'); - } - } else { - this.showScreen('PaymentScreen'); + } else { + const { buffer } = event.detail; + const val = buffer === null ? "remove" : buffer; + this._setValue(val); + if (val == "remove") { + NumberBuffer.reset(); + this.env.pos.numpadMode = "quantity"; } } - switchPane() { - this.state.mobile_pane = this.state.mobile_pane === "left" ? "right" : "left"; - } } - ProductScreen.template = 'ProductScreen'; + _setValue(val) { + if (this.currentOrder.get_selected_orderline()) { + if (this.env.pos.numpadMode === "quantity") { + const result = this.currentOrder.get_selected_orderline().set_quantity(val); + if (!result) { + NumberBuffer.reset(); + } + } else if (this.env.pos.numpadMode === "discount") { + this.currentOrder.get_selected_orderline().set_discount(val); + } else if (this.env.pos.numpadMode === "price") { + var selected_orderline = this.currentOrder.get_selected_orderline(); + selected_orderline.price_manually_set = true; + selected_orderline.set_unit_price(val); + } + } + } + async _barcodeProductAction(code) { + let product = this.env.pos.db.get_product_by_barcode(code.base_code); + if (!product) { + // find the barcode in the backend + let foundProductIds = []; + try { + foundProductIds = await this.rpc({ + model: "product.product", + method: "search", + args: [[["barcode", "=", code.base_code]]], + context: this.env.session.user_context, + }); + } catch (error) { + if (isConnectionError(error)) { + return this.showPopup("OfflineErrorPopup", { + title: this.env._t("Network Error"), + body: this.env._t( + "Product is not loaded. Tried loading the product from the server but there is a network error." + ), + }); + } else { + throw error; + } + } + if (foundProductIds.length) { + await this.env.pos._addProducts(foundProductIds); + // assume that the result is unique. + product = this.env.pos.db.get_product_by_id(foundProductIds[0]); + } else { + return this._barcodeErrorAction(code); + } + } + const options = await this._getAddProductOptions(product, code); + // Do not proceed on adding the product when no options is returned. + // This is consistent with _clickProduct. + if (!options) { + return; + } - Registries.Component.add(ProductScreen); + // update the options depending on the type of the scanned code + if (code.type === "price") { + Object.assign(options, { + price: code.value, + extras: { + price_manually_set: true, + }, + }); + } else if (code.type === "weight") { + Object.assign(options, { + quantity: code.value, + merge: false, + }); + } else if (code.type === "discount") { + Object.assign(options, { + discount: code.value, + merge: false, + }); + } + this.currentOrder.add_product(product, options); + } + _barcodePartnerAction(code) { + const partner = this.env.pos.db.get_partner_by_barcode(code.code); + if (partner) { + if (this.currentOrder.get_partner() !== partner) { + this.currentOrder.set_partner(partner); + this.currentOrder.set_pricelist( + _.findWhere(this.env.pos.pricelists, { + id: partner.property_product_pricelist[0], + }) || this.env.pos.default_pricelist + ); + } + return true; + } + this._barcodeErrorAction(code); + return false; + } + _barcodeDiscountAction(code) { + var last_orderline = this.currentOrder.get_last_orderline(); + if (last_orderline) { + last_orderline.set_discount(code.value); + } + } + // IMPROVEMENT: The following two methods should be in PosScreenComponent? + // 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) }); + } + _codeRepr(code) { + if (code.code.length > 32) { + return code.code.substring(0, 29) + "..."; + } else { + return code.code; + } + } + async _displayAllControlPopup() { + await this.showPopup("ControlButtonPopup", { + controlButtons: this.controlButtons, + }); + } + /** + * override this method to perform procedure if the scale is not available. + * @see isScaleAvailable + */ + async _onScaleNotAvailable() {} + async _showDecreaseQuantityPopup() { + const { confirmed, payload: inputNumber } = await this.showPopup("NumberPopup", { + startingValue: 0, + title: this.env._t("Set the new quantity"), + }); + const newQuantity = inputNumber && inputNumber !== "" ? parse.float(inputNumber) : null; + if (confirmed && newQuantity !== null) { + const order = this.env.pos.get_order(); + const selectedLine = this.env.pos.get_order().get_selected_orderline(); + const currentQuantity = selectedLine.get_quantity(); + if ( + selectedLine.is_last_line() && + currentQuantity === 1 && + newQuantity < currentQuantity + ) { + selectedLine.set_quantity(newQuantity); + } else if (newQuantity >= currentQuantity) { + selectedLine.set_quantity(newQuantity); + } else { + const newLine = selectedLine.clone(); + const decreasedQuantity = currentQuantity - newQuantity; + newLine.order = order; - return ProductScreen; -}); + newLine.set_quantity(-decreasedQuantity, true); + order.add_orderline(newLine); + } + } + } + async onClickPartner() { + // 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", { + title: this.env._t("Can't change customer"), + body: _.str.sprintf( + this.env._t( + "This order already has refund lines for %s. We can't change the customer associated to it. Create a new order for the new customer." + ), + currentPartner.name + ), + }); + return; + } + const { confirmed, payload: newPartner } = await this.showTempScreen("PartnerListScreen", { + partner: currentPartner, + }); + if (confirmed) { + this.currentOrder.set_partner(newPartner); + this.currentOrder.updatePricelist(newPartner); + } + } + async _onClickPay() { + if ( + this.env.pos + .get_order() + .orderlines.some( + (line) => + line.get_product().tracking !== "none" && !line.has_valid_product_lot() + ) && + (this.env.pos.picking_type.use_create_lots || + this.env.pos.picking_type.use_existing_lots) + ) { + 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?" + ), + confirmText: this.env._t("Yes"), + cancelText: this.env._t("No"), + }); + if (confirmed) { + this.showScreen("PaymentScreen"); + } + } else { + this.showScreen("PaymentScreen"); + } + } + switchPane() { + this.state.mobile_pane = this.state.mobile_pane === "left" ? "right" : "left"; + } +} +ProductScreen.template = "ProductScreen"; + +Registries.Component.add(ProductScreen); + +export default 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 17b9988f3ce..e5d027994a8 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,84 +1,88 @@ -odoo.define('point_of_sale.ProductsWidget', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const { useListener } = require("@web/core/utils/hooks"); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import { useListener } from "@web/core/utils/hooks"; +import Registries from "@point_of_sale/js/Registries"; - const { onWillUnmount, useState } = owl; +const { onWillUnmount, useState } = owl; - class ProductsWidget extends PosComponent { - /** - * @param {Object} props - * @param {number?} props.startCategoryId - */ - setup() { - super.setup(); - useListener('switch-category', this._switchCategory); - useListener('update-search', this._updateSearch); - useListener('clear-search', this._clearSearch); - useListener('update-product-list', this._updateProductList); - this.state = useState({ searchWord: '' }); - onWillUnmount(this.onWillUnmount); - } - onWillUnmount() { - this.trigger('toggle-mobile-searchbar', false); - } - get selectedCategoryId() { - return this.env.pos.selectedCategoryId; - } - get searchWord() { - return this.state.searchWord.trim(); - } - get productsToDisplay() { - let list = []; - if (this.searchWord !== '') { - list = this.env.pos.db.search_product_in_category( - this.selectedCategoryId, - this.searchWord - ); - } else { - list = this.env.pos.db.get_product_by_category(this.selectedCategoryId); - } - return list.sort(function (a, b) { return a.display_name.localeCompare(b.display_name) }); - } - get subcategories() { - return this.env.pos.db - .get_category_childs_ids(this.selectedCategoryId) - .map(id => this.env.pos.db.get_category_by_id(id)); - } - get breadcrumbs() { - if (this.selectedCategoryId === this.env.pos.db.root_category_id) return []; - return [ - ...this.env.pos.db - .get_category_ancestors_ids(this.selectedCategoryId) - .slice(1), - this.selectedCategoryId, - ].map(id => this.env.pos.db.get_category_by_id(id)); - } - get hasNoCategories() { - return this.env.pos.db.get_category_childs_ids(0).length === 0; - } - get shouldShowButton() { - return this.productsToDisplay.length === 0 && this.searchWord && !this.env.pos.isEveryProductLoaded; - } - _switchCategory(event) { - this.env.pos.setSelectedCategoryId(event.detail); - } - _updateSearch(event) { - this.state.searchWord = event.detail; - } - _clearSearch() { - this.state.searchWord = ''; - } - _updateProductList(event) { - this.render(true); - this.trigger('switch-category', 0); - } +class ProductsWidget extends PosComponent { + /** + * @param {Object} props + * @param {number?} props.startCategoryId + */ + setup() { + super.setup(); + useListener("switch-category", this._switchCategory); + useListener("update-search", this._updateSearch); + useListener("clear-search", this._clearSearch); + useListener("update-product-list", this._updateProductList); + this.state = useState({ searchWord: "" }); + onWillUnmount(this.onWillUnmount); } - ProductsWidget.template = 'ProductsWidget'; + onWillUnmount() { + this.trigger("toggle-mobile-searchbar", false); + } + get selectedCategoryId() { + return this.env.pos.selectedCategoryId; + } + get searchWord() { + return this.state.searchWord.trim(); + } + get productsToDisplay() { + let list = []; + if (this.searchWord !== "") { + list = this.env.pos.db.search_product_in_category( + this.selectedCategoryId, + this.searchWord + ); + } else { + list = this.env.pos.db.get_product_by_category(this.selectedCategoryId); + } + return list.sort(function (a, b) { + return a.display_name.localeCompare(b.display_name); + }); + } + get subcategories() { + return this.env.pos.db + .get_category_childs_ids(this.selectedCategoryId) + .map((id) => this.env.pos.db.get_category_by_id(id)); + } + get breadcrumbs() { + if (this.selectedCategoryId === this.env.pos.db.root_category_id) { + return []; + } + return [ + ...this.env.pos.db.get_category_ancestors_ids(this.selectedCategoryId).slice(1), + this.selectedCategoryId, + ].map((id) => this.env.pos.db.get_category_by_id(id)); + } + get hasNoCategories() { + return this.env.pos.db.get_category_childs_ids(0).length === 0; + } + get shouldShowButton() { + return ( + this.productsToDisplay.length === 0 && + this.searchWord && + !this.env.pos.isEveryProductLoaded + ); + } + _switchCategory(event) { + this.env.pos.setSelectedCategoryId(event.detail); + } + _updateSearch(event) { + this.state.searchWord = event.detail; + } + _clearSearch() { + this.state.searchWord = ""; + } + _updateProductList(event) { + this.render(true); + this.trigger("switch-category", 0); + } +} +ProductsWidget.template = "ProductsWidget"; - Registries.Component.add(ProductsWidget); +Registries.Component.add(ProductsWidget); - return 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 41d4f813312..07f248bd413 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,99 +1,124 @@ -odoo.define('point_of_sale.ProductsWidgetControlPanel', function(require) { - 'use strict'; +/** @odoo-module */ - const { identifyError } = require('point_of_sale.utils'); - const { ConnectionLostError, ConnectionAbortedError } = require('@web/core/network/rpc_service'); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); - const { debounce } = require("@web/core/utils/timing"); +import { identifyError } from "@point_of_sale/js/utils"; +import { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; +import { debounce } from "@web/core/utils/timing"; - const { onMounted, onWillUnmount } = owl; +const { onMounted, onWillUnmount } = owl; - class ProductsWidgetControlPanel extends PosComponent { - setup() { - super.setup(); - this.updateSearch = debounce(this.updateSearch, 100); - this.state = { searchInput: '' }; +class ProductsWidgetControlPanel extends PosComponent { + setup() { + super.setup(); + this.updateSearch = debounce(this.updateSearch, 100); + this.state = { searchInput: "" }; - onMounted(() => { - this.env.posbus.on('search-product-from-info-popup', this, this.searchProductFromInfo) - if(!this.env.pos.config.limited_products_loading) - this.env.pos.isEveryProductLoaded = true; - }); - - onWillUnmount(() => { - this.env.posbus.off('search-product-from-info-popup', this); - }); - } - _clearSearch() { - this.state.searchInput = ''; - this.trigger('clear-search'); - } - get displayCategImages() { - return Object.values(this.env.pos.db.category_by_id).some(categ => categ.has_image) && !this.env.isMobile; - } - updateSearch(event) { - this.trigger('update-search', this.state.searchInput); - if (event.key === 'Enter') { - this._onPressEnterKey() + onMounted(() => { + this.env.posbus.on("search-product-from-info-popup", this, this.searchProductFromInfo); + if (!this.env.pos.config.limited_products_loading) { + this.env.pos.isEveryProductLoaded = true; } - } - async _onPressEnterKey() { - if (!this.state.searchInput) return; - if (!this.env.pos.isEveryProductLoaded) { - const result = await this.loadProductFromDB(); - this.showNotification( - _.str.sprintf(this.env._t('%s product(s) found for "%s".'), - result.length, - this.state.searchInput) - , 3000); - if (!result.length) this._clearSearch(); - } - } - searchProductFromInfo(productName) { - this.state.searchInput = productName; - this.trigger('switch-category', 0); - this.trigger('update-search', productName); - } - _toggleMobileSearchbar() { - this.trigger('toggle-mobile-searchbar'); - } - async loadProductFromDB() { - if(!this.state.searchInput) - return; + }); - try { - let ProductIds = await this.rpc({ - model: 'product.product', - method: 'search', - args: [['&',['available_in_pos', '=', true], '|','|','|', - ['name', 'ilike', this.state.searchInput], - ['default_code', 'ilike', this.state.searchInput], - ['barcode', 'ilike', this.state.searchInput]]], - context: this.env.session.user_context, - }); - if(ProductIds.length) { - if (!this.env.pos.isEveryProductLoaded) await this.env.pos.updateIsEveryProductLoaded(); - await this.env.pos._addProducts(ProductIds, false); - } - this.trigger('update-product-list'); - return ProductIds; - } catch (error) { - const identifiedError = identifyError(error) - if (identifiedError instanceof ConnectionLostError || identifiedError instanceof ConnectionAbortedError) { - return this.showPopup('OfflineErrorPopup', { - title: this.env._t('Network Error'), - body: this.env._t("Product is not loaded. Tried loading the product from the server but there is a network error."), - }); - } else { - throw error; - } + onWillUnmount(() => { + this.env.posbus.off("search-product-from-info-popup", this); + }); + } + _clearSearch() { + this.state.searchInput = ""; + this.trigger("clear-search"); + } + get displayCategImages() { + return ( + Object.values(this.env.pos.db.category_by_id).some((categ) => categ.has_image) && + !this.env.isMobile + ); + } + updateSearch(event) { + this.trigger("update-search", this.state.searchInput); + if (event.key === "Enter") { + this._onPressEnterKey(); + } + } + async _onPressEnterKey() { + if (!this.state.searchInput) { + return; + } + if (!this.env.pos.isEveryProductLoaded) { + const result = await this.loadProductFromDB(); + this.showNotification( + _.str.sprintf( + this.env._t('%s product(s) found for "%s".'), + result.length, + this.state.searchInput + ), + 3000 + ); + if (!result.length) { + this._clearSearch(); } } } - ProductsWidgetControlPanel.template = 'ProductsWidgetControlPanel'; + searchProductFromInfo(productName) { + this.state.searchInput = productName; + this.trigger("switch-category", 0); + this.trigger("update-search", productName); + } + _toggleMobileSearchbar() { + this.trigger("toggle-mobile-searchbar"); + } + async loadProductFromDB() { + if (!this.state.searchInput) { + return; + } - Registries.Component.add(ProductsWidgetControlPanel); + try { + const ProductIds = await this.rpc({ + model: "product.product", + method: "search", + args: [ + [ + "&", + ["available_in_pos", "=", true], + "|", + "|", + "|", + ["name", "ilike", this.state.searchInput], + ["default_code", "ilike", this.state.searchInput], + ["barcode", "ilike", this.state.searchInput], + ], + ], + context: this.env.session.user_context, + }); + if (ProductIds.length) { + if (!this.env.pos.isEveryProductLoaded) { + await this.env.pos.updateIsEveryProductLoaded(); + } + await this.env.pos._addProducts(ProductIds, false); + } + this.trigger("update-product-list"); + return ProductIds; + } catch (error) { + const identifiedError = identifyError(error); + if ( + identifiedError instanceof ConnectionLostError || + identifiedError instanceof ConnectionAbortedError + ) { + return this.showPopup("OfflineErrorPopup", { + title: this.env._t("Network Error"), + body: this.env._t( + "Product is not loaded. Tried loading the product from the server but there is a network error." + ), + }); + } else { + throw error; + } + } + } +} +ProductsWidgetControlPanel.template = "ProductsWidgetControlPanel"; - return 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 8b358a5bb6c..4f7b5e29f55 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,50 +1,45 @@ -odoo.define('point_of_sale.OrderReceipt', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - const { onWillUpdateProps } = owl; +const { onWillUpdateProps } = owl; - class OrderReceipt extends PosComponent { - setup() { - super.setup(); - this._receiptEnv = this.props.order.getOrderReceiptEnv(); +class OrderReceipt extends PosComponent { + setup() { + super.setup(); + this._receiptEnv = this.props.order.getOrderReceiptEnv(); - onWillUpdateProps((nextProps) => { - this._receiptEnv = nextProps.order.getOrderReceiptEnv(); - }); - } - get receipt() { - return this.receiptEnv.receipt; - } - get orderlines() { - return this.receiptEnv.orderlines; - } - get paymentlines() { - return this.receiptEnv.paymentlines; - } - get isTaxIncluded() { - return Math.abs(this.receipt.subtotal - this.receipt.total_with_tax) <= 0.000001; - } - get receiptEnv () { - return this._receiptEnv; - } - isSimple(line) { - return ( - line.discount === 0 && - line.is_in_unit && - line.quantity === 1 && - !( - line.display_discount_policy == 'without_discount' && - line.price < line.price_lst - ) - ); - } + onWillUpdateProps((nextProps) => { + this._receiptEnv = nextProps.order.getOrderReceiptEnv(); + }); } - OrderReceipt.template = 'OrderReceipt'; + get receipt() { + return this.receiptEnv.receipt; + } + get orderlines() { + return this.receiptEnv.orderlines; + } + get paymentlines() { + return this.receiptEnv.paymentlines; + } + get isTaxIncluded() { + return Math.abs(this.receipt.subtotal - this.receipt.total_with_tax) <= 0.000001; + } + get receiptEnv() { + return this._receiptEnv; + } + isSimple(line) { + return ( + line.discount === 0 && + line.is_in_unit && + line.quantity === 1 && + !(line.display_discount_policy == "without_discount" && line.price < line.price_lst) + ); + } +} +OrderReceipt.template = "OrderReceipt"; - Registries.Component.add(OrderReceipt); +Registries.Component.add(OrderReceipt); - return 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 cb255fa7944..5d4b5407441 100644 --- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -1,139 +1,152 @@ -odoo.define('point_of_sale.ReceiptScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const { Printer } = require('point_of_sale.Printer'); - const { is_email } = require('web.utils'); - const { useErrorHandlers } = require('point_of_sale.custom_hooks'); - const Registries = require('point_of_sale.Registries'); - const AbstractReceiptScreen = require('point_of_sale.AbstractReceiptScreen'); +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"; - const { onMounted, useRef, status } = owl; +const { onMounted, useRef, status } = 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; +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; - 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") { - let images = this.orderReceipt.el.getElementsByTagName('img'); - for (let image of images) { - await image.decode(); - } - await this.handleAutoPrint(); + 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; - } - 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], - }); + 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." + ); } } - ReceiptScreen.template = 'ReceiptScreen'; - return ReceiptScreen; - }; - - Registries.Component.addByExtending(ReceiptScreen, AbstractReceiptScreen); - + 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(); + } + } + } + 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], + }); + } + } + ReceiptScreen.template = "ReceiptScreen"; return ReceiptScreen; -}); +}; + +Registries.Component.addByExtending(ReceiptScreen, AbstractReceiptScreen); + +export default 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 f630ac607e4..282954aefd0 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,13 +1,11 @@ -odoo.define('point_of_sale.WrappedProductNameLines', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class WrappedProductNameLines extends PosComponent {} - WrappedProductNameLines.template = 'WrappedProductNameLines'; +class WrappedProductNameLines extends PosComponent {} +WrappedProductNameLines.template = "WrappedProductNameLines"; - Registries.Component.add(WrappedProductNameLines); +Registries.Component.add(WrappedProductNameLines); - return WrappedProductNameLines; -}); +export default 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 172574a6c42..78b57cb11f4 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,105 +1,102 @@ -odoo.define('point_of_sale.ScaleScreen', function(require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const { round_precision: round_pr } = require('web.utils'); - const Registries = require('point_of_sale.Registries'); +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"; - const { onMounted, onWillUnmount, useExternalListener, useState } = owl; +const { onMounted, onWillUnmount, useExternalListener, useState } = owl; - class ScaleScreen extends PosComponent { - /** - * @param {Object} props - * @param {Object} props.product The product to weight. - */ - setup() { - super.setup(); - useExternalListener(document, 'keyup', this._onHotkeys); - this.state = useState({ weight: 0 }); - onMounted(this.onMounted); - onWillUnmount(this.onWillUnmount); - } - onMounted() { - // start the scale reading - this._readScale(); - } - onWillUnmount() { - // stop the scale reading - this.env.proxy_queue.clear(); - } - back() { - this.props.resolve({ confirmed: false, payload: null }); - this.trigger('close-temp-screen'); - } - confirm() { - this.props.resolve({ - confirmed: true, - payload: { weight: this.state.weight }, - }); - this.trigger('close-temp-screen'); - } - _onHotkeys(event) { - if (event.key === 'Escape') { - this.back(); - } else if (event.key === 'Enter') { - this.confirm(); - } - } - _readScale() { - this.env.proxy_queue.schedule(this._setWeight.bind(this), { - duration: 500, - repeat: true, - }); - } - async _setWeight() { - const reading = await this.env.proxy.scale_read(); - this.state.weight = reading.weight; - } - get _activePricelist() { - const current_order = this.env.pos.get_order(); - let current_pricelist = this.env.pos.default_pricelist; - if (current_order) { - current_pricelist = current_order.pricelist; - } - return current_pricelist; - } - get productWeightString() { - const defaultstr = (this.state.weight || 0).toFixed(3) + ' Kg'; - if (!this.props.product || !this.env.pos) { - return defaultstr; - } - const unit_id = this.props.product.uom_id; - if (!unit_id) { - return defaultstr; - } - const unit = this.env.pos.units_by_id[unit_id[0]]; - const weight = round_pr(this.state.weight || 0, unit.rounding); - let weightstr = weight.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))); - weightstr += ' ' + unit.name; - return weightstr; - } - get computedPriceString() { - return this.env.pos.format_currency(this.productPrice * this.state.weight); - } - get productPrice() { - const product = this.props.product; - return (product ? product.get_price(this._activePricelist, this.state.weight) : 0) || 0; - } - get productName() { - return ( - (this.props.product ? this.props.product.display_name : undefined) || - 'Unnamed Product' - ); - } - get productUom() { - return this.props.product - ? this.env.pos.units_by_id[this.props.product.uom_id[0]].name - : ''; +class ScaleScreen extends PosComponent { + /** + * @param {Object} props + * @param {Object} props.product The product to weight. + */ + setup() { + super.setup(); + useExternalListener(document, "keyup", this._onHotkeys); + this.state = useState({ weight: 0 }); + onMounted(this.onMounted); + onWillUnmount(this.onWillUnmount); + } + onMounted() { + // start the scale reading + this._readScale(); + } + onWillUnmount() { + // stop the scale reading + this.env.proxy_queue.clear(); + } + back() { + this.props.resolve({ confirmed: false, payload: null }); + this.trigger("close-temp-screen"); + } + confirm() { + this.props.resolve({ + confirmed: true, + payload: { weight: this.state.weight }, + }); + this.trigger("close-temp-screen"); + } + _onHotkeys(event) { + if (event.key === "Escape") { + this.back(); + } else if (event.key === "Enter") { + this.confirm(); } } - ScaleScreen.template = 'ScaleScreen'; + _readScale() { + this.env.proxy_queue.schedule(this._setWeight.bind(this), { + duration: 500, + repeat: true, + }); + } + async _setWeight() { + const reading = await this.env.proxy.scale_read(); + this.state.weight = reading.weight; + } + get _activePricelist() { + const current_order = this.env.pos.get_order(); + let current_pricelist = this.env.pos.default_pricelist; + if (current_order) { + current_pricelist = current_order.pricelist; + } + return current_pricelist; + } + get productWeightString() { + const defaultstr = (this.state.weight || 0).toFixed(3) + " Kg"; + if (!this.props.product || !this.env.pos) { + return defaultstr; + } + const unit_id = this.props.product.uom_id; + if (!unit_id) { + return defaultstr; + } + const unit = this.env.pos.units_by_id[unit_id[0]]; + const weight = round_pr(this.state.weight || 0, unit.rounding); + let weightstr = weight.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))); + weightstr += " " + unit.name; + return weightstr; + } + get computedPriceString() { + return this.env.pos.format_currency(this.productPrice * this.state.weight); + } + get productPrice() { + const product = this.props.product; + return (product ? product.get_price(this._activePricelist, this.state.weight) : 0) || 0; + } + get productName() { + return ( + (this.props.product ? this.props.product.display_name : undefined) || "Unnamed Product" + ); + } + get productUom() { + return this.props.product + ? this.env.pos.units_by_id[this.props.product.uom_id[0]].name + : ""; + } +} +ScaleScreen.template = "ScaleScreen"; - Registries.Component.add(ScaleScreen); +Registries.Component.add(ScaleScreen); - return ScaleScreen; -}); +export default 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 a13925a51fb..fd6c8005e6b 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,128 +1,131 @@ -odoo.define('point_of_sale.InvoiceButton', function (require) { - 'use strict'; +/** @odoo-module */ - const { useListener } = require("@web/core/utils/hooks"); - const { isConnectionError } = require('point_of_sale.utils'); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import { useListener } from "@web/core/utils/hooks"; +import { isConnectionError } from "@point_of_sale/js/utils"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class InvoiceButton extends PosComponent { - setup() { - super.setup(); - useListener('click', this._onClick); +class InvoiceButton extends PosComponent { + setup() { + super.setup(); + useListener("click", this._onClick); + } + get isAlreadyInvoiced() { + if (!this.props.order) { + return false; } - get isAlreadyInvoiced() { - if (!this.props.order) return false; - return Boolean(this.props.order.account_move); + return Boolean(this.props.order.account_move); + } + get commandName() { + if (!this.props.order) { + return this.env._t("Invoice"); + } else { + return this.isAlreadyInvoiced ? this.env._t("Reprint Invoice") : this.env._t("Invoice"); } - get commandName() { - if (!this.props.order) { - return this.env._t('Invoice'); + } + async _downloadInvoice(orderId) { + try { + const [orderWithInvoice] = await this.rpc({ + method: "read", + model: "pos.order", + args: [orderId, ["account_move"]], + kwargs: { load: false }, + }); + if (orderWithInvoice && orderWithInvoice.account_move) { + await this.env.legacyActionManager.do_action("account.account_invoices", { + additional_context: { + active_ids: [orderWithInvoice.account_move], + }, + }); + } + } catch (error) { + if (error instanceof Error) { + throw error; } else { - return this.isAlreadyInvoiced - ? this.env._t('Reprint Invoice') - : this.env._t('Invoice'); - } - } - async _downloadInvoice(orderId) { - try { - const [orderWithInvoice] = await this.rpc({ - method: 'read', - model: 'pos.order', - args: [orderId, ['account_move']], - kwargs: { load: false }, + // NOTE: error here is most probably undefined + this.showPopup("ErrorPopup", { + title: this.env._t("Network Error"), + body: this.env._t("Unable to download invoice."), }); - if (orderWithInvoice && orderWithInvoice.account_move) { - await this.env.legacyActionManager.do_action('account.account_invoices', { - additional_context: { - active_ids: [orderWithInvoice.account_move], - }, - }); - } - } catch (error) { - if (error instanceof Error) { - throw error; - } else { - // NOTE: error here is most probably undefined - this.showPopup('ErrorPopup', { - title: this.env._t('Network Error'), - body: this.env._t('Unable to download invoice.'), - }); - } - } - } - async _invoiceOrder() { - const order = this.props.order; - if (!order) return; - - const orderId = order.backendId; - - // Part 0. If already invoiced, print the invoice. - if (this.isAlreadyInvoiced) { - await this._downloadInvoice(orderId); - return; - } - - // Part 1: Handle missing partner. - // Write to pos.order the selected partner. - if (!order.get_partner()) { - 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?'), - }); - if (!confirmedPopup) return; - - const { confirmed: confirmedTempScreen, payload: newPartner } = await this.showTempScreen( - 'PartnerListScreen' - ); - if (!confirmedTempScreen) return; - - await this.rpc({ - model: 'pos.order', - method: 'write', - args: [[orderId], { partner_id: newPartner.id }], - kwargs: { context: this.env.session.user_context }, - }); - } - - // Part 2: Invoice the order. - await this.rpc( - { - model: 'pos.order', - method: 'action_pos_order_invoice', - args: [orderId], - kwargs: { context: this.env.session.user_context }, - }, - { - timeout: 30000, - shadow: true, - } - ); - - // Part 3: Download invoice. - await this._downloadInvoice(orderId); - this.trigger('order-invoiced', orderId); - } - async _onClick() { - try { - this.el.style.pointerEvents = 'none'; - await this._invoiceOrder(); - } catch (error) { - if (isConnectionError(error)) { - this.showPopup('ErrorPopup', { - title: this.env._t('Network Error'), - body: this.env._t('Unable to invoice order.'), - }); - } else { - throw error; - } - } finally { - this.el.style.pointerEvents = 'auto'; } } } - InvoiceButton.template = 'InvoiceButton'; - Registries.Component.add(InvoiceButton); + async _invoiceOrder() { + const order = this.props.order; + if (!order) { + return; + } - return InvoiceButton; -}); + const orderId = order.backendId; + + // Part 0. If already invoiced, print the invoice. + if (this.isAlreadyInvoiced) { + await this._downloadInvoice(orderId); + return; + } + + // Part 1: Handle missing partner. + // Write to pos.order the selected partner. + if (!order.get_partner()) { + 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?"), + }); + if (!confirmedPopup) { + return; + } + + const { confirmed: confirmedTempScreen, payload: newPartner } = + await this.showTempScreen("PartnerListScreen"); + if (!confirmedTempScreen) { + return; + } + + await this.rpc({ + model: "pos.order", + method: "write", + args: [[orderId], { partner_id: newPartner.id }], + kwargs: { context: this.env.session.user_context }, + }); + } + + // Part 2: Invoice the order. + await this.rpc( + { + model: "pos.order", + method: "action_pos_order_invoice", + args: [orderId], + kwargs: { context: this.env.session.user_context }, + }, + { + timeout: 30000, + shadow: true, + } + ); + + // Part 3: Download invoice. + await this._downloadInvoice(orderId); + this.trigger("order-invoiced", orderId); + } + async _onClick() { + try { + this.el.style.pointerEvents = "none"; + await this._invoiceOrder(); + } catch (error) { + if (isConnectionError(error)) { + this.showPopup("ErrorPopup", { + title: this.env._t("Network Error"), + body: this.env._t("Unable to invoice order."), + }); + } else { + throw error; + } + } finally { + this.el.style.pointerEvents = "auto"; + } + } +} +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 1306f51494c..6eb5d8bc97c 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,22 +1,22 @@ -odoo.define('point_of_sale.ReprintReceiptButton', function (require) { - 'use strict'; +/** @odoo-module */ - const { useListener } = require("@web/core/utils/hooks"); - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import { useListener } from "@web/core/utils/hooks"; +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - class ReprintReceiptButton extends PosComponent { - setup() { - super.setup(); - useListener('click', this._onClick); - } - async _onClick() { - if (!this.props.order) return; - this.showScreen('ReprintReceiptScreen', { order: this.props.order }); - } +class ReprintReceiptButton extends PosComponent { + setup() { + super.setup(); + useListener("click", this._onClick); } - ReprintReceiptButton.template = 'ReprintReceiptButton'; - Registries.Component.add(ReprintReceiptButton); + async _onClick() { + if (!this.props.order) { + return; + } + this.showScreen("ReprintReceiptScreen", { order: this.props.order }); + } +} +ReprintReceiptButton.template = "ReprintReceiptButton"; +Registries.Component.add(ReprintReceiptButton); - return 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 9c60498c58b..ffabdb098c8 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,29 +1,27 @@ -odoo.define('point_of_sale.OrderDetails', function (require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; - /** - * @props {models.Order} order - */ - class OrderDetails extends PosComponent { - get order() { - return this.props.order; - } - get orderlines() { - return this.order ? this.order.orderlines : []; - } - get total() { - return this.env.pos.format_currency(this.order ? this.order.get_total_with_tax() : 0); - } - get tax() { - return this.env.pos.format_currency(this.order ? this.order.get_total_tax() : 0) - } +/** + * @props {models.Order} order + */ +class OrderDetails extends PosComponent { + get order() { + return this.props.order; } - OrderDetails.template = 'OrderDetails'; + get orderlines() { + return this.order ? this.order.orderlines : []; + } + get total() { + return this.env.pos.format_currency(this.order ? this.order.get_total_with_tax() : 0); + } + get tax() { + return this.env.pos.format_currency(this.order ? this.order.get_total_tax() : 0); + } +} +OrderDetails.template = "OrderDetails"; - Registries.Component.add(OrderDetails); +Registries.Component.add(OrderDetails); - return 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 8af3df65ab4..b4de4db9c77 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,81 +1,79 @@ -odoo.define('point_of_sale.OrderlineDetails', function (require) { - 'use strict'; +/** @odoo-module */ - const PosComponent = require('point_of_sale.PosComponent'); - const Registries = require('point_of_sale.Registries'); - const { format } = require('web.field_utils'); - const { round_precision: round_pr } = require('web.utils'); +import PosComponent from "@point_of_sale/js/PosComponent"; +import Registries from "@point_of_sale/js/Registries"; +import { format } from "web.field_utils"; +import { round_precision as round_pr } from "web.utils"; - /** - * @props {pos.order.line} line - */ - class OrderlineDetails extends PosComponent { - get line() { - const line = this.props.line; - const formatQty = (line) => { - const quantity = line.get_quantity(); - const unit = line.get_unit(); - const decimals = this.env.pos.dp['Product Unit of Measure']; - const rounding = Math.max(unit.rounding, Math.pow(10, -decimals)); - const roundedQuantity = round_pr(quantity, rounding); - return format.float(roundedQuantity, { digits: [69, decimals] }); - }; - return { - productName: line.get_full_product_name(), - totalPrice: line.get_price_with_tax(), - quantity: formatQty(line), - unit: line.get_unit().name, - unitPrice: line.get_unit_price(), - }; - } - get productName() { - return this.line.productName; - } - get totalPrice() { - return this.env.pos.format_currency(this.line.totalPrice); - } - get quantity() { - return this.line.quantity; - } - get unitPrice() { - return this.env.pos.format_currency(this.line.unitPrice); - } - get unit() { - return this.line.unit; - } - get pricePerUnit() { - return ` ${this.unit} at ${this.unitPrice} / ${this.unit}`; - } - get customerNote() { - return this.props.line.get_customer_note(); - } - getToRefundDetail() { - return this.env.pos.toRefundLines[this.props.line.id]; - } - hasRefundedQty() { - return !this.env.pos.isProductQtyZero(this.props.line.refunded_qty); - } - getFormattedRefundedQty() { - return this.env.pos.formatProductQty(this.props.line.refunded_qty); - } - hasToRefundQty() { - const toRefundDetail = this.getToRefundDetail(); - return !this.env.pos.isProductQtyZero(toRefundDetail && toRefundDetail.qty); - } - getFormattedToRefundQty() { - const toRefundDetail = this.getToRefundDetail(); - return this.env.pos.formatProductQty(toRefundDetail && toRefundDetail.qty); - } - getRefundingMessage() { - return _.str.sprintf(this.env._t('Refunding %s in '), this.getFormattedToRefundQty()); - } - getToRefundMessage() { - return _.str.sprintf(this.env._t('To Refund: %s'), this.getFormattedToRefundQty()); - } +/** + * @props {pos.order.line} line + */ +class OrderlineDetails extends PosComponent { + get line() { + const line = this.props.line; + const formatQty = (line) => { + const quantity = line.get_quantity(); + const unit = line.get_unit(); + const decimals = this.env.pos.dp["Product Unit of Measure"]; + const rounding = Math.max(unit.rounding, Math.pow(10, -decimals)); + const roundedQuantity = round_pr(quantity, rounding); + return format.float(roundedQuantity, { digits: [69, decimals] }); + }; + return { + productName: line.get_full_product_name(), + totalPrice: line.get_price_with_tax(), + quantity: formatQty(line), + unit: line.get_unit().name, + unitPrice: line.get_unit_price(), + }; } - OrderlineDetails.template = 'OrderlineDetails'; + get productName() { + return this.line.productName; + } + get totalPrice() { + return this.env.pos.format_currency(this.line.totalPrice); + } + get quantity() { + return this.line.quantity; + } + get unitPrice() { + return this.env.pos.format_currency(this.line.unitPrice); + } + get unit() { + return this.line.unit; + } + get pricePerUnit() { + return ` ${this.unit} at ${this.unitPrice} / ${this.unit}`; + } + get customerNote() { + return this.props.line.get_customer_note(); + } + getToRefundDetail() { + return this.env.pos.toRefundLines[this.props.line.id]; + } + hasRefundedQty() { + return !this.env.pos.isProductQtyZero(this.props.line.refunded_qty); + } + getFormattedRefundedQty() { + return this.env.pos.formatProductQty(this.props.line.refunded_qty); + } + hasToRefundQty() { + const toRefundDetail = this.getToRefundDetail(); + return !this.env.pos.isProductQtyZero(toRefundDetail && toRefundDetail.qty); + } + getFormattedToRefundQty() { + const toRefundDetail = this.getToRefundDetail(); + return this.env.pos.formatProductQty(toRefundDetail && toRefundDetail.qty); + } + getRefundingMessage() { + return _.str.sprintf(this.env._t("Refunding %s in "), this.getFormattedToRefundQty()); + } + getToRefundMessage() { + return _.str.sprintf(this.env._t("To Refund: %s"), this.getFormattedToRefundQty()); + } +} +OrderlineDetails.template = "OrderlineDetails"; - Registries.Component.add(OrderlineDetails); +Registries.Component.add(OrderlineDetails); - return 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 f860ba47150..22b5050b08d 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,36 +1,35 @@ -odoo.define('point_of_sale.ReprintReceiptScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const AbstractReceiptScreen = require('point_of_sale.AbstractReceiptScreen'); - const Registries = require('point_of_sale.Registries'); +import AbstractReceiptScreen from "@point_of_sale/js/Misc/AbstractReceiptScreen"; +import Registries from "@point_of_sale/js/Registries"; - 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) { - let result = await this._printReceipt(); - if(result) - this.showScreen('TicketScreen', { reuseSavedUIState: true }); +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 }); } } - async tryReprint() { - await this._printReceipt(); - } } - ReprintReceiptScreen.template = 'ReprintReceiptScreen'; - return ReprintReceiptScreen; - }; - Registries.Component.addByExtending(ReprintReceiptScreen, AbstractReceiptScreen); - + async tryReprint() { + await this._printReceipt(); + } + } + ReprintReceiptScreen.template = "ReprintReceiptScreen"; return ReprintReceiptScreen; -}); +}; +Registries.Component.addByExtending(ReprintReceiptScreen, AbstractReceiptScreen); + +export default 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 c1ef0c25d3d..cbec4b7221d 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,658 +1,703 @@ -odoo.define('point_of_sale.TicketScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const { Order } = require('point_of_sale.models'); - const Registries = require('point_of_sale.Registries'); - const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen'); - const NumberBuffer = require('point_of_sale.NumberBuffer'); - const { useListener } = require("@web/core/utils/hooks"); - const { parse } = require('web.field_utils'); +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 { useListener } from "@web/core/utils/hooks"; +import { parse } from "web.field_utils"; - const { onMounted, onWillUnmount, useState } = owl; +const { onMounted, onWillUnmount, useState } = owl; - class TicketScreen extends IndependentToOrderScreen { - setup() { - super.setup(); - useListener('close-screen', this._onCloseScreen); - useListener('filter-selected', this._onFilterSelected); - useListener('search', this._onSearch); - useListener('click-order', this._onClickOrder); - useListener('create-new-order', this._onCreateNewOrder); - useListener('delete-order', this._onDeleteOrder); - useListener('next-page', this._onNextPage); - useListener('prev-page', this._onPrevPage); - useListener('order-invoiced', this._onInvoiceOrder); - useListener('click-order-line', this._onClickOrderline); - useListener('click-refund-order-uid', this._onClickRefundOrderUid); - useListener('update-selected-orderline', this._onUpdateSelectedOrderline); - useListener('do-refund', this._onDoRefund); - NumberBuffer.use({ - nonKeyboardInputEvent: 'numpad-click-input', - triggerAtInput: 'update-selected-orderline', - }); - this._state = this.env.pos.TICKET_SCREEN_STATE; - this.state = useState({ - showSearchBar: !this.env.isMobile, - }); - const defaultUIState = this.props.reuseSavedUIState - ? this._state.ui - : { - selectedSyncedOrderId: null, - searchDetails: this.env.pos.getDefaultSearchDetails(), - filter: null, - selectedOrderlineIds: {}, - }; - Object.assign(this._state.ui, defaultUIState, this.props.ui || {}); +class TicketScreen extends IndependentToOrderScreen { + setup() { + super.setup(); + useListener("close-screen", this._onCloseScreen); + useListener("filter-selected", this._onFilterSelected); + useListener("search", this._onSearch); + useListener("click-order", this._onClickOrder); + useListener("create-new-order", this._onCreateNewOrder); + useListener("delete-order", this._onDeleteOrder); + useListener("next-page", this._onNextPage); + useListener("prev-page", this._onPrevPage); + useListener("order-invoiced", this._onInvoiceOrder); + useListener("click-order-line", this._onClickOrderline); + useListener("click-refund-order-uid", this._onClickRefundOrderUid); + useListener("update-selected-orderline", this._onUpdateSelectedOrderline); + useListener("do-refund", this._onDoRefund); + NumberBuffer.use({ + nonKeyboardInputEvent: "numpad-click-input", + triggerAtInput: "update-selected-orderline", + }); + this._state = this.env.pos.TICKET_SCREEN_STATE; + this.state = useState({ + showSearchBar: !this.env.isMobile, + }); + const defaultUIState = this.props.reuseSavedUIState + ? this._state.ui + : { + selectedSyncedOrderId: null, + searchDetails: this.env.pos.getDefaultSearchDetails(), + filter: null, + selectedOrderlineIds: {}, + }; + Object.assign(this._state.ui, defaultUIState, this.props.ui || {}); - onMounted(this.onMounted); - onWillUnmount(this.onWillUnmount); - } - //#region LIFECYCLE METHODS - onMounted() { - this.env.posbus.on('ticket-button-clicked', this, this.close); - setTimeout(() => { - // Show updated list of synced orders when going back to the screen. - this._onFilterSelected({ detail: { filter: this._state.ui.filter } }); - }); - } - onWillUnmount() { - this.env.posbus.off('ticket-button-clicked', this); - } - //#endregion - //#region EVENT HANDLERS - _onCloseScreen() { - this.close(); - } - async _onFilterSelected(event) { - this._state.ui.filter = event.detail.filter; - if (this._state.ui.filter == 'SYNCED') { - await this._fetchSyncedOrders(); - } - } - async _onSearch(event) { - Object.assign(this._state.ui.searchDetails, event.detail); - if (this._state.ui.filter == 'SYNCED') { - this._state.syncedOrders.currentPage = 1; - await this._fetchSyncedOrders(); - } - } - _onClickOrder({ detail: clickedOrder }) { - if (!clickedOrder || clickedOrder.locked) { - if (this._state.ui.selectedSyncedOrderId == clickedOrder.backendId) { - this._state.ui.selectedSyncedOrderId = null; - } else { - this._state.ui.selectedSyncedOrderId = clickedOrder.backendId; - } - if (!this.getSelectedOrderlineId()) { - // Automatically select the first orderline of the selected order. - const firstLine = clickedOrder.get_orderlines()[0]; - if (firstLine) { - this._state.ui.selectedOrderlineIds[clickedOrder.backendId] = firstLine.id; - } - } - NumberBuffer.reset(); - } else { - this._setOrder(clickedOrder); - } - } - _onCreateNewOrder() { - this.env.pos.add_new_order(); - this.showScreen('ProductScreen'); - } - _selectNextOrder(currentOrder) { - const currentOrderIndex = this._getOrderList().indexOf(currentOrder); - const orderList = this._getOrderList(); - this.env.pos.set_order(orderList[currentOrderIndex+1] || orderList[currentOrderIndex-1]); - } - async _onDeleteOrder({ detail: order }) { - const screen = order.get_screen_data(); - if (['ProductScreen', 'PaymentScreen'].includes(screen.name) && order.get_orderlines().length > 0) { - const { confirmed } = await this.showPopup('ConfirmPopup', { - title: this.env._t('Existing orderlines'), - body: _.str.sprintf( - this.env._t('%s has a total amount of %s, are you sure you want to delete this order ?'), - order.name, this.getTotal(order) - ), - }); - if (!confirmed) return; - } - if (order && (await this._onBeforeDeleteOrder(order))) { - if (order === this.env.pos.get_order()) { - this._selectNextOrder(order); - } - this.env.pos.removeOrder(order); - } - } - async _onNextPage() { - if (this._state.syncedOrders.currentPage < this._getLastPage()) { - this._state.syncedOrders.currentPage += 1; - await this._fetchSyncedOrders(); - } - } - async _onPrevPage() { - if (this._state.syncedOrders.currentPage > 1) { - this._state.syncedOrders.currentPage -= 1; - await this._fetchSyncedOrders(); - } - } - async _onInvoiceOrder({ detail: orderId }) { - this.env.pos._invalidateSyncedOrdersCache([orderId]); + onMounted(this.onMounted); + onWillUnmount(this.onWillUnmount); + } + //#region LIFECYCLE METHODS + onMounted() { + this.env.posbus.on("ticket-button-clicked", this, this.close); + setTimeout(() => { + // Show updated list of synced orders when going back to the screen. + this._onFilterSelected({ detail: { filter: this._state.ui.filter } }); + }); + } + onWillUnmount() { + this.env.posbus.off("ticket-button-clicked", this); + } + //#endregion + //#region EVENT HANDLERS + _onCloseScreen() { + this.close(); + } + async _onFilterSelected(event) { + this._state.ui.filter = event.detail.filter; + if (this._state.ui.filter == "SYNCED") { await this._fetchSyncedOrders(); } - _onClickOrderline({ detail: orderline }) { - const order = this.getSelectedSyncedOrder(); - this._state.ui.selectedOrderlineIds[order.backendId] = orderline.id; + } + async _onSearch(event) { + Object.assign(this._state.ui.searchDetails, event.detail); + if (this._state.ui.filter == "SYNCED") { + this._state.syncedOrders.currentPage = 1; + await this._fetchSyncedOrders(); + } + } + _onClickOrder({ detail: clickedOrder }) { + if (!clickedOrder || clickedOrder.locked) { + if (this._state.ui.selectedSyncedOrderId == clickedOrder.backendId) { + this._state.ui.selectedSyncedOrderId = null; + } else { + this._state.ui.selectedSyncedOrderId = clickedOrder.backendId; + } + if (!this.getSelectedOrderlineId()) { + // Automatically select the first orderline of the selected order. + const firstLine = clickedOrder.get_orderlines()[0]; + if (firstLine) { + this._state.ui.selectedOrderlineIds[clickedOrder.backendId] = firstLine.id; + } + } NumberBuffer.reset(); + } else { + this._setOrder(clickedOrder); } - _onClickRefundOrderUid({ detail: orderUid }) { - // Open the refund order. - const refundOrder = this.env.pos.orders.find((order) => order.uid == orderUid); - if (refundOrder) { - this._setOrder(refundOrder); - } - } - _onUpdateSelectedOrderline({ detail }) { - const buffer = detail.buffer; - const order = this.getSelectedSyncedOrder(); - if (!order) return NumberBuffer.reset(); - - const selectedOrderlineId = this.getSelectedOrderlineId(); - const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); - if (!orderline) 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(); - - const refundableQty = toRefundDetail.orderline.qty - toRefundDetail.orderline.refundedQty; - if (refundableQty <= 0) return NumberBuffer.reset(); - - if (buffer == null || buffer == '') { - toRefundDetail.qty = 0; - } else { - const quantity = Math.abs(parse.float(buffer)); - if (quantity > refundableQty) { - NumberBuffer.reset(); - this.showPopup('ErrorPopup', { - title: this.env._t('Maximum Exceeded'), - body: _.str.sprintf( - this.env._t( - 'The requested quantity to be refunded is higher than the ordered quantity. %s is requested while only %s can be refunded.' - ), - quantity, - refundableQty - ), - }); - } else { - toRefundDetail.qty = quantity; - } - } - } - async _onDoRefund() { - const order = this.getSelectedSyncedOrder(); - - if (this._doesOrderHaveSoleItem(order)) { - if (!this._prepareAutoRefundOnOrder(order)) { - // Don't proceed on refund if preparation returned false. - return; - } - } - - if (!order) { - this._state.ui.highlightHeaderNote = !this._state.ui.highlightHeaderNote; - return; - } - - const partner = order.get_partner(); - - const allToRefundDetails = this._getRefundableDetails(partner); - if (allToRefundDetails.length == 0) { - this._state.ui.highlightHeaderNote = !this._state.ui.highlightHeaderNote; - return; - } - - // The order that will contain the refund orderlines. - // Use the destinationOrder from props if the order to refund has the same - // partner as the destinationOrder. - const destinationOrder = - this.props.destinationOrder && partner === this.props.destinationOrder.get_partner() - ? this.props.destinationOrder - : this._getEmptyOrder(partner); - - // Add orderline for each toRefundDetail to the destinationOrder. - for (const refundDetail of allToRefundDetails) { - const product = this.env.pos.db.get_product_by_id(refundDetail.orderline.productId); - const options = this._prepareRefundOrderlineOptions(refundDetail); - await destinationOrder.add_product(product, options); - refundDetail.destinationOrderUid = destinationOrder.uid; - } - - // Set the partner to the destinationOrder. - if (partner && !destinationOrder.get_partner()) { - destinationOrder.set_partner(partner); - destinationOrder.updatePricelist(partner); - } - - if (this.env.pos.get_order().cid !== destinationOrder.cid) { - this.env.pos.set_order(destinationOrder); - } - - this._onCloseScreen(); - } - //#endregion - //#region PUBLIC METHODS - close() { - /** - * Automatically create new order when there is no currently active order. - * Important in fiscal modules to keep the sequence of the orders. - */ - if (this.env.pos.orders.length == 0) { - this.env.pos.add_new_order(); - } - super.close(); - } - getSelectedSyncedOrder() { - if (this._state.ui.filter == 'SYNCED') { - return this._state.syncedOrders.cache[this._state.ui.selectedSyncedOrderId]; - } else { - return null; - } - } - getSelectedOrderlineId() { - return this._state.ui.selectedOrderlineIds[this._state.ui.selectedSyncedOrderId]; - } - /** - * Override to conditionally show the new ticket button. - */ - shouldShowNewOrderButton() { - return true; - } - getFilteredOrderList() { - if (this._state.ui.filter == 'SYNCED') return this._state.syncedOrders.toShow; - const filterCheck = (order) => { - if (this._state.ui.filter && this._state.ui.filter !== 'ACTIVE_ORDERS') { - const screen = order.get_screen_data(); - return this._state.ui.filter === this._getScreenToStatusMap()[screen.name]; - } - return true; - }; - const { fieldName, searchTerm } = this._state.ui.searchDetails; - const searchField = this._getSearchFields()[fieldName]; - const searchCheck = (order) => { - if (!searchField) return true; - const repr = searchField.repr(order); - if (repr === null) return true; - if (!searchTerm) return true; - return repr && repr.toString().toLowerCase().includes(searchTerm.toLowerCase()); - }; - const predicate = (order) => { - return filterCheck(order) && searchCheck(order); - }; - return this._getOrderList().filter(predicate); - } - getDate(order) { - return moment(order.validation_date).format('YYYY-MM-DD hh:mm A'); - } - getTotal(order) { - return this.env.pos.format_currency(order.get_total_with_tax()); - } - getPartner(order) { - return order.get_partner_name(); - } - getCardholderName(order) { - return order.get_cardholder_name(); - } - getCashier(order) { - return order.cashier ? order.cashier.name : ''; - } - getStatus(order) { - if (order.locked) { - return this.env._t('Paid'); - } else { - const screen = order.get_screen_data(); - return this._getOrderStates().get(this._getScreenToStatusMap()[screen.name]).text; - } - } - /** - * If the order is the only order and is empty - */ - isDefaultOrderEmpty(order) { - let status = this._getScreenToStatusMap()[order.get_screen_data().name]; - let productScreenStatus = this._getScreenToStatusMap().ProductScreen; - return order.get_orderlines().length === 0 && this.env.pos.get_order_list().length === 1 && - status === productScreenStatus && order.get_paymentlines().length === 0; - } - /** - * Hide the delete button if one of the payments is a 'done' electronic payment. - */ - shouldHideDeleteButton(order) { - return ( - this.isDefaultOrderEmpty(order)|| - order.locked || - order - .get_paymentlines() - .some((payment) => payment.is_electronic() && payment.get_payment_status() === 'done') - ); - } - isHighlighted(order) { - if (this._state.ui.filter == 'SYNCED') { - const selectedOrder = this.getSelectedSyncedOrder(); - return selectedOrder ? order.backendId == selectedOrder.backendId : false; - } else { - const activeOrder = this.env.pos.get_order(); - return activeOrder ? activeOrder.uid == order.uid : false; - } - } - showCardholderName() { - return this.env.pos.payment_methods.some((method) => method.use_payment_terminal); - } - getSearchBarConfig() { - return { - searchFields: new Map( - Object.entries(this._getSearchFields()).map(([key, val]) => [key, val.displayName]) + } + _onCreateNewOrder() { + this.env.pos.add_new_order(); + this.showScreen("ProductScreen"); + } + _selectNextOrder(currentOrder) { + const currentOrderIndex = this._getOrderList().indexOf(currentOrder); + const orderList = this._getOrderList(); + this.env.pos.set_order( + orderList[currentOrderIndex + 1] || orderList[currentOrderIndex - 1] + ); + } + async _onDeleteOrder({ detail: order }) { + const screen = order.get_screen_data(); + if ( + ["ProductScreen", "PaymentScreen"].includes(screen.name) && + order.get_orderlines().length > 0 + ) { + const { confirmed } = await this.showPopup("ConfirmPopup", { + title: this.env._t("Existing orderlines"), + body: _.str.sprintf( + this.env._t( + "%s has a total amount of %s, are you sure you want to delete this order ?" + ), + order.name, + this.getTotal(order) ), - filter: { show: true, options: this._getFilterOptions() }, - defaultSearchDetails: this._state.ui.searchDetails, - defaultFilter: this._state.ui.filter, - }; - } - shouldShowPageControls() { - return this._state.ui.filter == 'SYNCED' && this._getLastPage() > 1; - } - getPageNumber() { - if (!this._state.syncedOrders.totalCount) { - return `1/1`; - } else { - return `${this._state.syncedOrders.currentPage}/${this._getLastPage()}`; + }); + if (!confirmed) { + return; } } - getSelectedPartner() { - const order = this.getSelectedSyncedOrder(); - return order ? order.get_partner() : null; + if (order && (await this._onBeforeDeleteOrder(order))) { + if (order === this.env.pos.get_order()) { + this._selectNextOrder(order); + } + this.env.pos.removeOrder(order); } - getHasItemsToRefund() { - const order = this.getSelectedSyncedOrder(); - if (!order) return false; - if (this._doesOrderHaveSoleItem(order)) return true; - const total = Object.values(this.env.pos.toRefundLines) - .filter( - (toRefundDetail) => - toRefundDetail.orderline.orderUid === order.uid && !toRefundDetail.destinationOrderUid - ) - .map((toRefundDetail) => toRefundDetail.qty) - .reduce((acc, val) => acc + val, 0); - return !this.env.pos.isProductQtyZero(total); + } + async _onNextPage() { + if (this._state.syncedOrders.currentPage < this._getLastPage()) { + this._state.syncedOrders.currentPage += 1; + await this._fetchSyncedOrders(); } - //#endregion - //#region PRIVATE METHODS + } + async _onPrevPage() { + if (this._state.syncedOrders.currentPage > 1) { + this._state.syncedOrders.currentPage -= 1; + await this._fetchSyncedOrders(); + } + } + async _onInvoiceOrder({ detail: orderId }) { + this.env.pos._invalidateSyncedOrdersCache([orderId]); + await this._fetchSyncedOrders(); + } + _onClickOrderline({ detail: orderline }) { + const order = this.getSelectedSyncedOrder(); + this._state.ui.selectedOrderlineIds[order.backendId] = orderline.id; + NumberBuffer.reset(); + } + _onClickRefundOrderUid({ detail: orderUid }) { + // Open the refund order. + const refundOrder = this.env.pos.orders.find((order) => order.uid == orderUid); + if (refundOrder) { + this._setOrder(refundOrder); + } + } + _onUpdateSelectedOrderline({ detail }) { + const buffer = detail.buffer; + const order = this.getSelectedSyncedOrder(); + if (!order) { + return NumberBuffer.reset(); + } + + const selectedOrderlineId = this.getSelectedOrderlineId(); + const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); + if (!orderline) { + 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(); + } + + const refundableQty = toRefundDetail.orderline.qty - toRefundDetail.orderline.refundedQty; + if (refundableQty <= 0) { + return NumberBuffer.reset(); + } + + if (buffer == null || buffer == "") { + toRefundDetail.qty = 0; + } else { + const quantity = Math.abs(parse.float(buffer)); + if (quantity > refundableQty) { + NumberBuffer.reset(); + this.showPopup("ErrorPopup", { + title: this.env._t("Maximum Exceeded"), + body: _.str.sprintf( + this.env._t( + "The requested quantity to be refunded is higher than the ordered quantity. %s is requested while only %s can be refunded." + ), + quantity, + refundableQty + ), + }); + } else { + toRefundDetail.qty = quantity; + } + } + } + async _onDoRefund() { + const order = this.getSelectedSyncedOrder(); + + if (this._doesOrderHaveSoleItem(order)) { + if (!this._prepareAutoRefundOnOrder(order)) { + // Don't proceed on refund if preparation returned false. + return; + } + } + + if (!order) { + this._state.ui.highlightHeaderNote = !this._state.ui.highlightHeaderNote; + return; + } + + const partner = order.get_partner(); + + const allToRefundDetails = this._getRefundableDetails(partner); + if (allToRefundDetails.length == 0) { + this._state.ui.highlightHeaderNote = !this._state.ui.highlightHeaderNote; + return; + } + + // The order that will contain the refund orderlines. + // Use the destinationOrder from props if the order to refund has the same + // partner as the destinationOrder. + const destinationOrder = + this.props.destinationOrder && partner === this.props.destinationOrder.get_partner() + ? this.props.destinationOrder + : this._getEmptyOrder(partner); + + // Add orderline for each toRefundDetail to the destinationOrder. + for (const refundDetail of allToRefundDetails) { + const product = this.env.pos.db.get_product_by_id(refundDetail.orderline.productId); + const options = this._prepareRefundOrderlineOptions(refundDetail); + await destinationOrder.add_product(product, options); + refundDetail.destinationOrderUid = destinationOrder.uid; + } + + // Set the partner to the destinationOrder. + if (partner && !destinationOrder.get_partner()) { + destinationOrder.set_partner(partner); + destinationOrder.updatePricelist(partner); + } + + if (this.env.pos.get_order().cid !== destinationOrder.cid) { + this.env.pos.set_order(destinationOrder); + } + + this._onCloseScreen(); + } + //#endregion + //#region PUBLIC METHODS + close() { /** - * Find the empty order with the following priority: - * - The empty order with the same parter as the provided. - * - The first empty order without a partner. - * - If no empty order, create a new one. - * @param {Object | null} partner - * @returns {boolean} + * Automatically create new order when there is no currently active order. + * Important in fiscal modules to keep the sequence of the orders. */ - _getEmptyOrder(partner) { - let emptyOrderForPartner = null; - let emptyOrder = null; - for (const order of this.env.pos.orders) { - if (order.get_orderlines().length === 0 && order.get_paymentlines().length === 0) { - if (order.get_partner() === partner) { - emptyOrderForPartner = order; - break; - } else if (!order.get_partner() && emptyOrder === null) { - // If emptyOrderForPartner is not found, we will use the first empty order. - emptyOrder = order; - } + if (this.env.pos.orders.length == 0) { + this.env.pos.add_new_order(); + } + super.close(); + } + getSelectedSyncedOrder() { + if (this._state.ui.filter == "SYNCED") { + return this._state.syncedOrders.cache[this._state.ui.selectedSyncedOrderId]; + } else { + return null; + } + } + getSelectedOrderlineId() { + return this._state.ui.selectedOrderlineIds[this._state.ui.selectedSyncedOrderId]; + } + /** + * Override to conditionally show the new ticket button. + */ + shouldShowNewOrderButton() { + return true; + } + getFilteredOrderList() { + if (this._state.ui.filter == "SYNCED") { + return this._state.syncedOrders.toShow; + } + const filterCheck = (order) => { + if (this._state.ui.filter && this._state.ui.filter !== "ACTIVE_ORDERS") { + const screen = order.get_screen_data(); + return this._state.ui.filter === this._getScreenToStatusMap()[screen.name]; + } + return true; + }; + const { fieldName, searchTerm } = this._state.ui.searchDetails; + const searchField = this._getSearchFields()[fieldName]; + const searchCheck = (order) => { + if (!searchField) { + return true; + } + const repr = searchField.repr(order); + if (repr === null) { + return true; + } + if (!searchTerm) { + return true; + } + return repr && repr.toString().toLowerCase().includes(searchTerm.toLowerCase()); + }; + const predicate = (order) => { + return filterCheck(order) && searchCheck(order); + }; + return this._getOrderList().filter(predicate); + } + getDate(order) { + return moment(order.validation_date).format("YYYY-MM-DD hh:mm A"); + } + getTotal(order) { + return this.env.pos.format_currency(order.get_total_with_tax()); + } + getPartner(order) { + return order.get_partner_name(); + } + getCardholderName(order) { + return order.get_cardholder_name(); + } + getCashier(order) { + return order.cashier ? order.cashier.name : ""; + } + getStatus(order) { + if (order.locked) { + return this.env._t("Paid"); + } else { + const screen = order.get_screen_data(); + return this._getOrderStates().get(this._getScreenToStatusMap()[screen.name]).text; + } + } + /** + * If the order is the only order and is empty + */ + isDefaultOrderEmpty(order) { + const status = this._getScreenToStatusMap()[order.get_screen_data().name]; + const productScreenStatus = this._getScreenToStatusMap().ProductScreen; + return ( + order.get_orderlines().length === 0 && + this.env.pos.get_order_list().length === 1 && + status === productScreenStatus && + order.get_paymentlines().length === 0 + ); + } + /** + * Hide the delete button if one of the payments is a 'done' electronic payment. + */ + shouldHideDeleteButton(order) { + return ( + this.isDefaultOrderEmpty(order) || + order.locked || + order + .get_paymentlines() + .some( + (payment) => payment.is_electronic() && payment.get_payment_status() === "done" + ) + ); + } + isHighlighted(order) { + if (this._state.ui.filter == "SYNCED") { + const selectedOrder = this.getSelectedSyncedOrder(); + return selectedOrder ? order.backendId == selectedOrder.backendId : false; + } else { + const activeOrder = this.env.pos.get_order(); + return activeOrder ? activeOrder.uid == order.uid : false; + } + } + showCardholderName() { + return this.env.pos.payment_methods.some((method) => method.use_payment_terminal); + } + getSearchBarConfig() { + return { + searchFields: new Map( + Object.entries(this._getSearchFields()).map(([key, val]) => [key, val.displayName]) + ), + filter: { show: true, options: this._getFilterOptions() }, + defaultSearchDetails: this._state.ui.searchDetails, + defaultFilter: this._state.ui.filter, + }; + } + shouldShowPageControls() { + return this._state.ui.filter == "SYNCED" && this._getLastPage() > 1; + } + getPageNumber() { + if (!this._state.syncedOrders.totalCount) { + return `1/1`; + } else { + return `${this._state.syncedOrders.currentPage}/${this._getLastPage()}`; + } + } + getSelectedPartner() { + const order = this.getSelectedSyncedOrder(); + return order ? order.get_partner() : null; + } + getHasItemsToRefund() { + const order = this.getSelectedSyncedOrder(); + if (!order) { + return false; + } + if (this._doesOrderHaveSoleItem(order)) { + return true; + } + const total = Object.values(this.env.pos.toRefundLines) + .filter( + (toRefundDetail) => + toRefundDetail.orderline.orderUid === order.uid && + !toRefundDetail.destinationOrderUid + ) + .map((toRefundDetail) => toRefundDetail.qty) + .reduce((acc, val) => acc + val, 0); + return !this.env.pos.isProductQtyZero(total); + } + //#endregion + //#region PRIVATE METHODS + /** + * Find the empty order with the following priority: + * - The empty order with the same parter as the provided. + * - The first empty order without a partner. + * - If no empty order, create a new one. + * @param {Object | null} partner + * @returns {boolean} + */ + _getEmptyOrder(partner) { + let emptyOrderForPartner = null; + let emptyOrder = null; + for (const order of this.env.pos.orders) { + if (order.get_orderlines().length === 0 && order.get_paymentlines().length === 0) { + if (order.get_partner() === partner) { + emptyOrderForPartner = order; + break; + } else if (!order.get_partner() && emptyOrder === null) { + // If emptyOrderForPartner is not found, we will use the first empty order. + emptyOrder = order; } } - return emptyOrderForPartner || emptyOrder || this.env.pos.add_new_order(); } - _doesOrderHaveSoleItem(order) { - const orderlines = order.get_orderlines(); - if (orderlines.length !== 1) return false; - const theOrderline = orderlines[0]; - const refundableQty = theOrderline.get_quantity() - theOrderline.refunded_qty; - return this.env.pos.isProductQtyZero(refundableQty - 1); + return emptyOrderForPartner || emptyOrder || this.env.pos.add_new_order(); + } + _doesOrderHaveSoleItem(order) { + const orderlines = order.get_orderlines(); + if (orderlines.length !== 1) { + return false; + } + const theOrderline = orderlines[0]; + const refundableQty = theOrderline.get_quantity() - theOrderline.refunded_qty; + return this.env.pos.isProductQtyZero(refundableQty - 1); + } + _prepareAutoRefundOnOrder(order) { + const selectedOrderlineId = this.getSelectedOrderlineId(); + const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); + if (!orderline) { + return false; } - _prepareAutoRefundOnOrder(order) { - const selectedOrderlineId = this.getSelectedOrderlineId(); - const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); - if (!orderline) return false; - const toRefundDetail = this._getToRefundDetail(orderline); - const refundableQty = orderline.get_quantity() - orderline.refunded_qty; - if (this.env.pos.isProductQtyZero(refundableQty - 1)) { - toRefundDetail.qty = 1; - } - return true; + const toRefundDetail = this._getToRefundDetail(orderline); + const refundableQty = orderline.get_quantity() - orderline.refunded_qty; + if (this.env.pos.isProductQtyZero(refundableQty - 1)) { + toRefundDetail.qty = 1; } - /** - * Returns the corresponding toRefundDetail of the given orderline. - * SIDE-EFFECT: Automatically creates a toRefundDetail object for - * the given orderline if it doesn't exist and returns it. - * @param {models.Orderline} orderline - * @returns - */ - _getToRefundDetail(orderline) { - if (orderline.id in this.env.pos.toRefundLines) { - return this.env.pos.toRefundLines[orderline.id]; - } else { - const partner = orderline.order.get_partner(); - const orderPartnerId = partner ? partner.id : false; - const newToRefundDetail = { - qty: 0, - orderline: { - id: orderline.id, - productId: orderline.product.id, - price: orderline.price, - qty: orderline.quantity, - refundedQty: orderline.refunded_qty, - orderUid: orderline.order.uid, - orderBackendId: orderline.order.backendId, - orderPartnerId, - tax_ids: orderline.get_taxes().map(tax => tax.id), - discount: orderline.discount, - }, - destinationOrderUid: false, - }; - this.env.pos.toRefundLines[orderline.id] = newToRefundDetail; - return newToRefundDetail; - } - } - /** - * Select the lines from toRefundLines, as they can come from different orders. - * Returns only details that: - * - The quantity to refund is not zero - * - Filtered by partner (optional) - * - It's not yet linked to an active order (no destinationOrderUid) - * - * @param {Object} partner (optional) - * @returns {Array} refundableDetails - */ - _getRefundableDetails(partner) { - return Object.values(this.env.pos.toRefundLines).filter( - ({ qty, orderline, destinationOrderUid }) => - !this.env.pos.isProductQtyZero(qty) && - (partner ? orderline.orderPartnerId == partner.id : true) && - !destinationOrderUid - ); - } - /** - * Prepares the options to add a refund orderline. - * - * @param {Object} toRefundDetail - * @returns {Object} - */ - _prepareRefundOrderlineOptions(toRefundDetail) { - const { qty, orderline } = toRefundDetail; - return { - quantity: -qty, - price: orderline.price, - extras: { price_manually_set: true }, - merge: false, - refunded_orderline_id: orderline.id, - tax_ids: orderline.tax_ids, - discount: orderline.discount, - } - } - _setOrder(order) { - this.env.pos.set_order(order); - this.close(); - } - _getOrderList() { - return this.env.pos.get_order_list(); - } - _getFilterOptions() { - const orderStates = this._getOrderStates(); - orderStates.set('SYNCED', { text: this.env._t('Paid') }); - return orderStates; - } - /** - * @returns {Record string, displayName: string, modelField: string }>} - */ - _getSearchFields() { - const fields = { - RECEIPT_NUMBER: { - repr: (order) => order.name, - displayName: this.env._t('Receipt Number'), - modelField: 'pos_reference', - }, - DATE: { - repr: (order) => moment(order.creation_date).format('YYYY-MM-DD hh:mm A'), - displayName: this.env._t('Date'), - modelField: 'date_order', - }, - PARTNER: { - repr: (order) => order.get_partner_name(), - displayName: this.env._t('Customer'), - modelField: 'partner_id.display_name', + return true; + } + /** + * Returns the corresponding toRefundDetail of the given orderline. + * SIDE-EFFECT: Automatically creates a toRefundDetail object for + * the given orderline if it doesn't exist and returns it. + * @param {models.Orderline} orderline + * @returns + */ + _getToRefundDetail(orderline) { + if (orderline.id in this.env.pos.toRefundLines) { + return this.env.pos.toRefundLines[orderline.id]; + } else { + const partner = orderline.order.get_partner(); + const orderPartnerId = partner ? partner.id : false; + const newToRefundDetail = { + qty: 0, + orderline: { + id: orderline.id, + productId: orderline.product.id, + price: orderline.price, + qty: orderline.quantity, + refundedQty: orderline.refunded_qty, + orderUid: orderline.order.uid, + orderBackendId: orderline.order.backendId, + orderPartnerId, + tax_ids: orderline.get_taxes().map((tax) => tax.id), + discount: orderline.discount, }, + destinationOrderUid: false, }; - - if (this.showCardholderName()) { - fields.CARDHOLDER_NAME = { - repr: (order) => order.get_cardholder_name(), - displayName: this.env._t('Cardholder Name'), - modelField: 'payment_ids.cardholder_name', - }; - } - - return fields; + this.env.pos.toRefundLines[orderline.id] = newToRefundDetail; + return newToRefundDetail; } - /** - * Maps the order screen params to order status. - */ - _getScreenToStatusMap() { - return { - ProductScreen: 'ONGOING', - PaymentScreen: 'PAYMENT', - ReceiptScreen: 'RECEIPT', + } + /** + * Select the lines from toRefundLines, as they can come from different orders. + * Returns only details that: + * - The quantity to refund is not zero + * - Filtered by partner (optional) + * - It's not yet linked to an active order (no destinationOrderUid) + * + * @param {Object} partner (optional) + * @returns {Array} refundableDetails + */ + _getRefundableDetails(partner) { + return Object.values(this.env.pos.toRefundLines).filter( + ({ qty, orderline, destinationOrderUid }) => + !this.env.pos.isProductQtyZero(qty) && + (partner ? orderline.orderPartnerId == partner.id : true) && + !destinationOrderUid + ); + } + /** + * Prepares the options to add a refund orderline. + * + * @param {Object} toRefundDetail + * @returns {Object} + */ + _prepareRefundOrderlineOptions(toRefundDetail) { + const { qty, orderline } = toRefundDetail; + return { + quantity: -qty, + price: orderline.price, + extras: { price_manually_set: true }, + merge: false, + refunded_orderline_id: orderline.id, + tax_ids: orderline.tax_ids, + discount: orderline.discount, + }; + } + _setOrder(order) { + this.env.pos.set_order(order); + this.close(); + } + _getOrderList() { + return this.env.pos.get_order_list(); + } + _getFilterOptions() { + const orderStates = this._getOrderStates(); + orderStates.set("SYNCED", { text: this.env._t("Paid") }); + return orderStates; + } + /** + * @returns {Record string, displayName: string, modelField: string }>} + */ + _getSearchFields() { + const fields = { + RECEIPT_NUMBER: { + repr: (order) => order.name, + displayName: this.env._t("Receipt Number"), + modelField: "pos_reference", + }, + DATE: { + repr: (order) => moment(order.creation_date).format("YYYY-MM-DD hh:mm A"), + displayName: this.env._t("Date"), + modelField: "date_order", + }, + PARTNER: { + repr: (order) => order.get_partner_name(), + displayName: this.env._t("Customer"), + modelField: "partner_id.display_name", + }, + }; + + if (this.showCardholderName()) { + fields.CARDHOLDER_NAME = { + repr: (order) => order.get_cardholder_name(), + displayName: this.env._t("Cardholder Name"), + modelField: "payment_ids.cardholder_name", }; } - /** - * Override to do something before deleting the order. - * Make sure to return true to proceed on deleting the order. - * @param {*} order - * @returns {boolean} - */ - async _onBeforeDeleteOrder(order) { - return true; + + return fields; + } + /** + * Maps the order screen params to order status. + */ + _getScreenToStatusMap() { + return { + ProductScreen: "ONGOING", + PaymentScreen: "PAYMENT", + ReceiptScreen: "RECEIPT", + }; + } + /** + * Override to do something before deleting the order. + * Make sure to return true to proceed on deleting the order. + * @param {*} order + * @returns {boolean} + */ + async _onBeforeDeleteOrder(order) { + return true; + } + _getOrderStates() { + // We need the items to be ordered, therefore, Map is used instead of normal object. + const states = new Map(); + states.set("ACTIVE_ORDERS", { + text: this.env._t("All active orders"), + }); + // The spaces are important to make sure the following states + // are under the category of `All active orders`. + states.set("ONGOING", { + text: this.env._t("Ongoing"), + indented: true, + }); + states.set("PAYMENT", { + text: this.env._t("Payment"), + indented: true, + }); + states.set("RECEIPT", { + text: this.env._t("Receipt"), + indented: true, + }); + return states; + } + //#region SEARCH SYNCED ORDERS + _computeSyncedOrdersDomain() { + const { fieldName, searchTerm } = this._state.ui.searchDetails; + if (!searchTerm) { + return []; } - _getOrderStates() { - // We need the items to be ordered, therefore, Map is used instead of normal object. - const states = new Map(); - states.set('ACTIVE_ORDERS', { - text: this.env._t('All active orders'), - }); - // The spaces are important to make sure the following states - // are under the category of `All active orders`. - states.set('ONGOING', { - text: this.env._t('Ongoing'), - indented: true, - }); - states.set('PAYMENT', { - text: this.env._t('Payment'), - indented: true, - }); - states.set('RECEIPT', { - text: this.env._t('Receipt'), - indented: true, - }); - return states; + const modelField = this._getSearchFields()[fieldName].modelField; + if (modelField) { + return [[modelField, "ilike", `%${searchTerm}%`]]; + } else { + return []; } - //#region SEARCH SYNCED ORDERS - _computeSyncedOrdersDomain() { - const { fieldName, searchTerm } = this._state.ui.searchDetails; - if (!searchTerm) return []; - const modelField = this._getSearchFields()[fieldName].modelField; - if (modelField) { - return [[modelField, 'ilike', `%${searchTerm}%`]]; - } else { - return []; - } - } - /** - * Fetches the done orders from the backend that needs to be shown. - * If the order is already in cache, the full information about that - * order is not fetched anymore, instead, we use info from cache. - */ - async _fetchSyncedOrders() { - const domain = this._computeSyncedOrdersDomain(); - const limit = this._state.syncedOrders.nPerPage; - const offset = (this._state.syncedOrders.currentPage - 1) * this._state.syncedOrders.nPerPage; - const { ids, totalCount } = await this.rpc({ - model: 'pos.order', - method: 'search_paid_order_ids', - kwargs: { config_id: this.env.pos.config.id, domain, limit, offset }, + } + /** + * Fetches the done orders from the backend that needs to be shown. + * If the order is already in cache, the full information about that + * order is not fetched anymore, instead, we use info from cache. + */ + async _fetchSyncedOrders() { + const domain = this._computeSyncedOrdersDomain(); + const limit = this._state.syncedOrders.nPerPage; + const offset = + (this._state.syncedOrders.currentPage - 1) * this._state.syncedOrders.nPerPage; + const { ids, totalCount } = await this.rpc({ + model: "pos.order", + method: "search_paid_order_ids", + kwargs: { config_id: this.env.pos.config.id, domain, limit, offset }, + context: this.env.session.user_context, + }); + const idsNotInCache = ids.filter((id) => !(id in this._state.syncedOrders.cache)); + if (idsNotInCache.length > 0) { + const fetchedOrders = await this.rpc({ + model: "pos.order", + method: "export_for_ui", + args: [idsNotInCache], context: this.env.session.user_context, }); - const idsNotInCache = ids.filter((id) => !(id in this._state.syncedOrders.cache)); - if (idsNotInCache.length > 0) { - const fetchedOrders = await this.rpc({ - model: 'pos.order', - method: 'export_for_ui', - args: [idsNotInCache], - context: this.env.session.user_context, - }); - // Check for missing products and partners and load them in the PoS - await this.env.pos._loadMissingProducts(fetchedOrders); - await this.env.pos._loadMissingPartners(fetchedOrders); - // 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({}, { pos: this.env.pos, json: order }); - }); - } - this._state.syncedOrders.totalCount = totalCount; - this._state.syncedOrders.toShow = ids.map((id) => this._state.syncedOrders.cache[id]); + // Check for missing products and partners and load them in the PoS + await this.env.pos._loadMissingProducts(fetchedOrders); + await this.env.pos._loadMissingPartners(fetchedOrders); + // 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( + {}, + { pos: this.env.pos, json: order } + ); + }); } - _getLastPage() { - const totalCount = this._state.syncedOrders.totalCount; - const nPerPage = this._state.syncedOrders.nPerPage; - const remainder = totalCount % nPerPage; - if (remainder == 0) { - return totalCount / nPerPage; - } else { - return Math.ceil(totalCount / nPerPage); - } - } - //#endregion - //#endregion + this._state.syncedOrders.totalCount = totalCount; + this._state.syncedOrders.toShow = ids.map((id) => this._state.syncedOrders.cache[id]); } - 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: {}, - }; + _getLastPage() { + const totalCount = this._state.syncedOrders.totalCount; + const nPerPage = this._state.syncedOrders.nPerPage; + const remainder = totalCount % nPerPage; + if (remainder == 0) { + return totalCount / nPerPage; + } else { + return Math.ceil(totalCount / nPerPage); + } + } + //#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); +Registries.Component.add(TicketScreen); - return TicketScreen; -}); +export default 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 0a9c4b0f908..702473b3e05 100644 --- a/addons/point_of_sale/static/src/js/barcode_reader.js +++ b/addons/point_of_sale/static/src/js/barcode_reader.js @@ -1,8 +1,7 @@ -odoo.define('point_of_sale.BarcodeReader', function (require) { -"use strict"; +/** @odoo-module */ -var concurrency = require('web.concurrency'); -var core = require('web.core'); +import concurrency from "web.concurrency"; +import core from "web.core"; var Mutex = concurrency.Mutex; // this module interfaces with the barcode reader. It assumes the barcode reader @@ -10,11 +9,7 @@ var Mutex = concurrency.Mutex; // 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({ - actions:[ - 'product', - 'cashier', - 'client', - ], + actions: ["product", "cashier", "client"], init: function (attributes) { this.mutex = new Mutex(); @@ -29,7 +24,7 @@ var BarcodeReader = core.Class.extend({ this.action_callback_stack = []; - core.bus.on('barcode_scanned', this, function (barcode) { + core.bus.on("barcode_scanned", this, function (barcode) { // use mutex to make sure scans are done one after the other this.mutex.exec(async () => { await this.scan(barcode); @@ -59,7 +54,7 @@ var BarcodeReader = core.Class.extend({ } }, - remove_action_callback: function(name, callback) { + remove_action_callback: function (name, callback) { if (!callback) { delete this.action_callbacks[name]; return; @@ -105,13 +100,15 @@ var BarcodeReader = core.Class.extend({ }, scan: async function (code) { - if (!code) return; + if (!code) { + return; + } const callbacks = Object.keys(this.exclusive_callbacks).length ? this.exclusive_callbacks : this.action_callbacks; let parsed_results = this.barcode_parser.parse_barcode(code); - if (! Array.isArray(parsed_results)) { + if (!Array.isArray(parsed_results)) { parsed_results = [parsed_results]; } for (const parsed_result of parsed_results) { @@ -120,9 +117,9 @@ var BarcodeReader = core.Class.extend({ await cb(parsed_result); } } else if (callbacks.error) { - [...callbacks.error].map(cb => cb(parsed_result)); + [...callbacks.error].map((cb) => cb(parsed_result)); } else { - console.warn('Ignored Barcode Scan:', parsed_result); + console.warn("Ignored Barcode Scan:", parsed_result); } } }, @@ -137,23 +134,26 @@ var BarcodeReader = core.Class.extend({ } this.remote_active = 1; - function waitforbarcode(){ - return self.proxy.connection.rpc('/hw_proxy/scanner',{},{shadow: true, timeout:7500}) - .then(function (barcode) { - if (!self.remote_scanning) { - self.remote_active = 0; - return; + function waitforbarcode() { + return self.proxy.connection + .rpc("/hw_proxy/scanner", {}, { shadow: true, timeout: 7500 }) + .then( + function (barcode) { + if (!self.remote_scanning) { + self.remote_active = 0; + return; + } + self.scan(barcode); + waitforbarcode(); + }, + function () { + if (!self.remote_scanning) { + self.remote_active = 0; + return; + } + waitforbarcode(); } - self.scan(barcode); - waitforbarcode(); - }, - function () { - if (!self.remote_scanning) { - self.remote_active = 0; - return; - } - waitforbarcode(); - }); + ); } waitforbarcode(); }, @@ -164,6 +164,4 @@ var BarcodeReader = core.Class.extend({ }, }); -return BarcodeReader; - -}); +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 180cbcdf177..a62686e291a 100644 --- a/addons/point_of_sale/static/src/js/custom_hooks.js +++ b/addons/point_of_sale/static/src/js/custom_hooks.js @@ -1,118 +1,116 @@ -odoo.define('point_of_sale.custom_hooks', function (require) { - 'use strict'; +/** @odoo-module */ +const { onMounted, onPatched, onWillUnmount, useComponent } = owl; - const { onMounted, onPatched, onWillUnmount, useComponent } = owl; +/** + * Introduce error handlers in the component. + * + * IMPROVEMENT: This is a terrible hook. There could be a better way to handle + * the error when the order failed to sync. + */ +export function useErrorHandlers() { + const component = useComponent(); - /** - * Introduce error handlers in the component. - * - * IMPROVEMENT: This is a terrible hook. There could be a better way to handle - * the error when the order failed to sync. - */ - function useErrorHandlers() { - const component = useComponent(); - - 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', { - title: this.env._t('Please print the invoice from the backend'), - body: - this.env._t( - 'The order has been synchronized earlier. Please make the invoice from the backend for the order: ' - ) + error.data.order.name, - }); - } else if (error.code < 0) { - // XmlHttpRequest Errors - const title = this.env._t('Unable to sync order'); - 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 }); - } else if (error.code === 200) { - // OpenERP Server Errors - await this.showPopup('ErrorTracebackPopup', { - title: error.data.message || this.env._t('Server Error'), - body: - error.data.debug || - this.env._t('The server encountered an error while receiving your order.'), - }); - } else if (error.code === 700) { - // Sweden Fiscal module errors - await this.showPopup('ErrorPopup', { - title: this.env._t('Fiscal data module error'), - body: - error.data.error.status || - this.env._t('The fiscal data module encountered an error while receiving your order.'), - }); - } else if (error.code === 701) { - // Belgian Fiscal module errors - let bodyMessage = ""; - if(error.error.errorCode) - bodyMessage = "'" + error.error.errorCode + "': " + error.error.errorMessage; - else - bodyMessage = "Fiscal data module is not on."; - await this.showPopup('ErrorPopup', { - title: this.env._t('Fiscal data module error'), - body: bodyMessage - }); - } else { - // ??? - 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' + 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", { + title: this.env._t("Please print the invoice from the backend"), + body: + this.env._t( + "The order has been synchronized earlier. Please make the invoice from the backend for the order: " + ) + error.data.order.name, + }); + } else if (error.code < 0) { + // XmlHttpRequest Errors + const title = this.env._t("Unable to sync order"); + 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 }); + } else if (error.code === 200) { + // OpenERP Server Errors + await this.showPopup("ErrorTracebackPopup", { + title: error.data.message || this.env._t("Server Error"), + body: + error.data.debug || + this.env._t("The server encountered an error while receiving your order."), + }); + } else if (error.code === 700) { + // Sweden Fiscal module errors + await this.showPopup("ErrorPopup", { + title: this.env._t("Fiscal data module error"), + body: + error.data.error.status || + this.env._t( + "The fiscal data module encountered an error while receiving your order." ), - }); - } - }; - } - - function useAutoFocusToLast() { - const current = useComponent(); - let target = null; - function autofocus() { - const prevTarget = target; - const allInputs = current.el.querySelectorAll('input'); - target = allInputs[allInputs.length - 1]; - if (target && target !== prevTarget) { - target.focus(); - target.selectionStart = target.selectionEnd = target.value.length; + }); + } else if (error.code === 701) { + // Belgian Fiscal module errors + let bodyMessage = ""; + if (error.error.errorCode) { + bodyMessage = "'" + error.error.errorCode + "': " + error.error.errorMessage; + } else { + bodyMessage = "Fiscal data module is not on."; } + await this.showPopup("ErrorPopup", { + title: this.env._t("Fiscal data module error"), + body: bodyMessage, + }); + } else { + // ??? + 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" + ), + }); } - onMounted(autofocus); - onPatched(autofocus); - } + }; +} - function useBarcodeReader(callbackMap, exclusive = false) { - const current = useComponent(); - const barcodeReader = current.env.barcode_reader; - for (let [key, callback] of Object.entries(callbackMap)) { - callbackMap[key] = callback.bind(current); +export function useAutoFocusToLast() { + const current = useComponent(); + let target = null; + function autofocus() { + const prevTarget = target; + const allInputs = current.el.querySelectorAll("input"); + target = allInputs[allInputs.length - 1]; + if (target && target !== prevTarget) { + target.focus(); + target.selectionStart = target.selectionEnd = target.value.length; } - onMounted(() => { - if (barcodeReader) { - for (let key in callbackMap) { - if (exclusive) { - barcodeReader.set_exclusive_callback(key, callbackMap[key]); - } else { - barcodeReader.set_action_callback(key, callbackMap[key]); - } + } + onMounted(autofocus); + onPatched(autofocus); +} + +export function useBarcodeReader(callbackMap, exclusive = false) { + const current = useComponent(); + const barcodeReader = current.env.barcode_reader; + for (const [key, callback] of Object.entries(callbackMap)) { + callbackMap[key] = callback.bind(current); + } + onMounted(() => { + if (barcodeReader) { + for (const key in callbackMap) { + if (exclusive) { + barcodeReader.set_exclusive_callback(key, callbackMap[key]); + } else { + barcodeReader.set_action_callback(key, callbackMap[key]); } } - }); - onWillUnmount(() => { - if (barcodeReader) { - for (let key in callbackMap) { - if (exclusive) { - barcodeReader.remove_exclusive_callback(key, callbackMap[key]); - } else { - barcodeReader.remove_action_callback(key, callbackMap[key]); - } + } + }); + onWillUnmount(() => { + if (barcodeReader) { + for (const key in callbackMap) { + if (exclusive) { + barcodeReader.remove_exclusive_callback(key, callbackMap[key]); + } else { + barcodeReader.remove_action_callback(key, callbackMap[key]); } } - }); - } - - return { useErrorHandlers, useAutoFocusToLast, useBarcodeReader }; -}); + } + }); +} diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 1322143043d..e685715283f 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -1,14 +1,12 @@ -odoo.define('point_of_sale.DB', function (require) { -"use strict"; +/** @odoo-module */ -var core = require('web.core'); -var utils = require('web.utils'); +import core from "web.core"; +import utils from "web.utils"; /* The PosDB holds reference to data that is either * - static: does not change between pos reloads * - persistent : must stay between reloads ( orders ) */ - /** * cache the data in memory to avoid roundtrips to the localstorage * @@ -21,15 +19,15 @@ var utils = require('web.utils'); const CACHE = {}; var 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){ + name: "openerp_pos_db", //the prefix of the localstorage data + limit: 100, // the maximum number of results returned by a search + init: function (options) { options = options || {}; this.name = options.name || this.name; this.limit = options.limit || this.limit; if (options.uuid) { - this.name = this.name + '_' + options.uuid; + this.name = this.name + "_" + options.uuid; } this.product_by_id = {}; @@ -44,11 +42,11 @@ var PosDB = core.Class.extend({ this.partner_write_date = null; this.category_by_id = {}; - this.root_category_id = 0; + this.root_category_id = 0; this.category_products = {}; this.category_ancestors = {}; this.category_childs = {}; - this.category_parent = {}; + this.category_parent = {}; this.category_search_string = {}; }, @@ -59,87 +57,87 @@ var PosDB = core.Class.extend({ * * @param {string} uuid Unique identifier of the PoS Config linked to the current session. */ - set_uuid: function(uuid){ - this.name = this.name + '_' + uuid; + set_uuid: function (uuid) { + this.name = this.name + "_" + uuid; }, /* returns the category object from its id. If you pass a list of id as parameters, you get * a list of category objects. */ - get_category_by_id: function(categ_id){ - if(categ_id instanceof Array){ + get_category_by_id: function (categ_id) { + if (categ_id instanceof Array) { var list = []; - for(var i = 0, len = categ_id.length; i < len; i++){ + for (var i = 0, len = categ_id.length; i < len; i++) { var cat = this.category_by_id[categ_id[i]]; - if(cat){ + if (cat) { list.push(cat); - }else{ - console.error("get_category_by_id: no category has id:",categ_id[i]); + } else { + console.error("get_category_by_id: no category has id:", categ_id[i]); } } return list; - }else{ + } else { return this.category_by_id[categ_id]; } }, /* returns a list of the category's child categories ids, or an empty list * if a category has no childs */ - get_category_childs_ids: function(categ_id){ + get_category_childs_ids: function (categ_id) { return this.category_childs[categ_id] || []; }, /* returns a list of all ancestors (parent, grand-parent, etc) categories ids * starting from the root category to the direct parent */ - get_category_ancestors_ids: function(categ_id){ + get_category_ancestors_ids: function (categ_id) { return this.category_ancestors[categ_id] || []; }, /* returns the parent category's id of a category, or the root_category_id if no parent. * the root category is parent of itself. */ - get_category_parent_id: function(categ_id){ + get_category_parent_id: function (categ_id) { return this.category_parent[categ_id] || this.root_category_id; }, /* adds categories definitions to the database. categories is a list of categories objects as * returned by the openerp server. Categories must be inserted before the products or the * product/ categories association may (will) not work properly */ - add_categories: function(categories){ + add_categories: function (categories) { var self = this; - if(!this.category_by_id[this.root_category_id]){ + if (!this.category_by_id[this.root_category_id]) { this.category_by_id[this.root_category_id] = { - id : this.root_category_id, - name : 'Root', + id: this.root_category_id, + name: "Root", }; } - categories.forEach(function(cat){ + categories.forEach(function (cat) { self.category_by_id[cat.id] = cat; }); - categories.forEach(function(cat){ + categories.forEach(function (cat) { var parent_id = cat.parent_id[0]; - if(!(parent_id && self.category_by_id[parent_id])){ + if (!(parent_id && self.category_by_id[parent_id])) { parent_id = self.root_category_id; } self.category_parent[cat.id] = parent_id; - if(!self.category_childs[parent_id]){ + if (!self.category_childs[parent_id]) { self.category_childs[parent_id] = []; } self.category_childs[parent_id].push(cat.id); }); - function make_ancestors(cat_id, ancestors){ + function make_ancestors(cat_id, ancestors) { self.category_ancestors[cat_id] = ancestors; ancestors = ancestors.slice(0); ancestors.push(cat_id); var childs = self.category_childs[cat_id] || []; - for(var i=0, len = childs.length; i < len; i++){ + for (var i = 0, len = childs.length; i < len; i++) { make_ancestors(childs[i], ancestors); } } make_ancestors(this.root_category_id, []); }, - category_contains: function(categ_id, product_id) { + category_contains: function (categ_id, product_id) { var product = this.product_by_id[product_id]; if (product) { var cid = product.pos_categ_id[0]; - while (cid && cid !== categ_id){ + while (cid && cid !== categ_id) { cid = this.category_parent[cid]; } return !!cid; @@ -147,139 +145,151 @@ var PosDB = core.Class.extend({ return false; }, /* loads a record store from the database. returns default if nothing is found */ - load: function(store,deft){ - if(CACHE[store] !== undefined){ + load: function (store, deft) { + if (CACHE[store] !== undefined) { return CACHE[store]; } - var data = localStorage[this.name + '_' + store]; - if(data !== undefined && data !== ""){ + var data = localStorage[this.name + "_" + store]; + if (data !== undefined && data !== "") { data = JSON.parse(data); CACHE[store] = data; return data; - }else{ + } else { return deft; } }, /* saves a record store to the database */ - save: function(store,data){ - localStorage[this.name + '_' + store] = JSON.stringify(data); + save: function (store, data) { + localStorage[this.name + "_" + store] = JSON.stringify(data); CACHE[store] = data; }, - _product_search_string: function(product){ + _product_search_string: function (product) { var str = product.display_name; if (product.barcode) { - str += '|' + product.barcode; + str += "|" + product.barcode; } if (product.default_code) { - str += '|' + product.default_code; + str += "|" + product.default_code; } if (product.description) { - str += '|' + product.description; + str += "|" + product.description; } if (product.description_sale) { - str += '|' + product.description_sale; + str += "|" + product.description_sale; } - str = product.id + ':' + str.replace(/:/g,'') + '\n'; + str = product.id + ":" + str.replace(/:/g, "") + "\n"; return str; }, - add_products: function(products){ + add_products: function (products) { var stored_categories = this.product_by_category_id; - if(!(products instanceof Array)){ + if (!(products instanceof Array)) { products = [products]; } - for(var i = 0, len = products.length; i < len; i++){ + for (var i = 0, len = products.length; i < len; i++) { var product = products[i]; - if (product.id in this.product_by_id) continue; - if (product.available_in_pos){ + if (product.id in this.product_by_id) { + continue; + } + if (product.available_in_pos) { var search_string = utils.unaccent(this._product_search_string(product)); - var categ_id = product.pos_categ_id ? product.pos_categ_id[0] : this.root_category_id; + var categ_id = product.pos_categ_id + ? product.pos_categ_id[0] + : this.root_category_id; product.product_tmpl_id = product.product_tmpl_id[0]; - if(!stored_categories[categ_id]){ + if (!stored_categories[categ_id]) { stored_categories[categ_id] = []; } stored_categories[categ_id].push(product.id); - if(this.category_search_string[categ_id] === undefined){ - this.category_search_string[categ_id] = ''; + if (this.category_search_string[categ_id] === undefined) { + this.category_search_string[categ_id] = ""; } this.category_search_string[categ_id] += search_string; var ancestors = this.get_category_ancestors_ids(categ_id) || []; - for(var j = 0, jlen = ancestors.length; j < jlen; j++){ + for (var j = 0, jlen = ancestors.length; j < jlen; j++) { var ancestor = ancestors[j]; - if(! stored_categories[ancestor]){ + if (!stored_categories[ancestor]) { stored_categories[ancestor] = []; } stored_categories[ancestor].push(product.id); - if( this.category_search_string[ancestor] === undefined){ - this.category_search_string[ancestor] = ''; + if (this.category_search_string[ancestor] === undefined) { + this.category_search_string[ancestor] = ""; } this.category_search_string[ancestor] += search_string; } } this.product_by_id[product.id] = product; - if(product.barcode){ + if (product.barcode) { this.product_by_barcode[product.barcode] = product; } } }, - add_packagings: function(product_packagings){ + add_packagings: function (product_packagings) { var self = this; _.map(product_packagings, function (product_packaging) { - if (_.find(self.product_by_id, {'id': product_packaging.product_id[0]})) { + if (_.find(self.product_by_id, { id: product_packaging.product_id[0] })) { self.product_packaging_by_barcode[product_packaging.barcode] = product_packaging; } }); }, - _partner_search_string: function(partner){ - var str = partner.name || ''; - if(partner.barcode){ - str += '|' + partner.barcode; + _partner_search_string: function (partner) { + var str = partner.name || ""; + if (partner.barcode) { + str += "|" + partner.barcode; } - if(partner.address){ - str += '|' + partner.address; + if (partner.address) { + str += "|" + partner.address; } - if(partner.phone){ - str += '|' + partner.phone.split(' ').join(''); + if (partner.phone) { + str += "|" + partner.phone.split(" ").join(""); } - if(partner.mobile){ - str += '|' + partner.mobile.split(' ').join(''); + if (partner.mobile) { + str += "|" + partner.mobile.split(" ").join(""); } - if(partner.email){ - str += '|' + partner.email; + if (partner.email) { + str += "|" + partner.email; } - if(partner.vat){ - str += '|' + partner.vat; + if (partner.vat) { + str += "|" + partner.vat; } - if(partner.parent_name){ - str += '|' + partner.parent_name; + if (partner.parent_name) { + str += "|" + partner.parent_name; } - str = '' + partner.id + ':' + str.replace(':', '').replace(/\n/g, ' ') + '\n'; + str = "" + partner.id + ":" + str.replace(":", "").replace(/\n/g, " ") + "\n"; return str; }, - add_partners: function(partners){ + add_partners: function (partners) { var updated_count = 0; - var new_write_date = ''; + var new_write_date = ""; var partner; - for(var i = 0, len = partners.length; i < len; i++){ + for (var i = 0, len = partners.length; i < len; i++) { partner = partners[i]; - var local_partner_date = (this.partner_write_date || '').replace(/^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/, '$1T$2Z'); - var dist_partner_date = (partner.write_date || '').replace(/^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/, '$1T$2Z'); - if ( this.partner_write_date && - this.partner_by_id[partner.id] && - new Date(local_partner_date).getTime() + 1000 >= - new Date(dist_partner_date).getTime() ) { + var local_partner_date = (this.partner_write_date || "").replace( + /^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/, + "$1T$2Z" + ); + var dist_partner_date = (partner.write_date || "").replace( + /^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/, + "$1T$2Z" + ); + if ( + this.partner_write_date && + this.partner_by_id[partner.id] && + new Date(local_partner_date).getTime() + 1000 >= + new Date(dist_partner_date).getTime() + ) { // FIXME: The write_date is stored with milisec precision in the database // but the dates we get back are only precise to the second. This means when // you read partners modified strictly after time X, you get back partners that were // modified X - 1 sec ago. continue; - } else if ( new_write_date < partner.write_date ) { - new_write_date = partner.write_date; + } else if (new_write_date < partner.write_date) { + new_write_date = partner.write_date; } if (!this.partner_by_id[partner.id]) { this.partner_sorted.push(partner.id); @@ -301,14 +311,15 @@ var PosDB = core.Class.extend({ for (var id in this.partner_by_id) { partner = this.partner_by_id[id]; - if(partner.barcode){ + if (partner.barcode) { this.partner_by_barcode[partner.barcode] = partner; } - partner.address = (partner.street ? partner.street + ', ': '') + - (partner.zip ? partner.zip + ', ': '') + - (partner.city ? partner.city + ', ': '') + - (partner.state_id ? partner.state_id[1] + ', ': '') + - (partner.country_id ? partner.country_id[1]: ''); + partner.address = + (partner.street ? partner.street + ", " : "") + + (partner.zip ? partner.zip + ", " : "") + + (partner.city ? partner.city + ", " : "") + + (partner.state_id ? partner.state_id[1] + ", " : "") + + (partner.country_id ? partner.country_id[1] : ""); this.partner_search_string += this._partner_search_string(partner); } @@ -316,77 +327,82 @@ var PosDB = core.Class.extend({ } return updated_count; }, - get_partner_write_date: function(){ + get_partner_write_date: function () { return this.partner_write_date || "1970-01-01 00:00:00"; }, - get_partner_by_id: function(id){ + get_partner_by_id: function (id) { return this.partner_by_id[id]; }, - get_partner_by_barcode: function(barcode){ + get_partner_by_barcode: function (barcode) { return this.partner_by_barcode[barcode]; }, - get_partners_sorted: function(max_count){ - max_count = max_count ? Math.min(this.partner_sorted.length, max_count) : this.partner_sorted.length; + get_partners_sorted: function (max_count) { + max_count = max_count + ? Math.min(this.partner_sorted.length, max_count) + : this.partner_sorted.length; var partners = []; for (var i = 0; i < max_count; i++) { partners.push(this.partner_by_id[this.partner_sorted[i]]); } return partners; }, - search_partner: function(query){ + search_partner: function (query) { try { - query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.'); - query = query.replace(/ /g,'.+'); - var re = RegExp("([0-9]+):.*?"+utils.unaccent(query),"gi"); - }catch{ + // eslint-disable-next-line no-useless-escape + query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g, "."); + query = query.replace(/ /g, ".+"); + var re = RegExp("([0-9]+):.*?" + utils.unaccent(query), "gi"); + } catch { return []; } var results = []; - for(var i = 0; i < this.limit; i++){ + for (var i = 0; i < this.limit; i++) { var r = re.exec(this.partner_search_string); - if(r){ + if (r) { var id = Number(r[1]); results.push(this.get_partner_by_id(id)); - }else{ + } else { break; } } return results; }, /* removes all the data from the database. TODO : being able to selectively remove data */ - clear: function(){ - for(var i = 0, len = arguments.length; i < len; i++){ - localStorage.removeItem(this.name + '_' + arguments[i]); + clear: function () { + for (var i = 0, len = arguments.length; i < len; i++) { + localStorage.removeItem(this.name + "_" + arguments[i]); } }, /* this internal methods returns the count of properties in an object. */ - _count_props : function(obj){ + _count_props: function (obj) { var count = 0; - for(var prop in obj){ - if(obj.hasOwnProperty(prop)){ + for (var prop in obj) { + if (Object.hasOwnProperty.call(obj, prop)) { count++; } } return count; }, - get_product_by_id: function(id){ + get_product_by_id: function (id) { return this.product_by_id[id]; }, - get_product_by_barcode: function(barcode){ - if(this.product_by_barcode[barcode]){ + get_product_by_barcode: function (barcode) { + if (this.product_by_barcode[barcode]) { return this.product_by_barcode[barcode]; } else if (this.product_packaging_by_barcode[barcode]) { return this.product_by_id[this.product_packaging_by_barcode[barcode].product_id[0]]; } return undefined; }, - get_product_by_category: function(category_id){ - var product_ids = this.product_by_category_id[category_id]; + get_product_by_category: function (category_id) { + var product_ids = this.product_by_category_id[category_id]; var list = []; if (product_ids) { for (var i = 0, len = Math.min(product_ids.length, this.limit); i < len; i++) { const product = this.product_by_id[product_ids[i]]; - if (!(product.active && product.available_in_pos)) continue; + if (!(product.active && product.available_in_pos)) { + continue; + } list.push(product); } } @@ -396,23 +412,26 @@ var PosDB = core.Class.extend({ * - a category that is or is a child of category_id, * - a name, package or barcode containing the query (case insensitive) */ - search_product_in_category: function(category_id, query){ + search_product_in_category: function (category_id, query) { try { - query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.'); - query = query.replace(/ /g,'.+'); - var re = RegExp("([0-9]+):.*?"+utils.unaccent(query),"gi"); - }catch{ + // eslint-disable-next-line no-useless-escape + query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g, "."); + query = query.replace(/ /g, ".+"); + var re = RegExp("([0-9]+):.*?" + utils.unaccent(query), "gi"); + } catch { return []; } var results = []; - for(var i = 0; i < this.limit; i++){ + for (var i = 0; i < this.limit; i++) { var r = re.exec(this.category_search_string[category_id]); - if(r){ + if (r) { var id = Number(r[1]); const product = this.get_product_by_id(id); - if (!(product.active && product.available_in_pos)) continue; + if (!(product.active && product.available_in_pos)) { + continue; + } results.push(product); - }else{ + } else { break; } } @@ -422,11 +441,12 @@ var PosDB = core.Class.extend({ * true if the product belongs to one of the provided category * or one of its child categories. */ - is_product_in_category: function(category_ids, product_id) { + is_product_in_category: function (category_ids, product_id) { let cat = this.get_product_by_id(product_id).pos_categ_id[0]; while (cat) { - for (let cat_id of category_ids) { - if (cat == cat_id) { // The == is important, ids may be strings + for (const cat_id of category_ids) { + if (cat == cat_id) { + // The == is important, ids may be strings return true; } } @@ -436,15 +456,15 @@ var PosDB = core.Class.extend({ }, /* paid orders */ - add_order: function(order){ + add_order: function (order) { var order_id = order.uid; - var orders = this.load('orders',[]); + var orders = this.load("orders", []); // if the order was already stored, we overwrite its data - for(var i = 0, len = orders.length; i < len; i++){ - if(orders[i].id === order_id){ + for (var i = 0, len = orders.length; i < len; i++) { + if (orders[i].id === order_id) { orders[i].data = order; - this.save('orders',orders); + this.save("orders", orders); return order_id; } } @@ -453,27 +473,27 @@ var PosDB = core.Class.extend({ // that where already stored should already have been removed. this.remove_unpaid_order(order); - orders.push({id: order_id, data: order}); - this.save('orders',orders); + orders.push({ id: order_id, data: order }); + this.save("orders", orders); return order_id; }, - remove_order: function(order_id){ - var orders = this.load('orders',[]); - orders = _.filter(orders, function(order){ + remove_order: function (order_id) { + var orders = this.load("orders", []); + orders = _.filter(orders, function (order) { return order.id !== order_id; }); - this.save('orders',orders); + this.save("orders", orders); }, - remove_all_orders: function(){ - this.save('orders',[]); + remove_all_orders: function () { + this.save("orders", []); }, - get_orders: function(){ - return this.load('orders',[]); + get_orders: function () { + return this.load("orders", []); }, - get_order: function(order_id){ + get_order: function (order_id) { var orders = this.get_orders(); - for(var i = 0, len = orders.length; i < len; i++){ - if(orders[i].id === order_id){ + for (var i = 0, len = orders.length; i < len; i++) { + if (orders[i].id === order_id) { return orders[i]; } } @@ -481,35 +501,35 @@ var PosDB = core.Class.extend({ }, /* working orders */ - save_unpaid_order: function(order){ + save_unpaid_order: function (order) { var order_id = order.uid; - var orders = this.load('unpaid_orders',[]); + var orders = this.load("unpaid_orders", []); var serialized = order.export_as_JSON(); for (var i = 0; i < orders.length; i++) { - if (orders[i].id === order_id){ + if (orders[i].id === order_id) { orders[i].data = serialized; - this.save('unpaid_orders',orders); + this.save("unpaid_orders", orders); return order_id; } } - orders.push({id: order_id, data: serialized}); - this.save('unpaid_orders',orders); + orders.push({ id: order_id, data: serialized }); + this.save("unpaid_orders", orders); return order_id; }, - remove_unpaid_order: function(order){ - var orders = this.load('unpaid_orders',[]); - orders = _.filter(orders, function(o){ + remove_unpaid_order: function (order) { + var orders = this.load("unpaid_orders", []); + orders = _.filter(orders, function (o) { return o.id !== order.uid; }); - this.save('unpaid_orders',orders); + this.save("unpaid_orders", orders); }, - remove_all_unpaid_orders: function(){ - this.save('unpaid_orders',[]); + remove_all_unpaid_orders: function () { + this.save("unpaid_orders", []); }, - get_unpaid_orders: function(){ - var saved = this.load('unpaid_orders',[]); + get_unpaid_orders: function () { + var saved = this.load("unpaid_orders", []); var orders = []; for (var i = 0; i < saved.length; i++) { orders.push(saved[i].data); @@ -521,9 +541,13 @@ var PosDB = core.Class.extend({ * @param {array} ids order_ids. * @return {array} list of orders. */ - get_unpaid_orders_to_sync: function(ids){ - const savedOrders = this.load('unpaid_orders',[]); - return savedOrders.filter(order => ids.includes(order.id) && (order.data.server_id || order.data.lines.length || order.data.statement_ids.length)); + get_unpaid_orders_to_sync: function (ids) { + const savedOrders = this.load("unpaid_orders", []); + return savedOrders.filter( + (order) => + ids.includes(order.id) && + (order.data.server_id || order.data.lines.length || order.data.statement_ids.length) + ); }, /** * Add a given order to the orders to be removed from the server. @@ -532,34 +556,32 @@ var PosDB = core.Class.extend({ * after syncing. This function will add the server_id of the order to a list of orders still to be removed. * @param {object} order object. */ - set_order_to_remove_from_server: function(order){ + set_order_to_remove_from_server: function (order) { if (order.server_id !== undefined) { - var to_remove = this.load('unpaid_orders_to_remove',[]); + var to_remove = this.load("unpaid_orders_to_remove", []); to_remove.push(order.server_id); - this.save('unpaid_orders_to_remove', to_remove); + this.save("unpaid_orders_to_remove", to_remove); } }, /** * Get a list of server_ids of orders to be removed. * @return {array} list of server_ids. */ - get_ids_to_remove_from_server: function(){ - return this.load('unpaid_orders_to_remove',[]); + get_ids_to_remove_from_server: function () { + return this.load("unpaid_orders_to_remove", []); }, /** * Remove server_ids from the list of orders to be removed. * @param {array} ids */ - set_ids_removed_from_server: function(ids){ - var to_remove = this.load('unpaid_orders_to_remove',[]); + set_ids_removed_from_server: function (ids) { + var to_remove = this.load("unpaid_orders_to_remove", []); - to_remove = _.filter(to_remove, function(id){ + to_remove = _.filter(to_remove, function (id) { return !ids.includes(id); }); - this.save('unpaid_orders_to_remove', to_remove); + this.save("unpaid_orders_to_remove", to_remove); }, }); -return PosDB; - -}); +export default PosDB; diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index a285af5d2b4..8dd7ad29550 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -1,17 +1,15 @@ -odoo.define('point_of_sale.devices', function (require) { -"use strict"; - -var core = require('web.core'); -var mixins = require('web.mixins'); -var Session = require('web.Session'); -var Printer = require('point_of_sale.Printer').Printer; +/** @odoo-module */ +import core from "web.core"; +import mixins from "web.mixins"; +import Session from "web.Session"; +import { Printer } from "@point_of_sale/js/printers"; // the JobQueue schedules a sequence of 'jobs'. each job is // a function returning a promise. The queue waits for each job to finish // before launching the next. Each job can also be scheduled with a delay. // the is used to prevent parallel requests to the proxy. -var JobQueue = function(){ +export const JobQueue = function () { var queue = []; var running = false; var scheduled_end_time = 0; @@ -33,7 +31,7 @@ var JobQueue = function(){ } // the time scheduled for this job - scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0); + scheduled_end_time = new Date().getTime() + (job.opts.duration || 0); // we run the job and put in prom when it finishes var prom = job.fun() || Promise.resolve(); @@ -41,10 +39,7 @@ var JobQueue = function(){ var always = function () { // we run the next job after the scheduled_end_time, even if it finishes before return new Promise(function (resolve, reject) { - setTimeout( - resolve, - Math.max(0, scheduled_end_time - (new Date()).getTime()) - ); + setTimeout(resolve, Math.max(0, scheduled_end_time - new Date().getTime())); }); }; // we don't care if a job fails ... @@ -65,20 +60,22 @@ var JobQueue = function(){ * @param {boolean} [opts.repeat] if true, the job will be endlessly repeated * @param {boolean} [opts.important] if true, the scheduled job cannot be canceled by a queue.clear() */ - this.schedule = function (fun, opts) { - queue.push({fun:fun, opts:opts || {}}); - if(!running){ + this.schedule = function (fun, opts) { + queue.push({ fun: fun, opts: opts || {} }); + if (!running) { run(); } }; // remove all jobs from the schedule (except the ones marked as important) - this.clear = function(){ - queue = _.filter(queue,function(job){return job.opts.important === true;}); + this.clear = function () { + queue = _.filter(queue, function (job) { + return job.opts.important === true; + }); }; // end the repetition of the current job - this.stoprepeat = function(){ + this.stoprepeat = function () { stoprepeat = true; }; @@ -91,16 +88,14 @@ var JobQueue = function(){ this.finished = function () { return end_of_queue; }; - }; - // this object interfaces with the local proxy to communicate to the various hardware devices // connected to the Point of Sale. As the communication only goes from the POS to the proxy, // methods are used both to signal an event, and to fetch information. -var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ - init: function(options){ +export const ProxyDevice = core.Class.extend(mixins.PropertiesMixin, { + init: function (options) { mixins.PropertiesMixin.init.call(this); var self = this; options = options || {}; @@ -117,16 +112,16 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ this.bypass_proxy = false; this.connection = null; - this.host = ''; - this.keptalive = false; + this.host = ""; + this.keptalive = false; - this.set('status',{}); + this.set("status", {}); - this.set_connection_status('disconnected'); + this.set_connection_status("disconnected"); - this.on('change:status',this,function(eh,status){ + this.on("change:status", this, function (eh, status) { status = status.newValue; - if(status.status === 'connected' && self.printer) { + if (status.status === "connected" && self.printer) { self.printer.print_receipt(); } }); @@ -135,23 +130,23 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ window.hw_proxy = this; }, - set_pos: function(pos) { + set_pos: function (pos) { this.setParent(pos); this.pos = pos; }, - set_connection_status: function(status, drivers, msg=''){ - var oldstatus = this.get('status'); + set_connection_status: function (status, drivers, msg = "") { + var oldstatus = this.get("status"); var newstatus = {}; newstatus.status = status; - newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers; + newstatus.drivers = status === "disconnected" ? {} : oldstatus.drivers; newstatus.drivers = drivers ? drivers : newstatus.drivers; newstatus.msg = msg; - this.set('status',newstatus); + this.set("status", newstatus); }, - disconnect: function(){ - if(this.get('status').status !== 'disconnected'){ + disconnect: function () { + if (this.get("status").status !== "disconnected") { this.connection.destroy(); - this.set_connection_status('disconnected'); + this.set_connection_status("disconnected"); } }, @@ -161,28 +156,31 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ * @param {string} url * @returns {Promise} */ - connect: function(url){ + connect: function (url) { var self = this; - this.connection = new Session(undefined,url, { use_cors: true}); + this.connection = new Session(undefined, url, { use_cors: true }); this.host = url; if (this.pos.config.iface_print_via_proxy) { this.connect_to_printer(); } - this.set_connection_status('connecting',{}); + this.set_connection_status("connecting", {}); - return this.message('handshake').then(function(response){ - if(response){ - self.set_connection_status('connected'); + return this.message("handshake").then( + function (response) { + if (response) { + self.set_connection_status("connected"); localStorage.hw_proxy_url = url; self.keepalive(); - }else{ - self.set_connection_status('disconnected'); - console.error('Connection refused by the Proxy'); + } else { + self.set_connection_status("disconnected"); + console.error("Connection refused by the Proxy"); } - },function(){ - self.set_connection_status('disconnected'); - console.error('Could not connect to the Proxy'); - }); + }, + function () { + self.set_connection_status("disconnected"); + console.error("Could not connect to the Proxy"); + } + ); }, connect_to_printer: function () { @@ -200,7 +198,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ */ autoconnect: function (options) { var self = this; - this.set_connection_status('connecting',{}); + this.set_connection_status("connecting", {}); if (this.pos.config.iface_print_via_proxy) { this.connect_to_printer(); } @@ -211,15 +209,16 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ found_url = this.try_hard_to_connect(options.force_ip, options); } else if (localStorage.hw_proxy_url) { // try harder when we remember a good proxy url - found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options) - .catch(function () { - if (window.location.protocol != 'https:') { + found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options).catch( + function () { + if (window.location.protocol != "https:") { return self.find_proxy(options); } - }); + } + ); } else { // just find something quick - if (window.location.protocol != 'https:'){ + if (window.location.protocol != "https:") { found_url = this.find_proxy(options); } } @@ -229,7 +228,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ }); successProm.catch(function () { - self.set_connection_status('disconnected'); + self.set_connection_status("disconnected"); }); return successProm; @@ -239,18 +238,23 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ keepalive: function () { var self = this; - function status(){ + function status() { var always = function () { setTimeout(status, 5000); }; - self.connection.rpc('/hw_proxy/status_json',{},{shadow: true, timeout:2500}) - .then(function (driver_status) { - self.set_connection_status('connected',driver_status); - }, function () { - if(self.get('status').status !== 'connecting'){ - self.set_connection_status('disconnected'); + self.connection + .rpc("/hw_proxy/status_json", {}, { shadow: true, timeout: 2500 }) + .then( + function (driver_status) { + self.set_connection_status("connected", driver_status); + }, + function () { + if (self.get("status").status !== "connecting") { + self.set_connection_status("disconnected"); + } } - }).then(always, always); + ) + .then(always, always); } if (!this.keptalive) { @@ -264,13 +268,13 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ * @param {Object} [params] * @returns {Promise} */ - message : function (name, params) { + message: function (name, params) { var callbacks = this.notifications[name] || []; for (var i = 0; i < callbacks.length; i++) { callbacks[i](params); } - if (this.get('status').status !== 'disconnected') { - return this.connection.rpc('/hw_proxy/' + name, params || {}, {shadow: true}); + if (this.get("status").status !== "disconnected") { + return this.connection.rpc("/hw_proxy/" + name, params || {}, { shadow: true }); } else { return Promise.reject(); } @@ -285,17 +289,17 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ * @returns {Promise} */ try_hard_to_connect: function (url, options) { - options = options || {}; + options = options || {}; var protocol = window.location.protocol; - var port = ( !options.port && protocol == "https:") ? ':443' : ':' + (options.port || '8069'); + var port = !options.port && protocol == "https:" ? ":443" : ":" + (options.port || "8069"); - this.set_connection_status('connecting'); + this.set_connection_status("connecting"); - if(url.indexOf('//') < 0){ - url = protocol + '//' + url; + if (url.indexOf("//") < 0) { + url = protocol + "//" + url; } - if(url.indexOf(':',5) < 0){ + if (url.indexOf(":", 5) < 0) { url = url + port; } @@ -303,19 +307,21 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ function try_real_hard_to_connect(url, retries) { return Promise.resolve( $.ajax({ - url: url + '/hw_proxy/hello', - method: 'GET', + url: url + "/hw_proxy/hello", + method: "GET", timeout: 1000, - }) - .then(function () { - return Promise.resolve(url); - }, function (resp) { - if (retries > 0) { - return try_real_hard_to_connect(url, retries-1); - } else { - return Promise.reject([resp.statusText, url]); + }).then( + function () { + return Promise.resolve(url); + }, + function (resp) { + if (retries > 0) { + return try_real_hard_to_connect(url, retries - 1); + } else { + return Promise.reject([resp.statusText, url]); + } } - }) + ) ); } @@ -330,34 +336,33 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ * @param {function} [options.progress] callback for search progress ( fac in [0,1] ) * @returns {Promise} will be resolved with the proxy valid url */ - find_proxy: function(options){ + find_proxy: function (options) { options = options || {}; - var self = this; - var port = ':' + (options.port || '8069'); - var urls = []; + var self = this; + var port = ":" + (options.port || "8069"); + var urls = []; var found = false; var parallel = 8; - var threads = []; + var threads = []; var progress = 0; - - urls.push('http://localhost'+port); - for(var i = 0; i < 256; i++){ - urls.push('http://192.168.0.'+i+port); - urls.push('http://192.168.1.'+i+port); - urls.push('http://10.0.0.'+i+port); + urls.push("http://localhost" + port); + for (var i = 0; i < 256; i++) { + urls.push("http://192.168.0." + i + port); + urls.push("http://192.168.1." + i + port); + urls.push("http://10.0.0." + i + port); } - var prog_inc = 1/urls.length; + var prog_inc = 1 / urls.length; - function update_progress(){ + function update_progress() { progress = found ? 1 : progress + prog_inc; - if(options.progress){ + if (options.progress) { options.progress(progress); } } - function thread () { + function thread() { var url = urls.shift(); if (!url || found || !self.searching_for_proxy) { @@ -366,32 +371,35 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ return Promise.resolve( $.ajax({ - url: url + '/hw_proxy/hello', - method: 'GET', + url: url + "/hw_proxy/hello", + method: "GET", timeout: 400, - }).then(function () { - found = true; - update_progress(); - return Promise.resolve(url); - }, function () { - update_progress(); - return thread(); - }) + }).then( + function () { + found = true; + update_progress(); + return Promise.resolve(url); + }, + function () { + update_progress(); + return thread(); + } + ) ); } this.searching_for_proxy = true; - var len = Math.min(parallel, urls.length); - for(i = 0; i < len; i++){ + var len = Math.min(parallel, urls.length); + for (i = 0; i < len; i++) { threads.push(thread()); } return new Promise(function (resolve, reject) { - Promise.all(threads).then(function(results){ + Promise.all(threads).then(function (results) { var urls = []; - for(var i = 0; i < results.length; i++){ - if(results[i]){ + for (var i = 0; i < results.length; i++) { + if (results[i]) { urls.push(results[i]); } } @@ -400,15 +408,15 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ }); }, - stop_searching: function(){ + stop_searching: function () { this.searching_for_proxy = false; - this.set_connection_status('disconnected'); + this.set_connection_status("disconnected"); }, // this allows the client to be notified when a proxy call is made. The notification // callback will be executed with the same arguments as the proxy call - add_notification: function(name, callback){ - if(!this.notifications[name]){ + add_notification: function (name, callback) { + if (!this.notifications[name]) { this.notifications[name] = []; } this.notifications[name].push(callback); @@ -422,35 +430,36 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ scale_read: function () { var self = this; if (self.use_debug_weight) { - return Promise.resolve({weight:this.debug_weight, unit:'Kg', info:'ok'}); + return Promise.resolve({ weight: this.debug_weight, unit: "Kg", info: "ok" }); } return new Promise(function (resolve, reject) { - self.message('scale_read',{}) - .then(function (weight) { - resolve(weight); - }, function () { //failed to read weight - resolve({weight:0.0, unit:'Kg', info:'ok'}); - }); + self.message("scale_read", {}).then( + function (weight) { + resolve(weight); + }, + function () { + //failed to read weight + resolve({ weight: 0.0, unit: "Kg", info: "ok" }); + } + ); }); }, // sets a custom weight, ignoring the proxy returned value. - debug_set_weight: function(kg){ + debug_set_weight: function (kg) { this.use_debug_weight = true; this.debug_weight = kg; }, // resets the custom weight and re-enable listening to the proxy for weight values - debug_reset_weight: function(){ + debug_reset_weight: function () { this.use_debug_weight = false; this.debug_weight = 0; }, - update_customer_facing_display: function(html) { - if (this.posbox_supports_display && this.get('status').status == 'connected') { - return this.message('customer_facing_display', - { html: html }, - { timeout: 5000 }); + update_customer_facing_display: function (html) { + if (this.posbox_supports_display && this.get("status").status == "connected") { + return this.message("customer_facing_display", { html: html }, { timeout: 5000 }); } }, @@ -458,30 +467,22 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ * @param {string} html * @returns {Promise} */ - take_ownership_over_customer_screen: function(html) { + take_ownership_over_customer_screen: function (html) { return this.message("take_control", { html: html }); }, /** * @returns {Promise} */ - test_ownership_of_customer_screen: function() { + test_ownership_of_customer_screen: function () { if (this.connection) { return this.message("test_ownership", {}); } - return Promise.reject({abort: true}); + return Promise.reject({ abort: true }); }, // asks the proxy to log some information, as with the debug.log you can provide several arguments. - log: function(){ - return this.message('log',{'arguments': _.toArray(arguments)}); + log: function () { + return this.message("log", { arguments: _.toArray(arguments) }); }, - -}); - -return { - JobQueue: JobQueue, - ProxyDevice: ProxyDevice, -}; - }); diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 5920bf08dc4..f790454cb85 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1,22 +1,21 @@ +/** @odoo-module */ /* global waitForWebfonts */ -odoo.define('point_of_sale.models', function (require) { -"use strict"; -var PosDB = require('point_of_sale.DB'); -var config = require('web.config'); -var core = require('web.core'); -var field_utils = require('web.field_utils'); -var time = require('web.time'); -var utils = require('web.utils'); -var { Gui } = require('point_of_sale.Gui'); -const { batched, uuidv4 } = require("point_of_sale.utils"); +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"; +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"; var QWeb = core.qweb; var _t = core._t; var round_di = utils.round_decimals; var round_pr = utils.round_precision; -const Registries = require('point_of_sale.Registries'); +import Registries from "@point_of_sale/js/Registries"; const { markRaw, reactive } = owl; // Container of the product images fetched during rendering @@ -32,14 +31,16 @@ const PRODUCT_ID_TO_IMAGE_CACHE = {}; */ class PosCollection extends Array { getByCID(cid) { - return this.find(item => item.cid == cid); + return this.find((item) => item.cid == cid); } add(item) { this.push(item); } remove(item) { - const index = this.findIndex(_item => item.cid == _item.cid); - if (index < 0) return index; + const index = this.findIndex((_item) => item.cid == _item.cid); + if (index < 0) { + return index; + } this.splice(index, 1); return index; } @@ -72,9 +73,9 @@ class PosModel { */ _getCID(obj) { if (obj.id) { - if (typeof obj.id == 'string') { + if (typeof obj.id == "string") { return obj.id; - } else if (typeof obj.id == 'number') { + } else if (typeof obj.id == "number") { return `c${obj.id}`; } } @@ -82,17 +83,17 @@ class PosModel { } } -class PosGlobalState extends PosModel { +export class PosGlobalState extends PosModel { constructor(obj) { super(obj); - this.db = new PosDB(); // a local database used to search trough products and categories & store pending orders + this.db = new PosDB(); // a local database used to search trough products and categories & store pending orders this.debug = config.isDebug(); //debug mode this.unwatched = markRaw({}); // Business data; loaded from the server at launch this.company_logo = null; - this.company_logo_base64 = ''; + this.company_logo_base64 = ""; this.currency = null; this.company = null; this.user = null; @@ -110,7 +111,7 @@ class PosGlobalState extends PosModel { // validation (order paid then sent to the backend). this.validated_orders_name_server_id_map = {}; - this.numpadMode = 'quantity'; + this.numpadMode = "quantity"; this.isEveryPartnerLoaded = false; this.isEveryProductLoaded = false; @@ -137,85 +138,85 @@ class PosGlobalState extends PosModel { // these dynamic attributes can be watched for change by other models or widgets Object.assign(this, { - 'synch': { status:'connected', pending:0 }, - 'orders': new PosCollection(), - 'selectedOrder': null, - 'selectedPartner': null, - 'selectedCategoryId': null, + synch: { status: "connected", pending: 0 }, + orders: new PosCollection(), + selectedOrder: null, + selectedPartner: null, + selectedCategoryId: null, }); } getDefaultSearchDetails() { return { - fieldName: 'RECEIPT_NUMBER', - searchTerm: '', + fieldName: "RECEIPT_NUMBER", + searchTerm: "", }; } async load_product_uom_unit() { const params = { - model: 'ir.model.data', - method:'check_object_reference', - args: ['uom', 'product_uom_unit'], + model: "ir.model.data", + method: "check_object_reference", + args: ["uom", "product_uom_unit"], }; const uom_id = await this.env.services.rpc(params); this.uom_unit_id = uom_id[1]; } - async after_load_server_data(){ + async after_load_server_data() { await this.load_product_uom_unit(); await this.load_orders(); this.set_start_order(); } - async load_server_data(){ + async load_server_data() { const loadedData = await this.env.services.rpc({ - model: 'pos.session', - method: 'load_pos_data', + model: "pos.session", + method: "load_pos_data", args: [[odoo.pos_session_id]], }); await this._processData(loadedData); return this.after_load_server_data(); } - async _processData(loadedData) { - this.version = loadedData['version']; - this.company = loadedData['res.company']; - this.dp = loadedData['decimal.precision']; - this.units = loadedData['uom.uom']; - this.units_by_id = loadedData['units_by_id']; - this.states = loadedData['res.country.state']; - this.countries = loadedData['res.country']; - this.langs = loadedData['res.lang']; - this.taxes = loadedData['account.tax']; - this.taxes_by_id = loadedData['taxes_by_id']; - this.pos_session = loadedData['pos.session']; + async _processData(loadedData) { + this.version = loadedData["version"]; + this.company = loadedData["res.company"]; + this.dp = loadedData["decimal.precision"]; + this.units = loadedData["uom.uom"]; + this.units_by_id = loadedData["units_by_id"]; + this.states = loadedData["res.country.state"]; + this.countries = loadedData["res.country"]; + this.langs = loadedData["res.lang"]; + this.taxes = loadedData["account.tax"]; + this.taxes_by_id = loadedData["taxes_by_id"]; + this.pos_session = loadedData["pos.session"]; this._loadPosSession(); - this.config = loadedData['pos.config']; + this.config = loadedData["pos.config"]; this._loadPoSConfig(); - this.bills = loadedData['pos.bill']; - this.partners = loadedData['res.partner']; + this.bills = loadedData["pos.bill"]; + this.partners = loadedData["res.partner"]; this.addPartners(this.partners); - this.picking_type = loadedData['stock.picking.type']; - this.user = loadedData['res.users']; - this.pricelists = loadedData['product.pricelist']; - this.default_pricelist = loadedData['default_pricelist']; - this.currency = loadedData['res.currency']; - this.db.add_categories(loadedData['pos.category']); - this._loadProductProduct(loadedData['product.product']); - this.db.add_packagings(loadedData['product.packaging']); - this.attributes_by_ptal_id = loadedData['attributes_by_ptal_id']; - this.cash_rounding = loadedData['account.cash.rounding']; - this.payment_methods = loadedData['pos.payment.method']; + this.picking_type = loadedData["stock.picking.type"]; + this.user = loadedData["res.users"]; + this.pricelists = loadedData["product.pricelist"]; + this.default_pricelist = loadedData["default_pricelist"]; + this.currency = loadedData["res.currency"]; + this.db.add_categories(loadedData["pos.category"]); + this._loadProductProduct(loadedData["product.product"]); + this.db.add_packagings(loadedData["product.packaging"]); + this.attributes_by_ptal_id = loadedData["attributes_by_ptal_id"]; + this.cash_rounding = loadedData["account.cash.rounding"]; + this.payment_methods = loadedData["pos.payment.method"]; this._loadPosPaymentMethod(); - this.fiscal_positions = loadedData['account.fiscal.position']; - this.base_url = loadedData['base_url']; + this.fiscal_positions = loadedData["account.fiscal.position"]; + this.base_url = loadedData["base_url"]; await this._loadFonts(); await this._loadPictures(); } _loadPosSession() { // We need to do it here, since only then the local storage has the correct uuid - this.db.save('pos_session_id', this.pos_session.id); - let orders = this.db.get_orders(); - let sequences = orders.map(order => order.data.sequence_number + 1) + this.db.save("pos_session_id", this.pos_session.id); + const orders = this.db.get_orders(); + const sequences = orders.map((order) => order.data.sequence_number + 1); this.pos_session.sequence_number = Math.max(this.pos_session.sequence_number, ...sequences); this.pos_session.login_number = odoo.login_number; } @@ -235,45 +236,45 @@ class PosGlobalState extends PosModel { const productMap = {}; const productTemplateMap = {}; - const modelProducts = products.map(product => { + const modelProducts = products.map((product) => { product.pos = this; product.applicablePricelistItems = {}; productMap[product.id] = product; - productTemplateMap[product.product_tmpl_id[0]] = (productTemplateMap[product.product_tmpl_id[0]] || []).concat(product); + productTemplateMap[product.product_tmpl_id[0]] = ( + productTemplateMap[product.product_tmpl_id[0]] || [] + ).concat(product); return Product.create(product); }); - for (let pricelist of this.pricelists) { + for (const pricelist of this.pricelists) { for (const pricelistItem of pricelist.items) { if (pricelistItem.product_id) { - let product_id = pricelistItem.product_id[0]; - let correspondingProduct = productMap[product_id]; + const product_id = pricelistItem.product_id[0]; + const correspondingProduct = productMap[product_id]; if (correspondingProduct) { this._assignApplicableItems(pricelist, correspondingProduct, pricelistItem); } - } - else if (pricelistItem.product_tmpl_id) { - let product_tmpl_id = pricelistItem.product_tmpl_id[0]; - let correspondingProducts = productTemplateMap[product_tmpl_id]; - for (let correspondingProduct of (correspondingProducts || [])) { + } else if (pricelistItem.product_tmpl_id) { + const product_tmpl_id = pricelistItem.product_tmpl_id[0]; + const correspondingProducts = productTemplateMap[product_tmpl_id]; + for (const correspondingProduct of correspondingProducts || []) { this._assignApplicableItems(pricelist, correspondingProduct, pricelistItem); } - } - else { + } else { for (const correspondingProduct of products) { this._assignApplicableItems(pricelist, correspondingProduct, pricelistItem); } } } } - this.db.add_products(modelProducts) + this.db.add_products(modelProducts); } _loadPosPaymentMethod() { // need to do this for pos_iot due to reference, this is a temporary fix - this.payment_methods_by_id = {} - for (let pm of this.payment_methods) { + this.payment_methods_by_id = {}; + for (const pm of this.payment_methods) { this.payment_methods_by_id[pm.id] = pm; - let PaymentInterface = this.electronic_payment_interfaces[pm.use_payment_terminal]; + const PaymentInterface = this.electronic_payment_interfaces[pm.use_payment_terminal]; if (PaymentInterface) { pm.payment_terminal = new PaymentInterface(this, pm); } @@ -284,7 +285,7 @@ class PosGlobalState extends PosModel { // Waiting for fonts to be loaded to prevent receipt printing // from printing empty receipt while loading Inconsolata // ( The font used for the receipt ) - waitForWebfonts(['Lato','Inconsolata'], function () { + waitForWebfonts(["Lato", "Inconsolata"], function () { resolve(); }); // The JS used to detect font loading is not 100% robust, so @@ -296,23 +297,23 @@ class PosGlobalState extends PosModel { this.company_logo = new Image(); return new Promise((resolve, reject) => { this.company_logo.onload = () => { - let img = this.company_logo; + const img = this.company_logo; let ratio = 1; - let targetwidth = 300; - let maxheight = 150; + const targetwidth = 300; + const maxheight = 150; if (img.width !== targetwidth) { ratio = targetwidth / img.width; } if (img.height * ratio > maxheight) { ratio = maxheight / img.height; } - let width = Math.floor(img.width * ratio); - let height = Math.floor(img.height * ratio); - let c = document.createElement('canvas'); - c.width = width; + const width = Math.floor(img.width * ratio); + const height = Math.floor(img.height * ratio); + const c = document.createElement("canvas"); + c.width = width; c.height = height; - let ctx = c.getContext('2d'); - ctx.drawImage(this.company_logo,0,0, width, height); + const ctx = c.getContext("2d"); + ctx.drawImage(this.company_logo, 0, 0, width, height); this.company_logo_base64 = c.toDataURL(); resolve(); @@ -321,53 +322,70 @@ class PosGlobalState extends PosModel { reject(); }; this.company_logo.crossOrigin = "anonymous"; - this.company_logo.src = '/web/binary/company_logo' + '?dbname=' + this.env.session.db + '&company=' + this.company.id + '&_' + Math.random(); + this.company_logo.src = + "/web/binary/company_logo" + + "?dbname=" + + this.env.session.db + + "&company=" + + this.company.id + + "&_" + + Math.random(); }); - } - prepare_new_partners_domain(){ - return [['write_date','>', this.db.get_partner_write_date()]]; + prepare_new_partners_domain() { + return [["write_date", ">", this.db.get_partner_write_date()]]; } // reload the list of partner, returns as a promise that resolves if there were // updated partners, and fails if not - load_new_partners(){ - return new Promise((resolve, reject) => { + load_new_partners() { + return new Promise((resolve, reject) => { var domain = this.prepare_new_partners_domain(); - this.env.services.rpc({ - model: 'pos.session', - method: 'get_pos_ui_res_partner_by_params', - args: [[odoo.pos_session_id], {domain}], - }, { - timeout: 3000, - shadow: true, - }) - .then(partners => { - if (this.addPartners(partners)) { // check if the partners we got were real updates - resolve(); - } else { - reject('Failed in updating partners.'); - } - }, function (type, err) { reject(); }); + this.env.services + .rpc( + { + model: "pos.session", + method: "get_pos_ui_res_partner_by_params", + args: [[odoo.pos_session_id], { domain }], + }, + { + timeout: 3000, + shadow: true, + } + ) + .then( + (partners) => { + if (this.addPartners(partners)) { + // check if the partners we got were real updates + resolve(); + } else { + reject("Failed in updating partners."); + } + }, + function (type, err) { + reject(); + } + ); }); } async updateIsEveryPartnerLoaded() { - let partnersCount = await this.env.services.rpc({ - model: 'res.partner', - method: 'search_count', + const partnersCount = await this.env.services.rpc({ + model: "res.partner", + method: "search_count", args: [[]], }); this.isEveryPartnerLoaded = partnersCount === this.db.partner_sorted.length; } async updateIsEveryProductLoaded() { - let productsCount = await this.env.services.rpc({ - model: 'product.product', - method: 'search_count', - args: [[['available_in_pos', '=', true]]], + const productsCount = await this.env.services.rpc({ + model: "product.product", + method: "search_count", + args: [[["available_in_pos", "=", true]]], }); - this.isEveryProductLoaded = productsCount === this.db.get_product_by_category(this.db.root_category_id).length; + this.isEveryProductLoaded = + productsCount === this.db.get_product_by_category(this.db.root_category_id).length; } setSelectedCategoryId(categoryId) { @@ -399,13 +417,13 @@ class PosGlobalState extends PosModel { return this.user.id; } cashierHasPriceControlRights() { - return !this.config.restrict_price_control || this.get_cashier().role == 'manager'; + return !this.config.restrict_price_control || this.get_cashier().role == "manager"; } _onReactiveOrderUpdated(order) { order.save_to_db(); } createReactiveOrder(json) { - const options = {pos:this}; + const options = { pos: this }; if (json) { options.json = json; } @@ -413,14 +431,14 @@ class PosGlobalState extends PosModel { } makeOrderReactive(order) { const batchedCallback = batched(() => { - this._onReactiveOrderUpdated(order) + this._onReactiveOrderUpdated(order); }); order = reactive(order, batchedCallback); order.save_to_db(); return order; } // creates a new empty order and sets it as the current order - add_new_order(){ + add_new_order() { const order = this.createReactiveOrder(); this.orders.add(order); this.selectedOrder = order; @@ -433,7 +451,7 @@ class PosGlobalState extends PosModel { * Second load all orders belonging to the same config but from other sessions, * Only if tho order has orderlines. */ - async load_orders(){ + async load_orders() { var jsons = this.db.get_unpaid_orders(); await this._loadMissingProducts(jsons); await this._loadMissingPartners(jsons); @@ -445,16 +463,19 @@ class PosGlobalState extends PosModel { orders.push(this.createReactiveOrder(json)); } } - for (var i = 0; i < jsons.length; i++) { - var json = jsons[i]; - if (json.pos_session_id !== this.pos_session.id && (json.lines.length > 0 || json.statement_ids.length > 0)) { + for (i = 0; i < jsons.length; i++) { + json = jsons[i]; + if ( + json.pos_session_id !== this.pos_session.id && + (json.lines.length > 0 || json.statement_ids.length > 0) + ) { orders.push(this.createReactiveOrder(json)); } else if (json.pos_session_id !== this.pos_session.id) { this.db.remove_unpaid_order(jsons[i]); } } - orders = orders.sort(function(a,b){ + orders = orders.sort(function (a, b) { return a.sequence_number - b.sequence_number; }); @@ -469,31 +490,36 @@ class PosGlobalState extends PosModel { for (const order of orders) { for (const line of order.lines) { const productId = line[2].product_id; - if (missingProductIds.has(productId)) continue; + if (missingProductIds.has(productId)) { + continue; + } if (!this.db.get_product_by_id(productId)) { missingProductIds.add(productId); } } } const products = await this.env.services.rpc({ - model: 'pos.session', - method: 'get_pos_ui_product_product_by_params', - args: [odoo.pos_session_id, {domain: [['id', 'in', [...missingProductIds]]]}], + model: "pos.session", + method: "get_pos_ui_product_product_by_params", + args: [odoo.pos_session_id, { domain: [["id", "in", [...missingProductIds]]] }], }); this._loadProductProduct(products); } // load the partners based on the ids async _loadPartners(partnerIds) { if (partnerIds.length > 0) { - var domain = [['id','in', partnerIds]]; - const fetchedPartners = await this.env.services.rpc({ - model: 'pos.session', - method: 'get_pos_ui_res_partner_by_params', - args: [[odoo.pos_session_id], {domain}], - }, { - timeout: 3000, - shadow: true, - }); + var domain = [["id", "in", partnerIds]]; + const fetchedPartners = await this.env.services.rpc( + { + model: "pos.session", + method: "get_pos_ui_res_partner_by_params", + args: [[odoo.pos_session_id], { domain }], + }, + { + timeout: 3000, + shadow: true, + } + ); this.addPartners(fetchedPartners); } } @@ -501,7 +527,9 @@ class PosGlobalState extends PosModel { const missingPartnerIds = new Set([]); for (const order of orders) { const partnerId = order.partner_id; - if(missingPartnerIds.has(partnerId)) continue; + if (missingPartnerIds.has(partnerId)) { + continue; + } if (partnerId && !this.db.get_partner_by_id(partnerId)) { missingPartnerIds.add(partnerId); } @@ -512,17 +540,23 @@ class PosGlobalState extends PosModel { let page = 0; let products = []; do { - products = await this.env.services.rpc({ - model: 'pos.session', - method: 'get_pos_ui_product_product_by_params', - args: [odoo.pos_session_id, { - offset: page * this.config.limited_products_amount, - limit: this.config.limited_products_amount, - }], - }, { shadow: true }); + products = await this.env.services.rpc( + { + model: "pos.session", + method: "get_pos_ui_product_product_by_params", + args: [ + odoo.pos_session_id, + { + offset: page * this.config.limited_products_amount, + limit: this.config.limited_products_amount, + }, + ], + }, + { shadow: true } + ); this._loadProductProduct(products); page += 1; - } while(products.length == this.config.limited_products_amount); + } while (products.length == this.config.limited_products_amount); } async loadPartnersBackground() { // Start at the first page since the first set of loaded partners are not actually in the @@ -530,37 +564,42 @@ class PosGlobalState extends PosModel { let i = 0; let partners = []; do { - partners = await this.env.services.rpc({ - model: 'pos.session', - method: 'get_pos_ui_res_partner_by_params', - args: [ - [odoo.pos_session_id], - { - limit: this.config.limited_partners_amount, - offset: this.config.limited_partners_amount * i, - }, - ], - context: this.env.session.user_context, - }, { shadow: true }); + partners = await this.env.services.rpc( + { + model: "pos.session", + method: "get_pos_ui_res_partner_by_params", + args: [ + [odoo.pos_session_id], + { + limit: this.config.limited_partners_amount, + offset: this.config.limited_partners_amount * i, + }, + ], + context: this.env.session.user_context, + }, + { shadow: true } + ); this.addPartners(partners); i += 1; - } while(partners.length); + } while (partners.length); } async getProductInfo(product, quantity) { const order = this.get_order(); // check back-end method `get_product_info_pos` to see what it returns // We do this so it's easier to override the value returned and use it in the component template later const productInfo = await this.env.services.rpc({ - model: 'product.product', - method: 'get_product_info_pos', - args: [[product.id], + model: "product.product", + method: "get_product_info_pos", + args: [ + [product.id], product.get_price(order.pricelist, quantity), quantity, - this.config.id], - kwargs: {context: this.env.session.user_context}, + this.config.id, + ], + kwargs: { context: this.env.session.user_context }, }); - const priceWithoutTax = productInfo['all_prices']['price_without_tax']; + const priceWithoutTax = productInfo["all_prices"]["price_without_tax"]; const margin = priceWithoutTax - product.standard_price; const orderPriceWithoutTax = order.get_total_without_tax(); const orderCost = order.get_total_cost(); @@ -568,21 +607,31 @@ class PosGlobalState extends PosModel { const costCurrency = this.format_currency(product.standard_price); const marginCurrency = this.format_currency(margin); - const marginPercent = priceWithoutTax ? Math.round(margin/priceWithoutTax * 10000) / 100 : 0; + const marginPercent = priceWithoutTax + ? Math.round((margin / priceWithoutTax) * 10000) / 100 + : 0; const orderPriceWithoutTaxCurrency = this.format_currency(orderPriceWithoutTax); const orderCostCurrency = this.format_currency(orderCost); const orderMarginCurrency = this.format_currency(orderMargin); - const orderMarginPercent = orderPriceWithoutTax ? Math.round(orderMargin/orderPriceWithoutTax * 10000) / 100 : 0; + const orderMarginPercent = orderPriceWithoutTax + ? Math.round((orderMargin / orderPriceWithoutTax) * 10000) / 100 + : 0; return { - costCurrency, marginCurrency, marginPercent, orderPriceWithoutTaxCurrency, - orderCostCurrency, orderMarginCurrency, orderMarginPercent,productInfo - } + costCurrency, + marginCurrency, + marginPercent, + orderPriceWithoutTaxCurrency, + orderCostCurrency, + orderMarginCurrency, + orderMarginPercent, + productInfo, + }; } async getClosePosInfo() { const closingData = await this.env.services.rpc({ - model: 'pos.session', - method: 'get_closing_control_data', - args: [[this.pos_session.id]] + model: "pos.session", + method: "get_closing_control_data", + args: [[this.pos_session.id]], }); const ordersDetails = closingData.orders_details; const paymentsAmount = closingData.payments_amount; @@ -595,63 +644,79 @@ class PosGlobalState extends PosModel { const cashControl = this.config.cash_control; // component state and refs definition - const state = {notes: '', acceptClosing: false, payments: {}}; + const state = { notes: "", acceptClosing: false, payments: {} }; if (cashControl) { - state.payments[defaultCashDetails.id] = {counted: 0, difference: -defaultCashDetails.amount, number: 0}; + state.payments[defaultCashDetails.id] = { + counted: 0, + difference: -defaultCashDetails.amount, + number: 0, + }; } if (otherPaymentMethods.length > 0) { - otherPaymentMethods.forEach(pm => { - if (pm.type === 'bank') { - state.payments[pm.id] = {counted: this.round_decimals_currency(pm.amount), difference: 0, number: pm.number} + otherPaymentMethods.forEach((pm) => { + if (pm.type === "bank") { + state.payments[pm.id] = { + counted: this.round_decimals_currency(pm.amount), + difference: 0, + number: pm.number, + }; } - }) + }); } return { - ordersDetails, paymentsAmount, payLaterAmount, openingNotes, defaultCashDetails, otherPaymentMethods, - isManager, amountAuthorizedDiff, state, cashControl - } + ordersDetails, + paymentsAmount, + payLaterAmount, + openingNotes, + defaultCashDetails, + otherPaymentMethods, + isManager, + amountAuthorizedDiff, + state, + cashControl, + }; } - set_start_order(){ + set_start_order() { if (this.orders.length && !this.selectedOrder) { - this.selectedOrder = this.orders[0] + this.selectedOrder = this.orders[0]; } else { this.add_new_order(); } } // return the current order - get_order(){ + get_order() { return this.selectedOrder; } // change the current order - set_order(order, options){ + set_order(order, options) { this.selectedOrder = order; } // return the list of unpaid orders - get_order_list(){ + get_order_list() { return this.orders; } - _convert_product_img_to_base64 (product, url) { + _convert_product_img_to_base64(product, url) { return new Promise(function (resolve, reject) { var img = new Image(); img.onload = function () { - var canvas = document.createElement('CANVAS'); - var ctx = canvas.getContext('2d'); + var canvas = document.createElement("CANVAS"); + var ctx = canvas.getContext("2d"); canvas.height = this.height; canvas.width = this.width; - ctx.drawImage(this,0,0); + ctx.drawImage(this, 0, 0); - var dataURL = canvas.toDataURL('image/jpeg'); + var dataURL = canvas.toDataURL("image/jpeg"); canvas = null; resolve([product, dataURL]); }; - img.crossOrigin = 'use-credentials'; + img.crossOrigin = "use-credentials"; img.src = url; }); } @@ -666,14 +731,23 @@ class PosGlobalState extends PosModel { send_current_order_to_customer_facing_display() { var self = this; - if (!this.config.iface_customer_facing_display) return; + if (!this.config.iface_customer_facing_display) { + return; + } this.render_html_for_customer_facing_display().then((rendered_html) => { if (self.env.pos.customer_display) { - var $renderedHtml = $('
').html(rendered_html); - $(self.env.pos.customer_display.document.body).html($renderedHtml.find('.pos-customer_facing_display')); - var orderlines = $(self.env.pos.customer_display.document.body).find('.pos_orderlines_list'); + var $renderedHtml = $("
").html(rendered_html); + $(self.env.pos.customer_display.document.body).html( + $renderedHtml.find(".pos-customer_facing_display") + ); + var orderlines = $(self.env.pos.customer_display.document.body).find( + ".pos_orderlines_list" + ); orderlines.scrollTop(orderlines.prop("scrollHeight")); - } else if (this.config.iface_customer_facing_display_via_proxy && this.env.proxy.posbox_supports_display) { + } else if ( + this.config.iface_customer_facing_display_via_proxy && + this.env.proxy.posbox_supports_display + ) { this.env.proxy.update_customer_facing_display(rendered_html); } }); @@ -682,7 +756,7 @@ class PosGlobalState extends PosModel { /** * @returns {Promise} */ - render_html_for_customer_facing_display () { + render_html_for_customer_facing_display() { var self = this; var order = this.get_order(); @@ -700,13 +774,15 @@ class PosGlobalState extends PosModel { // only download and convert image if we haven't done it before if (!(product.id in PRODUCT_ID_TO_IMAGE_CACHE)) { - get_image_promises.push(self._convert_product_img_to_base64(product, image_url)); + get_image_promises.push( + self._convert_product_img_to_base64(product, image_url) + ); } }); } return Promise.all(get_image_promises).then(function (productIdImagePairs) { - for (let [product, image] of productIdImagePairs) { + for (const [product, image] of productIdImagePairs) { PRODUCT_ID_TO_IMAGE_CACHE[product.id] = image; } // Collect the product images that will be used in rendering the customer display template. @@ -716,18 +792,18 @@ class PosGlobalState extends PosModel { productImages[line.product.id] = PRODUCT_ID_TO_IMAGE_CACHE[line.product.id]; } } - return QWeb.render('CustomerFacingDisplayOrder', { + return QWeb.render("CustomerFacingDisplayOrder", { pos: self, origin: window.location.origin, order: order, - productImages + productImages, }); }); } // saves the order locally and try to send it to the backend. // it returns a promise that succeeds after having tried to send the order and all the other pending orders. - push_orders (order, opts) { + push_orders(order, opts) { opts = opts || {}; var self = this; @@ -746,7 +822,7 @@ class PosGlobalState extends PosModel { }); } - push_single_order (order, opts) { + push_single_order(order, opts) { opts = opts || {}; const self = this; const order_id = self.db.add_order(order.export_as_JSON()); @@ -768,14 +844,17 @@ class PosGlobalState extends PosModel { _flush_orders(orders, options) { var self = this; - return this._save_to_server(orders, options).then(function (server_ids) { - for (let i = 0; i < server_ids.length; i++) { - self.validated_orders_name_server_id_map[server_ids[i].pos_reference] = server_ids[i].id; - } - return server_ids; - }).finally(function() { - self._after_flush_orders(orders); - }); + return this._save_to_server(orders, options) + .then(function (server_ids) { + for (let i = 0; i < server_ids.length; i++) { + self.validated_orders_name_server_id_map[server_ids[i].pos_reference] = + server_ids[i].id; + } + return server_ids; + }) + .finally(function () { + self._after_flush_orders(orders); + }); } /** * Hook method after _flush_orders resolved or rejected. @@ -788,7 +867,9 @@ class PosGlobalState extends PosModel { for (const order of orders) { for (const line of order.data.lines) { const refundDetail = this.toRefundLines[line[2].refunded_orderline_id]; - if (!refundDetail) continue; + if (!refundDetail) { + continue; + } // Collect the backend id of the refunded orders. refundedOrderIds.add(refundDetail.orderline.orderBackendId); // Reset the refund detail for the orderline. @@ -803,10 +884,11 @@ class PosGlobalState extends PosModel { } } set_synch(status, pending) { - if (['connected', 'connecting', 'error', 'disconnected'].indexOf(status) === -1) { - console.error(status, ' is not a known connection state.'); + if (["connected", "connecting", "error", "disconnected"].indexOf(status) === -1) { + console.error(status, " is not a known connection state."); } - pending = pending || this.db.get_orders().length + this.db.get_ids_to_remove_from_server().length; + pending = + pending || this.db.get_orders().length + this.db.get_ids_to_remove_from_server().length; this.synch = { status, pending }; } @@ -815,79 +897,95 @@ class PosGlobalState extends PosModel { // - timeout: timeout for the rpc call in ms // returns a promise that resolves with the list of // server generated ids for the sent orders - _save_to_server (orders, options) { + _save_to_server(orders, options) { if (!orders || !orders.length) { return Promise.resolve([]); } - this.set_synch('connecting', orders.length); + this.set_synch("connecting", orders.length); options = options || {}; var self = this; - var timeout = typeof options.timeout === 'number' ? options.timeout : 30000 * orders.length; + var timeout = typeof options.timeout === "number" ? options.timeout : 30000 * orders.length; // Keep the order ids that are about to be sent to the // backend. In between create_from_ui and the success callback // new orders may have been added to it. - var order_ids_to_sync = _.pluck(orders, 'id'); + var order_ids_to_sync = _.pluck(orders, "id"); // we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice, // then we want to notify the user that we are waiting on something ) - var args = [_.map(orders, function (order) { + var args = [ + _.map(orders, function (order) { order.to_invoice = options.to_invoice || false; return order; - })]; + }), + ]; args.push(options.draft || false); - return this.env.services.rpc({ - model: 'pos.order', - method: 'create_from_ui', - args: args, - kwargs: {context: this.env.session.user_context}, - }, { - timeout: timeout, - shadow: !options.to_invoice - }) + return this.env.services + .rpc( + { + model: "pos.order", + method: "create_from_ui", + args: args, + kwargs: { context: this.env.session.user_context }, + }, + { + timeout: timeout, + shadow: !options.to_invoice, + } + ) .then(function (server_ids) { _.each(order_ids_to_sync, function (order_id) { self.db.remove_order(order_id); }); self.failed = false; - self.set_synch('connected'); + self.set_synch("connected"); return server_ids; - }).catch(function (error){ - console.warn('Failed to send orders:', orders); - if(error.code === 200 ){ // Business Logic Error, not a connection problem + }) + .catch(function (error) { + console.warn("Failed to send orders:", orders); + if (error.code === 200) { + // Business Logic Error, not a connection problem // Hide error if already shown before ... if ((!self.failed || options.show_error) && !options.to_invoice) { self.failed = error; - self.set_synch('error'); + self.set_synch("error"); throw error; } } - self.set_synch('disconnected'); + self.set_synch("disconnected"); throw error; }); } // Exports the paid orders (the ones waiting for internet connection) export_paid_orders() { - return JSON.stringify({ - 'paid_orders': this.db.get_orders(), - 'session': this.pos_session.name, - 'session_id': this.pos_session.id, - 'date': (new Date()).toUTCString(), - 'version': this.version.server_version_info, - },null,2); + return JSON.stringify( + { + paid_orders: this.db.get_orders(), + session: this.pos_session.name, + session_id: this.pos_session.id, + date: new Date().toUTCString(), + version: this.version.server_version_info, + }, + null, + 2 + ); } // Exports the unpaid orders (the tabs) export_unpaid_orders() { - return JSON.stringify({ - 'unpaid_orders': this.db.get_unpaid_orders(), - 'session': this.pos_session.name, - 'session_id': this.pos_session.id, - 'date': (new Date()).toUTCString(), - 'version': this.version.server_version_info, - },null,2); + return JSON.stringify( + { + unpaid_orders: this.db.get_unpaid_orders(), + session: this.pos_session.name, + session_id: this.pos_session.id, + date: new Date().toUTCString(), + version: this.version.server_version_info, + }, + null, + 2 + ); } // This imports paid or unpaid orders from a json file whose @@ -904,7 +1002,7 @@ class PosGlobalState extends PosModel { // Orders that were not imported because they already exist (uid conflict) unpaid_skipped_existing: 0, // Orders that were not imported because they belong to another session - unpaid_skipped_session: 0, + unpaid_skipped_session: 0, // The list of session ids to which skipped orders belong. unpaid_skipped_sessions: [], }; @@ -918,17 +1016,16 @@ class PosGlobalState extends PosModel { } if (json.unpaid_orders) { - - var orders = []; + var orders = []; var existing = this.get_order_list(); var existing_uids = {}; var skipped_sessions = {}; - for (var i = 0; i < existing.length; i++) { + for (i = 0; i < existing.length; i++) { existing_uids[existing[i].uid] = true; } - for (var i = 0; i < json.unpaid_orders.length; i++) { + for (i = 0; i < json.unpaid_orders.length; i++) { var order = json.unpaid_orders[i]; if (order.pos_session_id !== this.pos_session.id) { report.unpaid_skipped_session += 1; @@ -940,7 +1037,7 @@ class PosGlobalState extends PosModel { } } - orders = orders.sort(function(a,b){ + orders = orders.sort(function (a, b) { return a.sequence_number - b.sequence_number; }); @@ -955,7 +1052,7 @@ class PosGlobalState extends PosModel { return report; } - _load_orders(){ + _load_orders() { var jsons = this.db.get_unpaid_orders(); var orders = []; var not_loaded_count = 0; @@ -970,10 +1067,14 @@ class PosGlobalState extends PosModel { } if (not_loaded_count) { - console.info('There are '+not_loaded_count+' locally saved unpaid orders belonging to another session'); + console.info( + "There are " + + not_loaded_count + + " locally saved unpaid orders belonging to another session" + ); } - orders = orders.sort(function(a,b){ + orders = orders.sort(function (a, b) { return a.sequence_number - b.sequence_number; }); @@ -987,11 +1088,12 @@ class PosGlobalState extends PosModel { * _compute_amount in addons/account/models/account.py */ _compute_all(tax, base_amount, quantity, price_exclude) { - if(price_exclude === undefined) + if (price_exclude === undefined) { var price_include = tax.price_include; - else - var price_include = !price_exclude; - if (tax.amount_type === 'fixed') { + } else { + price_include = !price_exclude; + } + if (tax.amount_type === "fixed") { // Use sign on base_amount and abs on quantity to take into account the sign of the base amount, // which includes the sign of the quantity and the sign of the price_unit // Amount is the fixed price for the tax, it can be negative @@ -1000,22 +1102,23 @@ class PosGlobalState extends PosModel { // sign of the price unit. // When the price unit is equal to 0, the sign of the quantity is absorbed in base_amount then // a "else" case is needed. - if (base_amount) + if (base_amount) { return Math.sign(base_amount) * Math.abs(quantity) * tax.amount; - else + } else { return quantity * tax.amount; + } } - if (tax.amount_type === 'percent' && !price_include){ - return base_amount * tax.amount / 100; + if (tax.amount_type === "percent" && !price_include) { + return (base_amount * tax.amount) / 100; } - if (tax.amount_type === 'percent' && price_include){ - return base_amount - (base_amount / (1 + tax.amount / 100)); + if (tax.amount_type === "percent" && price_include) { + return base_amount - base_amount / (1 + tax.amount / 100); } - if (tax.amount_type === 'division' && !price_include) { + if (tax.amount_type === "division" && !price_include) { return base_amount / (1 - tax.amount / 100) - base_amount; } - if (tax.amount_type === 'division' && price_include) { - return base_amount - (base_amount * (tax.amount / 100)); + if (tax.amount_type === "division" && price_include) { + return base_amount - base_amount * (tax.amount / 100); } return false; } @@ -1026,46 +1129,52 @@ class PosGlobalState extends PosModel { * * Read comments in the python side method for more details about each sub-methods. */ - compute_all(taxes, price_unit, quantity, currency_rounding, handle_price_include=true) { + compute_all(taxes, price_unit, quantity, currency_rounding, handle_price_include = true) { var self = this; // 1) Flatten the taxes. - var _collect_taxes = function(taxes, all_taxes){ + var _collect_taxes = function (taxes, all_taxes) { taxes = [...taxes].sort(function (tax1, tax2) { return tax1.sequence - tax2.sequence; }); - _(taxes).each(function(tax){ - if(tax.amount_type === 'group') + _(taxes).each(function (tax) { + if (tax.amount_type === "group") { all_taxes = _collect_taxes(tax.children_tax_ids, all_taxes); - else + } else { all_taxes.push(tax); + } }); return all_taxes; - } - var collect_taxes = function(taxes){ + }; + var collect_taxes = function (taxes) { return _collect_taxes(taxes, []); - } + }; taxes = collect_taxes(taxes); // 2) Deal with the rounding methods - var round_tax = this.company.tax_calculation_rounding_method != 'round_globally'; + var round_tax = this.company.tax_calculation_rounding_method != "round_globally"; var initial_currency_rounding = currency_rounding; - if(!round_tax) + if (!round_tax) { currency_rounding = currency_rounding * 0.00001; + } // 3) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation. - var recompute_base = function(base_amount, fixed_amount, percent_amount, division_amount){ - return (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0) * (100 - division_amount) / 100; - } + var recompute_base = function (base_amount, fixed_amount, percent_amount, division_amount) { + return ( + (((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0)) * + (100 - division_amount)) / + 100 + ); + }; var base = round_pr(price_unit * quantity, initial_currency_rounding); var sign = 1; - if(base < 0){ + if (base < 0) { base = -base; sign = -1; } @@ -1079,28 +1188,33 @@ class PosGlobalState extends PosModel { var incl_division_amount = 0.0; var cached_tax_amounts = {}; - if (handle_price_include){ - _(taxes.reverse()).each(function(tax){ - if(tax.include_base_amount){ - base = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount); + if (handle_price_include) { + _(taxes.reverse()).each(function (tax) { + if (tax.include_base_amount) { + base = recompute_base( + base, + incl_fixed_amount, + incl_percent_amount, + incl_division_amount + ); incl_fixed_amount = 0.0; incl_percent_amount = 0.0; incl_division_amount = 0.0; store_included_tax_total = true; } - if(tax.price_include){ - if(tax.amount_type === 'percent') + if (tax.price_include) { + if (tax.amount_type === "percent") { incl_percent_amount += tax.amount; - else if(tax.amount_type === 'division') + } else if (tax.amount_type === "division") { incl_division_amount += tax.amount; - else if(tax.amount_type === 'fixed') - incl_fixed_amount += Math.abs(quantity) * tax.amount - else{ + } else if (tax.amount_type === "fixed") { + incl_fixed_amount += Math.abs(quantity) * tax.amount; + } else { var tax_amount = self._compute_all(tax, base, quantity); incl_fixed_amount += tax_amount; cached_tax_amounts[i] = tax_amount; } - if(store_included_tax_total){ + if (store_included_tax_total) { total_included_checkpoints[i] = base; store_included_tax_total = false; } @@ -1109,7 +1223,10 @@ class PosGlobalState extends PosModel { }); } - var total_excluded = round_pr(recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount), initial_currency_rounding); + var total_excluded = round_pr( + recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount), + initial_currency_rounding + ); var total_included = total_excluded; // 4) Iterate the taxes in the sequence order to fill missing base/amount values. @@ -1121,34 +1238,43 @@ class PosGlobalState extends PosModel { var taxes_vals = []; i = 0; var cumulated_tax_included_amount = 0; - _(taxes.reverse()).each(function(tax){ - if(tax.price_include || tax.is_base_affected) + _(taxes.reverse()).each(function (tax) { + if (tax.price_include || tax.is_base_affected) { var tax_base_amount = base; - else - var tax_base_amount = total_excluded; + } else { + tax_base_amount = total_excluded; + } - if(!skip_checkpoint && tax.price_include && total_included_checkpoints[i] !== undefined){ - var tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount); + if ( + !skip_checkpoint && + tax.price_include && + total_included_checkpoints[i] !== undefined + ) { + var tax_amount = + total_included_checkpoints[i] - (base + cumulated_tax_included_amount); cumulated_tax_included_amount = 0; - }else - var tax_amount = self._compute_all(tax, tax_base_amount, quantity, true); + } else { + tax_amount = self._compute_all(tax, tax_base_amount, quantity, true); + } tax_amount = round_pr(tax_amount, currency_rounding); - if(tax.price_include && total_included_checkpoints[i] === undefined) + if (tax.price_include && total_included_checkpoints[i] === undefined) { cumulated_tax_included_amount += tax_amount; + } taxes_vals.push({ - 'id': tax.id, - 'name': tax.name, - 'amount': sign * tax_amount, - 'base': sign * round_pr(tax_base_amount, currency_rounding), + id: tax.id, + name: tax.name, + amount: sign * tax_amount, + base: sign * round_pr(tax_base_amount, currency_rounding), }); - if(tax.include_base_amount){ + if (tax.include_base_amount) { base += tax_amount; - if(!tax.price_include) + if (!tax.price_include) { skip_checkpoint = true; + } } total_included += tax_amount; @@ -1156,9 +1282,9 @@ class PosGlobalState extends PosModel { }); return { - 'taxes': taxes_vals, - 'total_excluded': sign * round_pr(total_excluded, this.currency.rounding), - 'total_included': sign * round_pr(total_included, this.currency.rounding), + taxes: taxes_vals, + total_excluded: sign * round_pr(total_excluded, this.currency.rounding), + total_included: sign * round_pr(total_included, this.currency.rounding), }; } @@ -1168,11 +1294,11 @@ class PosGlobalState extends PosModel { * @param {object | falsy} fpos - fiscal position * @returns {object[]} */ - get_taxes_after_fp(taxIds, fpos){ + get_taxes_after_fp(taxIds, fpos) { if (!fpos) { return taxIds.map((taxId) => this.taxes_by_id[taxId]); } - let mappedTaxes = []; + const mappedTaxes = []; for (const taxId of taxIds) { const tax = this.taxes_by_id[taxId]; if (tax) { @@ -1194,7 +1320,7 @@ class PosGlobalState extends PosModel { } } return _.uniq(mappedTaxes, (tax) => tax.id); - } + } /** * TODO: We can probably remove this here and put it somewhere else. @@ -1204,14 +1330,14 @@ class PosGlobalState extends PosModel { * * @param {OdooEvent} ev */ - _trigger_up (ev) { + _trigger_up(ev) { if (ev.is_stopped()) { return; } const payload = ev.data; - if (ev.name === 'call_service') { + if (ev.name === "call_service") { let args = payload.args || []; - if (payload.service === 'ajax' && payload.method === 'rpc') { + if (payload.service === "ajax" && payload.method === "rpc") { // ajax service uses an extra 'target' argument for rpc args = args.concat(ev.target); } @@ -1222,26 +1348,28 @@ class PosGlobalState extends PosModel { } isProductQtyZero(qty) { - return utils.float_is_zero(qty, this.dp['Product Unit of Measure']); + return utils.float_is_zero(qty, this.dp["Product Unit of Measure"]); } formatProductQty(qty) { - return field_utils.format.float(qty, { digits: [true, this.dp['Product Unit of Measure']] }); + return field_utils.format.float(qty, { + digits: [true, this.dp["Product Unit of Measure"]], + }); } format_currency(amount, precision) { amount = this.format_currency_no_symbol(amount, precision, this.currency); - if (this.currency.position === 'after') { - return amount + ' ' + (this.currency.symbol || ''); + if (this.currency.position === "after") { + return amount + " " + (this.currency.symbol || ""); } else { - return (this.currency.symbol || '') + ' ' + amount; + return (this.currency.symbol || "") + " " + amount; } } format_currency_no_symbol(amount, precision, currency) { if (!currency) { - currency = this.currency + currency = this.currency; } var decimals = currency.decimal_places; @@ -1249,7 +1377,7 @@ class PosGlobalState extends PosModel { decimals = this.dp[precision]; } - if (typeof amount === 'number') { + if (typeof amount === "number") { amount = round_di(amount, decimals).toFixed(decimals); amount = field_utils.format.float(round_di(amount, decimals), { digits: [69, decimals], @@ -1261,9 +1389,7 @@ class PosGlobalState extends PosModel { format_pr(value, precision) { var decimals = - precision > 0 - ? Math.max(0, Math.ceil(Math.log(1.0 / precision) / Math.log(10))) - : 0; + precision > 0 ? Math.max(0, Math.ceil(Math.log(1.0 / precision) / Math.log(10))) : 0; return value.toFixed(decimals); } @@ -1287,34 +1413,34 @@ class PosGlobalState extends PosModel { } getCurrencySymbol() { - return this.currency ? this.currency.symbol : '$'; + return this.currency ? this.currency.symbol : "$"; } /** * Make the products corresponding to the given ids to be available_in_pos and * fetch them to be added on the loaded products. */ - async _addProducts(ids, setAvailable=true){ - if(setAvailable){ + async _addProducts(ids, setAvailable = true) { + if (setAvailable) { await this.env.services.rpc({ - model: 'product.product', - method: 'write', - args: [ids, {'available_in_pos': true}], + model: "product.product", + method: "write", + args: [ids, { available_in_pos: true }], context: this.env.session.user_context, }); } - let product = await this.env.services.rpc({ - model: 'pos.session', - method: 'get_pos_ui_product_product_by_params', - args: [odoo.pos_session_id, {domain: [['id', 'in', ids]]}], + const product = await this.env.services.rpc({ + model: "pos.session", + method: "get_pos_ui_product_product_by_params", + args: [odoo.pos_session_id, { domain: [["id", "in", ids]] }], }); this._loadProductProduct(product); } async refreshTotalDueOfPartner(partner) { const partnerWithUpdatedTotalDue = await this.env.services.rpc({ - model: 'res.partner', - method: 'search_read', - fields: ['total_due'], - domain: [['id', '=', partner.id]], + model: "res.partner", + method: "search_read", + fields: ["total_due"], + domain: [["id", "=", partner.id]], }); this.db.update_partners(partnerWithUpdatedTotalDue); return partnerWithUpdatedTotalDue; @@ -1335,23 +1461,23 @@ Registries.Model.add(PosGlobalState); * @param {Object} ImplementedPaymentInterface - implemented * PaymentInterface */ -function register_payment_method(use_payment_terminal, ImplementedPaymentInterface) { - PosGlobalState.prototype.electronic_payment_interfaces[use_payment_terminal] = ImplementedPaymentInterface; -}; +export function register_payment_method(use_payment_terminal, ImplementedPaymentInterface) { + PosGlobalState.prototype.electronic_payment_interfaces[use_payment_terminal] = + ImplementedPaymentInterface; +} - -class Product extends PosModel { +export class Product extends PosModel { isAllowOnlyOneLot() { const productUnit = this.get_unit(); - return this.tracking === 'lot' || !productUnit || !productUnit.is_pos_groupable; + return this.tracking === "lot" || !productUnit || !productUnit.is_pos_groupable; } get_unit() { var unit_id = this.uom_id; - if(!unit_id){ + if (!unit_id) { return undefined; } unit_id = unit_id[0]; - if(!this.pos){ + if (!this.pos) { return undefined; } return this.pos.units_by_id[unit_id]; @@ -1366,17 +1492,19 @@ class Product extends PosModel { // product.pricelist.item records are loaded with a search_read // and were automatically sorted based on their _order by the // ORM. After that they are added in this order to the pricelists. - get_price(pricelist, quantity, price_extra){ + get_price(pricelist, quantity, price_extra) { var self = this; var date = moment(); // In case of nested pricelists, it is necessary that all pricelists are made available in // the POS. Display a basic alert to the user in this case. if (!pricelist) { - alert(_t( - 'An error occurred when loading product prices. ' + - 'Make sure all pricelists are available in the POS.' - )); + alert( + _t( + "An error occurred when loading product prices. " + + "Make sure all pricelists are available in the POS." + ) + ); } var category_ids = []; @@ -1386,14 +1514,19 @@ class Product extends PosModel { category = category.parent; } - var pricelist_items = _.filter(self.applicablePricelistItems[pricelist.id], function (item) { - return (! item.categ_id || _.contains(category_ids, item.categ_id[0])) && - (! item.date_start || moment.utc(item.date_start).isSameOrBefore(date)) && - (! item.date_end || moment.utc(item.date_end).isSameOrAfter(date)); - }); + var pricelist_items = _.filter( + self.applicablePricelistItems[pricelist.id], + function (item) { + return ( + (!item.categ_id || _.contains(category_ids, item.categ_id[0])) && + (!item.date_start || moment.utc(item.date_start).isSameOrBefore(date)) && + (!item.date_end || moment.utc(item.date_end).isSameOrAfter(date)) + ); + } + ); var price = self.lst_price; - if (price_extra){ + if (price_extra) { price += price_extra; } _.find(pricelist_items, function (rule) { @@ -1401,25 +1534,26 @@ class Product extends PosModel { return false; } - if (rule.base === 'pricelist') { - let base_pricelist = _.find(self.pos.pricelists, function (pricelist) { - return pricelist.id === rule.base_pricelist_id[0];}); + if (rule.base === "pricelist") { + const base_pricelist = _.find(self.pos.pricelists, function (pricelist) { + return pricelist.id === rule.base_pricelist_id[0]; + }); if (base_pricelist) { price = self.get_price(base_pricelist, quantity); } - } else if (rule.base === 'standard_price') { + } else if (rule.base === "standard_price") { price = self.standard_price; } - if (rule.compute_price === 'fixed') { + if (rule.compute_price === "fixed") { price = rule.fixed_price; return true; - } else if (rule.compute_price === 'percentage') { - price = price - (price * (rule.percent_price / 100)); + } else if (rule.compute_price === "percentage") { + price = price - price * (rule.percent_price / 100); return true; } else { var price_limit = price; - price = price - (price * (rule.price_discount / 100)); + price = price - price * (rule.price_discount / 100); if (rule.price_round) { price = round_pr(price, rule.price_round); } @@ -1434,8 +1568,6 @@ class Product extends PosModel { } return true; } - - return false; }); // This return value has to be rounded with round_di before @@ -1445,10 +1577,18 @@ class Product extends PosModel { return price; } get_display_price(pricelist, quantity) { - if (this.pos.config.iface_tax_included === 'total') { + if (this.pos.config.iface_tax_included === "total") { const order = this.pos.get_order(); - const taxes = this.pos.get_taxes_after_fp(this.taxes_id, order && order.fiscal_position); - const allPrices = this.pos.compute_all(taxes, this.get_price(pricelist, quantity), 1, this.pos.currency.rounding); + const taxes = this.pos.get_taxes_after_fp( + this.taxes_id, + order && order.fiscal_position + ); + const allPrices = this.pos.compute_all( + taxes, + this.get_price(pricelist, quantity), + 1, + this.pos.currency.rounding + ); return allPrices.total_included; } else { return this.get_price(pricelist, quantity); @@ -1462,18 +1602,21 @@ var orderline_id = 1; // An orderline represent one element of the content of a customer's shopping cart. // An orderline contains a product, its quantity, its price, discount. etc. // An Order contains zero or more Orderlines. -class Orderline extends PosModel { +export class Orderline extends PosModel { constructor(obj, options) { super(obj); - this.pos = options.pos; + this.pos = options.pos; this.order = options.order; this.price_manually_set = options.price_manually_set || false; if (options.json) { try { this.init_from_JSON(options.json); } catch { - console.error('ERROR: attempting to recover product ID', options.json.product_id, - 'not available in the point of sale. Correct the product or clean the browser cache.'); + console.error( + "ERROR: attempting to recover product ID", + options.json.product_id, + "not available in the point of sale. Correct the product or clean the browser cache." + ); } return; } @@ -1482,13 +1625,13 @@ class Orderline extends PosModel { this.set_product_lot(this.product); this.set_quantity(1); this.discount = 0; - this.discountStr = '0'; + this.discountStr = "0"; this.selected = false; - this.description = ''; + this.description = ""; this.price_extra = 0; - this.full_product_name = options.description || ''; + this.full_product_name = options.description || ""; this.id = orderline_id++; - this.customerNote = this.customerNote || ''; + this.customerNote = this.customerNote || ""; if (options.price) { this.set_unit_price(options.price); @@ -1502,16 +1645,19 @@ class Orderline extends PosModel { this.price = json.price_unit; this.price_manually_set = json.price_manually_set; this.set_discount(json.discount); - this.set_quantity(json.qty, 'do not recompute unit price'); + this.set_quantity(json.qty, "do not recompute unit price"); this.set_description(json.description); this.set_price_extra(json.price_extra); this.set_full_product_name(json.full_product_name); this.id = json.id ? json.id : orderline_id++; - orderline_id = Math.max(this.id+1,orderline_id); + orderline_id = Math.max(this.id + 1, orderline_id); 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({}, {'json': _.extend(packlotline, {'order_line':this})}); + var pack_lot_line = Packlotline.create( + {}, + { json: _.extend(packlotline, { order_line: this }) } + ); this.pack_lot_lines.add(pack_lot_line); } this.tax_ids = json.tax_ids && json.tax_ids.length !== 0 ? json.tax_ids[0][2] : undefined; @@ -1519,13 +1665,16 @@ class Orderline extends PosModel { this.refunded_qty = json.refunded_qty; this.refunded_orderline_id = json.refunded_orderline_id; } - clone(){ - var orderline = Orderline.create({},{ - pos: this.pos, - order: this.order, - product: this.product, - price: this.price, - }); + clone() { + var orderline = Orderline.create( + {}, + { + pos: this.pos, + order: this.order, + product: this.product, + price: this.price, + } + ); orderline.order = null; orderline.quantity = this.quantity; orderline.quantityStr = this.quantityStr; @@ -1542,13 +1691,13 @@ class Orderline extends PosModel { nExtraLines = Math.ceil(nExtraLines); nExtraLines = nExtraLines > 0 ? nExtraLines : 1; const tempLines = currentPackLotLines - .map(lotLine => ({ + .map((lotLine) => ({ id: lotLine.cid, text: lotLine.lot_name, })) .concat( - Array.from(Array(nExtraLines)).map(_ => ({ - text: '', + Array.from(Array(nExtraLines)).map((_) => ({ + text: "", })) ); return isAllowOnlyOneLot ? [tempLines[0]] : tempLines; @@ -1560,8 +1709,8 @@ class Orderline extends PosModel { */ setPackLotLines({ modifiedPackLotLines, newPackLotLines }) { // Set the new values for modified lot lines. - let lotLinesToRemove = []; - for (let lotLine of this.pack_lot_lines) { + const lotLinesToRemove = []; + for (const lotLine of this.pack_lot_lines) { const modifiedLotName = modifiedPackLotLines[lotLine.cid]; if (modifiedLotName) { lotLine.lot_name = modifiedLotName; @@ -1573,74 +1722,85 @@ class Orderline extends PosModel { } // Remove those that needed to be removed. - for (let lotLine of lotLinesToRemove) { + for (const lotLine of lotLinesToRemove) { this.pack_lot_lines.remove(lotLine); } // Create new pack lot lines. let newPackLotLine; - for (let newLotLine of newPackLotLines) { + for (const newLotLine of newPackLotLines) { newPackLotLine = Packlotline.create({}, { order_line: this }); newPackLotLine.lot_name = newLotLine.lot_name; this.pack_lot_lines.add(newPackLotLine); } // Set the quantity of the line based on number of pack lots. - if(!this.product.to_weight){ + if (!this.product.to_weight) { this.set_quantity_by_lot(); } } - set_product_lot(product){ - this.has_product_lot = product.tracking !== 'none'; - this.pack_lot_lines = this.has_product_lot && new PosCollection(); + set_product_lot(product) { + this.has_product_lot = product.tracking !== "none"; + this.pack_lot_lines = this.has_product_lot && new PosCollection(); } // sets a discount [0,100]% - set_discount(discount){ - var parsed_discount = typeof(discount) === 'number' ? discount : isNaN(parseFloat(discount)) ? 0 : field_utils.parse.float('' + discount); - var disc = Math.min(Math.max(parsed_discount || 0, 0),100); + set_discount(discount) { + var parsed_discount = + typeof discount === "number" + ? discount + : isNaN(parseFloat(discount)) + ? 0 + : field_utils.parse.float("" + discount); + var disc = Math.min(Math.max(parsed_discount || 0, 0), 100); this.discount = disc; - this.discountStr = '' + disc; + this.discountStr = "" + disc; } // returns the discount [0,100]% - get_discount(){ + get_discount() { return this.discount; } - get_discount_str(){ + get_discount_str() { return this.discountStr; } - set_description(description){ - this.description = description || ''; + set_description(description) { + this.description = description || ""; } - set_price_extra(price_extra){ + set_price_extra(price_extra) { this.price_extra = parseFloat(price_extra) || 0.0; } - set_full_product_name(full_product_name){ - this.full_product_name = full_product_name || ''; + set_full_product_name(full_product_name) { + this.full_product_name = full_product_name || ""; } - get_price_extra () { + get_price_extra() { return this.price_extra; } // sets the quantity of the product. The quantity will be rounded according to the // product's unity of measure properties. Quantities greater than zero will not get // rounded to zero // Return true if successfully set the quantity, otherwise, return false. - set_quantity(quantity, keep_price){ + set_quantity(quantity, keep_price) { this.order.assert_editable(); - if(quantity === 'remove'){ + if (quantity === "remove") { if (this.refunded_orderline_id in this.pos.toRefundLines) { delete this.pos.toRefundLines[this.refunded_orderline_id]; } this.order.remove_orderline(this); return true; - }else{ - var quant = typeof(quantity) === 'number' ? quantity : (field_utils.parse.float('' + (quantity ? quantity : 0 ))); + } else { + var quant = + typeof quantity === "number" + ? quantity + : field_utils.parse.float("" + (quantity ? quantity : 0)); if (this.refunded_orderline_id in this.pos.toRefundLines) { const toRefundDetail = this.pos.toRefundLines[this.refunded_orderline_id]; - const maxQtyToRefund = toRefundDetail.orderline.qty - toRefundDetail.orderline.refundedQty + const maxQtyToRefund = + toRefundDetail.orderline.qty - toRefundDetail.orderline.refundedQty; if (quant > 0) { - 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.') + 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." + ), }); return false; } else if (quant == 0) { @@ -1648,10 +1808,12 @@ class Orderline extends PosModel { } else if (-quant <= maxQtyToRefund) { toRefundDetail.qty = -quant; } else { - Gui.showPopup('ErrorPopup', { - title: _t('Greater than allowed'), + Gui.showPopup("ErrorPopup", { + title: _t("Greater than allowed"), body: _.str.sprintf( - _t('The requested quantity to be refunded is higher than the refundable quantity of %s.'), + _t( + "The requested quantity to be refunded is higher than the refundable quantity of %s." + ), this.pos.formatProductQty(maxQtyToRefund) ), }); @@ -1659,41 +1821,49 @@ class Orderline extends PosModel { } } var unit = this.get_unit(); - if(unit){ + if (unit) { if (unit.rounding) { - var decimals = this.pos.dp['Product Unit of Measure']; + var decimals = this.pos.dp["Product Unit of Measure"]; var rounding = Math.max(unit.rounding, Math.pow(10, -decimals)); - this.quantity = round_pr(quant, rounding); - this.quantityStr = field_utils.format.float(this.quantity, {digits: [69, decimals]}); + this.quantity = round_pr(quant, rounding); + this.quantityStr = field_utils.format.float(this.quantity, { + digits: [69, decimals], + }); } else { - this.quantity = round_pr(quant, 1); + this.quantity = round_pr(quant, 1); this.quantityStr = this.quantity.toFixed(0); } - }else{ - this.quantity = quant; - this.quantityStr = '' + this.quantity; + } else { + this.quantity = quant; + this.quantityStr = "" + this.quantity; } } // just like in sale.order changing the quantity will recompute the unit price - if(! keep_price && ! this.price_manually_set){ - this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity(), this.get_price_extra())); + if (!keep_price && !this.price_manually_set) { + this.set_unit_price( + this.product.get_price( + this.order.pricelist, + this.get_quantity(), + this.get_price_extra() + ) + ); this.order.fix_tax_included_price(this); } return true; } // return the quantity of product - get_quantity(){ + get_quantity() { return this.quantity; } - get_quantity_str(){ + get_quantity_str() { return this.quantityStr; } - get_quantity_str_with_unit(){ + get_quantity_str_with_unit() { var unit = this.get_unit(); - if(unit && !unit.is_pos_groupable){ - return this.quantityStr + ' ' + unit.name; - }else{ + if (unit && !unit.is_pos_groupable) { + return this.quantityStr + " " + unit.name; + } else { return this.quantityStr; } } @@ -1702,17 +1872,17 @@ class Orderline extends PosModel { return this.pack_lot_lines && this.pack_lot_lines; } - get_required_number_of_lots(){ + get_required_number_of_lots() { var lots_required = 1; - if (this.product.tracking == 'serial') { + if (this.product.tracking == "serial") { lots_required = Math.abs(this.quantity); } return lots_required; } - get_valid_lots(){ + get_valid_lots() { return this.pack_lot_lines.filter((item) => { return item.lot_name; }); @@ -1720,14 +1890,14 @@ class Orderline extends PosModel { set_quantity_by_lot() { var valid_lots_quantity = this.get_valid_lots().length; - if (this.quantity < 0){ + if (this.quantity < 0) { valid_lots_quantity = -valid_lots_quantity; } this.set_quantity(valid_lots_quantity); } - has_valid_product_lot(){ - if(!this.has_product_lot){ + has_valid_product_lot() { + if (!this.has_product_lot) { return true; } var valid_product_lot = this.get_valid_lots(); @@ -1735,16 +1905,16 @@ class Orderline extends PosModel { } // return the unit of measure of the product - get_unit(){ + get_unit() { return this.product.get_unit(); } // return the product of this orderline - get_product(){ + get_product() { return this.product; } - get_full_product_name () { + get_full_product_name() { if (this.full_product_name) { - return this.full_product_name + return this.full_product_name; } var full_name = this.product.display_name; if (this.description) { @@ -1753,49 +1923,67 @@ class Orderline extends PosModel { return full_name; } // selects or deselects this orderline - set_selected(selected){ + set_selected(selected) { this.selected = selected; // this trigger also triggers the change event of the collection. } // returns true if this orderline is selected - is_selected(){ + is_selected() { return this.selected; } // when we add an new orderline we want to merge it with the last line to see reduce the number of items // in the orderline. This returns true if it makes sense to merge the two - can_be_merged_with(orderline){ - var price = parseFloat(round_di(this.price || 0, this.pos.dp['Product Price']).toFixed(this.pos.dp['Product Price'])); - var order_line_price = orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity()); - order_line_price = round_di(orderline.compute_fixed_price(order_line_price), this.pos.currency.decimal_places); - if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged + can_be_merged_with(orderline) { + var price = parseFloat( + round_di(this.price || 0, this.pos.dp["Product Price"]).toFixed( + this.pos.dp["Product Price"] + ) + ); + var order_line_price = orderline + .get_product() + .get_price(orderline.order.pricelist, this.get_quantity()); + order_line_price = round_di( + orderline.compute_fixed_price(order_line_price), + this.pos.currency.decimal_places + ); + if (this.get_product().id !== orderline.get_product().id) { + //only orderline of the same product can be merged return false; - }else if(!this.get_unit() || !this.get_unit().is_pos_groupable){ + } else if (!this.get_unit() || !this.get_unit().is_pos_groupable) { return false; - }else if(this.get_discount() > 0){ // we don't merge discounted orderlines + } else if (this.get_discount() > 0) { + // we don't merge discounted orderlines return false; - }else if(!utils.float_is_zero(price - order_line_price - orderline.get_price_extra(), - this.pos.currency.decimal_places)){ + } else if ( + !utils.float_is_zero( + price - order_line_price - orderline.get_price_extra(), + this.pos.currency.decimal_places + ) + ) { return false; - }else if(this.product.tracking == 'lot' && (this.pos.picking_type.use_create_lots || this.pos.picking_type.use_existing_lots)) { + } else if ( + this.product.tracking == "lot" && + (this.pos.picking_type.use_create_lots || this.pos.picking_type.use_existing_lots) + ) { return false; - }else if (this.description !== orderline.description) { + } else if (this.description !== orderline.description) { return false; - }else if (orderline.get_customer_note() !== this.get_customer_note()) { + } else if (orderline.get_customer_note() !== this.get_customer_note()) { return false; } else if (this.refunded_orderline_id) { return false; - }else{ + } else { return true; } } - merge(orderline){ + merge(orderline) { this.order.assert_editable(); this.set_quantity(this.get_quantity() + orderline.get_quantity()); } export_as_JSON() { var pack_lot_ids = []; - if (this.has_product_lot){ - this.pack_lot_lines.forEach(item => { + if (this.has_product_lot) { + this.pack_lot_lines.forEach((item) => { return pack_lot_ids.push([0, 0, item.export_as_JSON()]); }); } @@ -1806,7 +1994,15 @@ class Orderline extends PosModel { price_subtotal_incl: this.get_price_with_tax(), discount: this.get_discount(), product_id: this.get_product().id, - tax_ids: [[6, false, _.map(this.get_applicable_taxes(), function(tax){ return tax.id; })]], + tax_ids: [ + [ + 6, + false, + _.map(this.get_applicable_taxes(), function (tax) { + return tax.id; + }), + ], + ], id: this.id, pack_lot_ids: pack_lot_ids, description: this.description, @@ -1814,34 +2010,34 @@ class Orderline extends PosModel { price_extra: this.get_price_extra(), customer_note: this.get_customer_note(), refunded_orderline_id: this.refunded_orderline_id, - price_manually_set: this.price_manually_set + price_manually_set: this.price_manually_set, }; } //used to create a json of the ticket, to be sent to the printer - export_for_printing(){ + export_for_printing() { return { id: this.id, - quantity: this.get_quantity(), - unit_name: this.get_unit().name, - is_in_unit: this.get_unit().id == this.pos.uom_unit_id, - price: this.get_unit_display_price(), - discount: this.get_discount(), - product_name: this.get_product().display_name, + quantity: this.get_quantity(), + unit_name: this.get_unit().name, + is_in_unit: this.get_unit().id == this.pos.uom_unit_id, + price: this.get_unit_display_price(), + discount: this.get_discount(), + product_name: this.get_product().display_name, product_name_wrapped: this.generate_wrapped_product_name(), - price_lst: this.get_lst_price(), - fixed_lst_price: this.get_fixed_lst_price(), + price_lst: this.get_lst_price(), + fixed_lst_price: this.get_fixed_lst_price(), price_manually_set: this.price_manually_set, - display_discount_policy: this.display_discount_policy(), - price_display_one: this.get_display_price_one(), - price_display : this.get_display_price(), - price_with_tax : this.get_price_with_tax(), - price_without_tax: this.get_price_without_tax(), - price_with_tax_before_discount: this.get_price_with_tax_before_discount(), - tax: this.get_tax(), - product_description: this.get_product().description, + display_discount_policy: this.display_discount_policy(), + price_display_one: this.get_display_price_one(), + price_display: this.get_display_price(), + price_with_tax: this.get_price_with_tax(), + price_without_tax: this.get_price_without_tax(), + price_with_tax_before_discount: this.get_price_with_tax_before_discount(), + tax: this.get_tax(), + product_description: this.get_product().description, product_description_sale: this.get_product().description_sale, - pack_lot_lines: this.get_lot_lines(), - customer_note: this.get_customer_note(), + pack_lot_lines: this.get_lot_lines(), + customer_note: this.get_customer_note(), }; } generate_wrapped_product_name() { @@ -1875,73 +2071,84 @@ class Orderline extends PosModel { return wrapped; } // changes the base price of the product for this orderline - set_unit_price(price){ + set_unit_price(price) { this.order.assert_editable(); - var parsed_price = !isNaN(price) ? - price : - isNaN(parseFloat(price)) ? 0 : field_utils.parse.float('' + price) - this.price = round_di(parsed_price || 0, this.pos.dp['Product Price']); + var parsed_price = !isNaN(price) + ? price + : isNaN(parseFloat(price)) + ? 0 + : field_utils.parse.float("" + price); + this.price = round_di(parsed_price || 0, this.pos.dp["Product Price"]); } - get_unit_price(){ - var digits = this.pos.dp['Product Price']; + get_unit_price() { + var digits = this.pos.dp["Product Price"]; // round and truncate to mimic _symbol_set behavior return parseFloat(round_di(this.price || 0, digits).toFixed(digits)); } - get_unit_display_price(){ - if (this.pos.config.iface_tax_included === 'total') { + get_unit_display_price() { + if (this.pos.config.iface_tax_included === "total") { return this.get_all_prices(1).priceWithTax; } else { return this.get_unit_price(); } } - get_base_price(){ + get_base_price() { var rounding = this.pos.currency.rounding; - return round_pr(this.get_unit_price() * this.get_quantity() * (1 - this.get_discount()/100), rounding); + return round_pr( + this.get_unit_price() * this.get_quantity() * (1 - this.get_discount() / 100), + rounding + ); } - get_display_price_one(){ + get_display_price_one() { var rounding = this.pos.currency.rounding; var price_unit = this.get_unit_price(); - if (this.pos.config.iface_tax_included !== 'total') { - return round_pr(price_unit * (1.0 - (this.get_discount() / 100.0)), rounding); + if (this.pos.config.iface_tax_included !== "total") { + return round_pr(price_unit * (1.0 - this.get_discount() / 100.0), rounding); } else { - var product = this.get_product(); + var product = this.get_product(); var taxes_ids = this.tax_ids || product.taxes_id; var product_taxes = this.pos.get_taxes_after_fp(taxes_ids, this.order.fiscal_position); - var all_taxes = this.compute_all(product_taxes, price_unit, 1, this.pos.currency.rounding); + var all_taxes = this.compute_all( + product_taxes, + price_unit, + 1, + this.pos.currency.rounding + ); - return round_pr(all_taxes.total_included * (1 - this.get_discount()/100), rounding); + return round_pr(all_taxes.total_included * (1 - this.get_discount() / 100), rounding); } } - get_display_price(){ - if (this.pos.config.iface_tax_included === 'total') { + get_display_price() { + if (this.pos.config.iface_tax_included === "total") { return this.get_price_with_tax(); } else { return this.get_base_price(); } } - get_taxed_lst_unit_price(){ + get_taxed_lst_unit_price() { var lst_price = this.get_lst_price(); - if (this.pos.config.iface_tax_included === 'total') { - var product = this.get_product(); + if (this.pos.config.iface_tax_included === "total") { + var product = this.get_product(); var taxes_ids = product.taxes_id; var product_taxes = this.pos.get_taxes_after_fp(taxes_ids); - return this.compute_all(product_taxes, lst_price, 1, this.pos.currency.rounding).total_included; + return this.compute_all(product_taxes, lst_price, 1, this.pos.currency.rounding) + .total_included; } return lst_price; } - get_price_without_tax(){ + get_price_without_tax() { return this.get_all_prices().priceWithoutTax; } - get_price_with_tax(){ + get_price_with_tax() { return this.get_all_prices().priceWithTax; } - get_price_with_tax_before_discount () { + get_price_with_tax_before_discount() { return this.get_all_prices().priceWithTaxBeforeDiscount; } - get_tax(){ + get_tax() { return this.get_all_prices().tax; } - get_applicable_taxes(){ + get_applicable_taxes() { var i; // Shenaningans because we need // to keep the taxes ordering. @@ -1958,10 +2165,10 @@ class Orderline extends PosModel { } return taxes; } - get_tax_details(){ + get_tax_details() { return this.get_all_prices().taxDetails; } - get_taxes(){ + get_taxes() { var taxes_ids = this.tax_ids || this.get_product().taxes_id; var taxes = []; for (var i = 0; i < taxes_ids.length; i++) { @@ -1977,10 +2184,8 @@ class Orderline extends PosModel { */ get_total_taxes_included_in_price() { return this.get_taxes() - .filter(tax => tax.price_include) - .reduce((sum, tax) => sum + this.get_tax_details()[tax.id], - 0 - ); + .filter((tax) => tax.price_include) + .reduce((sum, tax) => sum + this.get_tax_details()[tax.id], 0); } _map_tax_fiscal_position(tax, order = false) { return this.pos._map_tax_fiscal_position(tax, order); @@ -1998,82 +2203,115 @@ class Orderline extends PosModel { * * Read comments in the python side method for more details about each sub-methods. */ - compute_all(taxes, price_unit, quantity, currency_rounding, handle_price_include=true) { - return this.pos.compute_all(taxes, price_unit, quantity, currency_rounding, handle_price_include); + compute_all(taxes, price_unit, quantity, currency_rounding, handle_price_include = true) { + return this.pos.compute_all( + taxes, + price_unit, + quantity, + currency_rounding, + handle_price_include + ); } - get_all_prices(qty = this.get_quantity()){ - var price_unit = this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)); + get_all_prices(qty = this.get_quantity()) { + var price_unit = this.get_unit_price() * (1.0 - this.get_discount() / 100.0); var taxtotal = 0; - var product = this.get_product(); + var product = this.get_product(); var taxes_ids = this.tax_ids || product.taxes_id; - taxes_ids = _.filter(taxes_ids, t => t in this.pos.taxes_by_id); + taxes_ids = _.filter(taxes_ids, (t) => t in this.pos.taxes_by_id); var taxdetail = {}; var product_taxes = this.pos.get_taxes_after_fp(taxes_ids, this.order.fiscal_position); - var all_taxes = this.compute_all(product_taxes, price_unit, qty, this.pos.currency.rounding); - var all_taxes_before_discount = this.compute_all(product_taxes, this.get_unit_price(), qty, this.pos.currency.rounding); - _(all_taxes.taxes).each(function(tax) { + var all_taxes = this.compute_all( + product_taxes, + price_unit, + qty, + this.pos.currency.rounding + ); + var all_taxes_before_discount = this.compute_all( + product_taxes, + this.get_unit_price(), + qty, + this.pos.currency.rounding + ); + _(all_taxes.taxes).each(function (tax) { taxtotal += tax.amount; taxdetail[tax.id] = tax.amount; }); return { - "priceWithTax": all_taxes.total_included, - "priceWithoutTax": all_taxes.total_excluded, - "priceSumTaxVoid": all_taxes.total_void, - "priceWithTaxBeforeDiscount": all_taxes_before_discount.total_included, - "tax": taxtotal, - "taxDetails": taxdetail, + priceWithTax: all_taxes.total_included, + priceWithoutTax: all_taxes.total_excluded, + priceSumTaxVoid: all_taxes.total_void, + priceWithTaxBeforeDiscount: all_taxes_before_discount.total_included, + tax: taxtotal, + taxDetails: taxdetail, }; } - display_discount_policy(){ + display_discount_policy() { return this.order.pricelist.discount_policy; } - compute_fixed_price (price) { + compute_fixed_price(price) { var order = this.order; - if(order.fiscal_position) { + if (order.fiscal_position) { var taxes = this.get_taxes(); var mapped_included_taxes = []; var new_included_taxes = []; var self = this; - _(taxes).each(function(tax) { + _(taxes).each(function (tax) { var line_taxes = self.pos.get_taxes_after_fp([tax.id], order.fiscal_position); - if (line_taxes.length && line_taxes[0].price_include){ + if (line_taxes.length && line_taxes[0].price_include) { new_included_taxes = new_included_taxes.concat(line_taxes); } - if(tax.price_include && !_.contains(line_taxes, tax)){ + if (tax.price_include && !_.contains(line_taxes, tax)) { mapped_included_taxes.push(tax); } }); if (mapped_included_taxes.length > 0) { if (new_included_taxes.length > 0) { - var price_without_taxes = this.compute_all(mapped_included_taxes, price, 1, order.pos.currency.rounding, true).total_excluded - return this.compute_all(new_included_taxes, price_without_taxes, 1, order.pos.currency.rounding, false).total_included - } - else{ - return this.compute_all(mapped_included_taxes, price, 1, order.pos.currency.rounding, true).total_excluded; + var price_without_taxes = this.compute_all( + mapped_included_taxes, + price, + 1, + order.pos.currency.rounding, + true + ).total_excluded; + return this.compute_all( + new_included_taxes, + price_without_taxes, + 1, + order.pos.currency.rounding, + false + ).total_included; + } else { + return this.compute_all( + mapped_included_taxes, + price, + 1, + order.pos.currency.rounding, + true + ).total_excluded; } } } return price; } - get_fixed_lst_price(){ + get_fixed_lst_price() { return this.compute_fixed_price(this.get_lst_price()); } - get_lst_price(){ + get_lst_price() { return this.product.lst_price; } - set_lst_price(price){ - this.order.assert_editable(); - this.product.lst_price = round_di(parseFloat(price) || 0, this.pos.dp['Product Price']); + set_lst_price(price) { + this.order.assert_editable(); + this.product.lst_price = round_di(parseFloat(price) || 0, this.pos.dp["Product Price"]); } is_last_line() { var order = this.pos.get_order(); var orderlines = order.orderlines; var last_id = orderlines[orderlines.length - 1].cid; - var selectedLine = order? order.selected_orderline: null; + var selectedLine = order ? order.selected_orderline : null; return !selectedLine ? false : last_id === selectedLine.cid; } @@ -2089,8 +2327,8 @@ class Orderline extends PosModel { } Registries.Model.add(Orderline); -class Packlotline extends PosModel { - constructor(obj, options){ +export class Packlotline extends PosModel { + constructor(obj, options) { super(obj); this.lot_name = null; this.order_line = options.order_line; @@ -2105,15 +2343,15 @@ class Packlotline extends PosModel { this.set_lot_name(json.lot_name); } - set_lot_name(name){ + set_lot_name(name) { this.lot_name = _.str.trim(name) || null; } - get_lot_name(){ + get_lot_name() { return this.lot_name; } - export_as_JSON(){ + export_as_JSON() { return { lot_name: this.get_lot_name(), }; @@ -2122,19 +2360,19 @@ class Packlotline extends PosModel { Registries.Model.add(Packlotline); // Every Paymentline contains a cashregister and an amount of money. -class Payment extends PosModel { +export class Payment extends PosModel { constructor(obj, options) { super(obj); this.pos = options.pos; this.order = options.order; this.amount = 0; this.selected = false; - this.cashier_receipt = ''; - this.ticket = ''; - this.payment_status = ''; - this.card_type = ''; - this.cardholder_name = ''; - this.transaction_id = ''; + this.cashier_receipt = ""; + this.ticket = ""; + this.payment_status = ""; + this.card_type = ""; + this.cardholder_name = ""; + this.transaction_id = ""; if (options.json) { this.init_from_JSON(options.json); @@ -2142,11 +2380,11 @@ class Payment extends PosModel { } this.payment_method = options.payment_method; if (this.payment_method === undefined) { - throw new Error(_t('Please configure a payment method in your POS.')); + throw new Error(_t("Please configure a payment method in your POS.")); } this.name = this.payment_method.name; } - init_from_JSON(json){ + init_from_JSON(json) { this.amount = json.amount; this.payment_method = this.pos.payment_methods_by_id[json.payment_method_id]; this.can_be_reversed = json.can_be_reversed; @@ -2159,19 +2397,21 @@ class Payment extends PosModel { this.is_change = json.is_change; } //sets the amount of money on this payment line - set_amount(value){ + set_amount(value) { this.order.assert_editable(); this.amount = round_di(parseFloat(value) || 0, this.pos.currency.decimal_places); } // returns the amount of money on this paymentline - get_amount(){ + get_amount() { return this.amount; } - get_amount_str(){ - return field_utils.format.float(this.amount, {digits: [69, this.pos.currency.decimal_places]}); + get_amount_str() { + return field_utils.format.float(this.amount, { + digits: [69, this.pos.currency.decimal_places], + }); } - set_selected(selected){ - if(this.selected !== selected){ + set_selected(selected) { + if (this.selected !== selected) { this.selected = selected; } } @@ -2196,16 +2436,18 @@ class Payment extends PosModel { * Paymentline is done if there is no payment status or the payment status is done. */ is_done() { - return this.get_payment_status() ? this.get_payment_status() === 'done' || this.get_payment_status() === 'reversed': true; + return this.get_payment_status() + ? this.get_payment_status() === "done" || this.get_payment_status() === "reversed" + : true; } /** - * Set info to be printed on the cashier receipt. value should - * be compatible with both the QWeb and ESC/POS receipts. - * - * @param {string} value - receipt info - */ - set_cashier_receipt (value) { + * Set info to be printed on the cashier receipt. value should + * be compatible with both the QWeb and ESC/POS receipts. + * + * @param {string} value - receipt info + */ + set_cashier_receipt(value) { this.cashier_receipt = value; } @@ -2221,7 +2463,7 @@ class Payment extends PosModel { // returns the associated cashregister //exports as JSON for server communication - export_as_JSON(){ + export_as_JSON() { return { name: time.datetime_to_str(new Date()), payment_method_id: this.payment_method.id, @@ -2235,7 +2477,7 @@ class Payment extends PosModel { }; } //exports as JSON for receipt printing - export_for_printing(){ + export_for_printing() { return { cid: this.cid, amount: this.get_amount(), @@ -2256,57 +2498,57 @@ Registries.Model.add(Payment); // 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. -class Order extends PosModel { +export class Order extends PosModel { constructor(obj, options) { super(obj); var self = this; - options = options || {}; + options = options || {}; - this.locked = false; - this.pos = options.pos; - this.selected_orderline = undefined; + this.locked = false; + this.pos = options.pos; + this.selected_orderline = undefined; this.selected_paymentline = undefined; - this.screen_data = {}; // see Gui - this.temporary = options.temporary || false; - this.creation_date = new Date(); - this.to_invoice = false; - this.orderlines = new PosCollection(); - this.paymentlines = new PosCollection(); + this.screen_data = {}; // see Gui + this.temporary = options.temporary || false; + this.creation_date = new Date(); + this.to_invoice = false; + this.orderlines = new PosCollection(); + this.paymentlines = new PosCollection(); this.pos_session_id = this.pos.pos_session.id; - this.cashier = this.pos.get_cashier(); - this.finalized = false; // if true, cannot be modified. + this.cashier = this.pos.get_cashier(); + this.finalized = false; // if true, cannot be modified. this.set_pricelist(this.pos.default_pricelist); this.partner = null; this.uiState = { ReceiptScreen: { - inputEmail: '', + inputEmail: "", // if null: not yet tried to send // if false/true: tried sending email emailSuccessful: null, - emailNotice: '', + emailNotice: "", }, // TODO: This should be in pos_restaurant. TipScreen: { - inputTipAmount: '', - } + inputTipAmount: "", + }, }; if (options.json) { this.init_from_JSON(options.json); } else { this.sequence_number = this.pos.pos_session.sequence_number++; - this.access_token = uuidv4(); // unique uuid used to identify the authenticity of the request from the QR code. - this.uid = this.generate_unique_id(); + this.access_token = uuidv4(); // unique uuid used to identify the authenticity of the request from the QR code. + this.uid = this.generate_unique_id(); this.name = _.str.sprintf(_t("Order %s"), this.uid); this.validation_date = undefined; - this.fiscal_position = _.find(this.pos.fiscal_positions, function(fp) { + this.fiscal_position = _.find(this.pos.fiscal_positions, function (fp) { return fp.id === self.pos.config.default_fiscal_position_id[0]; }); } } - save_to_db(){ + save_to_db() { if (!this.temporary && !this.locked && !this.finalized) { this.assert_editable(); this.pos.db.save_unpaid_order(this); @@ -2323,13 +2565,16 @@ class Order extends PosModel { */ init_from_JSON(json) { let partner; - if (json.state && ['done', 'invoiced', 'paid'].includes(json.state)) { + if (json.state && ["done", "invoiced", "paid"].includes(json.state)) { this.sequence_number = json.sequence_number; } else if (json.pos_session_id !== this.pos.pos_session.id) { this.sequence_number = this.pos.pos_session.sequence_number++; } else { this.sequence_number = json.sequence_number; - this.pos.pos_session.sequence_number = Math.max(this.sequence_number+1,this.pos.pos_session.sequence_number); + this.pos.pos_session.sequence_number = Math.max( + this.sequence_number + 1, + this.pos.pos_session.sequence_number + ); } this.session_id = this.pos.pos_session.id; this.uid = json.uid; @@ -2346,7 +2591,7 @@ class Order extends PosModel { if (fiscal_position) { this.fiscal_position = fiscal_position; } else { - console.error('ERROR: trying to load a fiscal position not available in the pos'); + console.error("ERROR: trying to load a fiscal position not available in the pos"); } } @@ -2361,29 +2606,34 @@ class Order extends PosModel { if (json.partner_id) { partner = this.pos.db.get_partner_by_id(json.partner_id); if (!partner) { - console.error('ERROR: trying to load a partner not available in the pos'); + console.error("ERROR: trying to load a partner not available in the pos"); } } else { partner = null; } this.set_partner(partner); - this.temporary = false; // FIXME - this.to_invoice = false; // FIXME + this.temporary = false; // FIXME + this.to_invoice = false; // FIXME this.to_ship = false; var orderlines = json.lines; for (var i = 0; i < orderlines.length; i++) { 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 })); + this.add_orderline( + Orderline.create({}, { pos: this.pos, order: this, json: orderline }) + ); } } var paymentlines = json.statement_ids; - for (var i = 0; i < paymentlines.length; i++) { + for (i = 0; i < paymentlines.length; i++) { var paymentline = paymentlines[i][2]; - var newpaymentline = Payment.create({},{pos: this.pos, order: this, json: paymentline}); + var newpaymentline = Payment.create( + {}, + { pos: this.pos, order: this, json: paymentline } + ); this.paymentlines.add(newpaymentline); if (i === paymentlines.length - 1) { @@ -2392,25 +2642,27 @@ class Order extends PosModel { } // Tag this order as 'locked' if it is already paid. - this.locked = ['paid', 'done', 'invoiced'].includes(json.state); + this.locked = ["paid", "done", "invoiced"].includes(json.state); this.state = json.state; this.amount_return = json.amount_return; this.account_move = json.account_move; this.backendId = json.id; this.is_tipped = json.is_tipped || false; this.tip_amount = json.tip_amount || 0; - this.access_token = json.access_token || ''; + this.access_token = json.access_token || ""; } export_as_JSON() { var orderLines, paymentLines; orderLines = []; - this.orderlines.forEach(item => { + this.orderlines.forEach((item) => { return orderLines.push([0, 0, item.export_as_JSON()]); }); paymentLines = []; - this.paymentlines.forEach(_.bind( function(item) { - return paymentLines.push([0, 0, item.export_as_JSON()]); - }, this)); + this.paymentlines.forEach( + _.bind(function (item) { + return paymentLines.push([0, 0, item.export_as_JSON()]); + }, this) + ); var json = { name: this.get_name(), amount_paid: this.get_total_paid() - this.get_change(), @@ -2432,18 +2684,18 @@ class Order extends PosModel { to_ship: this.to_ship ? this.to_ship : false, is_tipped: this.is_tipped || false, tip_amount: this.tip_amount || 0, - access_token: this.access_token || '', + access_token: this.access_token || "", }; if (!this.is_paid && this.user_id) { json.user_id = this.user_id; } return json; } - export_for_printing(){ + export_for_printing() { var orderlines = []; var self = this; - this.orderlines.forEach(function(orderline){ + this.orderlines.forEach(function (orderline) { orderlines.push(orderline.export_for_printing()); }); @@ -2457,26 +2709,28 @@ class Order extends PosModel { .map(function (paymentline) { return paymentline.export_for_printing(); }); - let partner = this.partner; - let cashier = this.pos.get_cashier(); - let company = this.pos.company; - let date = new Date(); + const partner = this.partner; + const cashier = this.pos.get_cashier(); + const company = this.pos.company; + const date = new Date(); - function is_html(subreceipt){ - return subreceipt ? (subreceipt.split('\n')[0].indexOf('= 0) : false; + function is_html(subreceipt) { + return subreceipt ? subreceipt.split("\n")[0].indexOf("= 0 : false; } - function render_html(subreceipt){ + function render_html(subreceipt) { if (!is_html(subreceipt)) { return subreceipt; } else { - subreceipt = subreceipt.split('\n').slice(1).join('\n'); + subreceipt = subreceipt.split("\n").slice(1).join("\n"); var qweb = new QWeb2.Engine(); - qweb.debug = config.isDebug(); - qweb.default_dict = _.clone(QWeb.default_dict); - qweb.add_template(''+subreceipt+''); + qweb.debug = config.isDebug(); + qweb.default_dict = _.clone(QWeb.default_dict); + qweb.add_template( + '' + subreceipt + "" + ); - return qweb.render('subreceipt',{'pos':self.pos,'order':self, 'receipt': receipt}) ; + return qweb.render("subreceipt", { pos: self.pos, order: self, receipt: receipt }); } } @@ -2493,9 +2747,9 @@ class Order extends PosModel { rounding_applied: this.get_rounding_applied(), tax_details: this.get_tax_details(), change: this.locked ? this.amount_return : this.get_change(), - name : this.get_name(), - partner: partner ? partner : null , - invoice_id: null, //TODO + name: this.get_name(), + partner: partner ? partner : null, + invoice_id: null, //TODO cashier: cashier ? cashier.name : null, precision: { price: 2, @@ -2505,49 +2759,53 @@ class Order extends PosModel { date: { year: date.getFullYear(), month: date.getMonth(), - date: date.getDate(), // day of the month - day: date.getDay(), // day of the week + date: date.getDate(), // day of the month + day: date.getDay(), // day of the week hour: date.getHours(), - minute: date.getMinutes() , + minute: date.getMinutes(), isostring: date.toISOString(), localestring: this.formatted_validation_date, validation_date: this.validation_date, }, - company:{ + company: { email: company.email, website: company.website, company_registry: company.company_registry, contact_address: company.partner_id[1], vat: company.vat, - vat_label: company.country && company.country.vat_label || _t('Tax ID'), + vat_label: (company.country && company.country.vat_label) || _t("Tax ID"), name: company.name, phone: company.phone, - logo: this.pos.company_logo_base64, + logo: this.pos.company_logo_base64, }, currency: this.pos.currency, pos_qr_code: this._get_qr_code_data(), }; - if (is_html(this.pos.config.receipt_header)){ - receipt.header = ''; + if (is_html(this.pos.config.receipt_header)) { + receipt.header = ""; receipt.header_html = render_html(this.pos.config.receipt_header); } else { - receipt.header = this.pos.config.receipt_header || ''; + receipt.header = this.pos.config.receipt_header || ""; } - if (is_html(this.pos.config.receipt_footer)){ - receipt.footer = ''; + if (is_html(this.pos.config.receipt_footer)) { + receipt.footer = ""; receipt.footer_html = render_html(this.pos.config.receipt_footer); } else { - receipt.footer = this.pos.config.receipt_footer || ''; + receipt.footer = this.pos.config.receipt_footer || ""; } - if (!receipt.date.localestring && (!this.state || this.state == 'draft')){ - receipt.date.localestring = field_utils.format.datetime(moment(new Date()), {}, {timezone: false}); + if (!receipt.date.localestring && (!this.state || this.state == "draft")) { + receipt.date.localestring = field_utils.format.datetime( + moment(new Date()), + {}, + { timezone: false } + ); } return receipt; } - is_empty(){ + is_empty() { return this.orderlines.length === 0; } generate_unique_id() { @@ -2555,45 +2813,49 @@ class Order extends PosModel { // The generated number must be unique and sequential. They are made 12 digit long // to fit into EAN-13 barcodes, should it be needed - function zero_pad(num,size){ - var s = ""+num; + function zero_pad(num, size) { + var s = "" + num; while (s.length < size) { s = "0" + s; } return s; } - return zero_pad(this.pos.pos_session.id,5) +'-'+ - zero_pad(this.pos.pos_session.login_number,3) +'-'+ - zero_pad(this.sequence_number,4); + return ( + zero_pad(this.pos.pos_session.id, 5) + + "-" + + zero_pad(this.pos.pos_session.login_number, 3) + + "-" + + zero_pad(this.sequence_number, 4) + ); } get_name() { return this.name; } assert_editable() { if (this.finalized) { - throw new Error('Finalized Order cannot be modified'); + throw new Error("Finalized Order cannot be modified"); } } /* ---- Order Lines --- */ - add_orderline(line){ + add_orderline(line) { this.assert_editable(); - if(line.order){ + if (line.order) { line.order.remove_orderline(line); } line.order = this; this.orderlines.add(line); this.select_orderline(this.get_last_orderline()); } - get_orderline(id){ + get_orderline(id) { var orderlines = this.orderlines; - for(var i = 0; i < orderlines.length; i++){ - if(orderlines[i].id === id){ + for (var i = 0; i < orderlines.length; i++) { + if (orderlines[i].id === id) { return orderlines[i]; } } return null; } - get_orderlines(){ + get_orderlines() { return this.orderlines; } /** @@ -2608,9 +2870,9 @@ class Order extends PosModel { * } */ get_orderlines_grouped_by_tax_ids() { - let orderlines_by_tax_group = {}; + const orderlines_by_tax_group = {}; const lines = this.get_orderlines(); - for (let line of lines) { + for (const line of lines) { const tax_group = this._get_tax_group_key(line); if (!(tax_group in orderlines_by_tax_group)) { orderlines_by_tax_group[tax_group] = []; @@ -2622,8 +2884,8 @@ class Order extends PosModel { _get_tax_group_key(line) { return line .get_taxes() - .map(tax => tax.id) - .join(','); + .map((tax) => tax.id) + .join(","); } /** * Calculate the amount that will be used as a base in order to apply a downpayment or discount product in PoS. @@ -2636,11 +2898,12 @@ class Order extends PosModel { */ calculate_base_amount(tax_ids_array, lines) { // Consider price_include taxes use case - let has_taxes_included_in_price = tax_ids_array.filter(tax_id => - this.pos.taxes_by_id[tax_id].price_include + const has_taxes_included_in_price = tax_ids_array.filter( + (tax_id) => this.pos.taxes_by_id[tax_id].price_include ).length; - let base_amount = lines.reduce((sum, line) => + const base_amount = lines.reduce( + (sum, line) => sum + line.get_price_without_tax() + (has_taxes_included_in_price ? line.get_total_taxes_included_in_price() : 0), @@ -2648,9 +2911,9 @@ class Order extends PosModel { ); return base_amount; } - get_last_orderline(){ + get_last_orderline() { const orderlines = this.orderlines; - return this.orderlines.at(orderlines.length -1); + return this.orderlines.at(orderlines.length - 1); } get_tip() { var tip_product = this.pos.db.get_product_by_id(this.pos.config.tip_product_id[0]); @@ -2667,10 +2930,13 @@ class Order extends PosModel { } } - initialize_validation_date () { + initialize_validation_date() { this.validation_date = new Date(); this.formatted_validation_date = field_utils.format.datetime( - moment(this.validation_date), {}, {timezone: false}); + moment(this.validation_date), + {}, + { timezone: false } + ); } set_tip(tip) { @@ -2687,59 +2953,61 @@ class Order extends PosModel { } } return this.add_product(tip_product, { - is_tip: true, - quantity: 1, - price: tip, - lst_price: tip, - extras: {price_manually_set: true}, + is_tip: true, + quantity: 1, + price: tip, + lst_price: tip, + extras: { price_manually_set: true }, }); } } set_fiscal_position(fiscal_position) { this.fiscal_position = fiscal_position; } - set_pricelist (pricelist) { + set_pricelist(pricelist) { var self = this; this.pricelist = pricelist; var lines_to_recompute = _.filter(this.get_orderlines(), function (line) { - return ! line.price_manually_set; + return !line.price_manually_set; }); _.each(lines_to_recompute, function (line) { - line.set_unit_price(line.product.get_price(self.pricelist, line.get_quantity(), line.get_price_extra())); + line.set_unit_price( + line.product.get_price(self.pricelist, line.get_quantity(), line.get_price_extra()) + ); self.fix_tax_included_price(line); }); } - remove_orderline( line ){ + remove_orderline(line) { this.assert_editable(); this.orderlines.remove(line); this.select_orderline(this.get_last_orderline()); } - fix_tax_included_price(line){ + fix_tax_included_price(line) { line.set_unit_price(line.compute_fixed_price(line.price)); } - add_product(product, options){ - if(this._printed){ + add_product(product, options) { + if (this._printed) { // when adding product with a barcode while being in receipt screen this.pos.removeOrder(this); return this.pos.add_new_order().add_product(product, options); } this.assert_editable(); options = options || {}; - var line = Orderline.create({}, {pos: this.pos, order: this, product: product}); + var line = Orderline.create({}, { pos: this.pos, order: this, product: product }); this.fix_tax_included_price(line); this.set_orderline_options(line, options); var to_merge_orderline; for (var i = 0; i < this.orderlines.length; i++) { - if(this.orderlines.at(i).can_be_merged_with(line) && options.merge !== false){ + if (this.orderlines.at(i).can_be_merged_with(line) && options.merge !== false) { to_merge_orderline = this.orderlines.at(i); } } - if (to_merge_orderline){ + if (to_merge_orderline) { to_merge_orderline.merge(line); this.select_orderline(to_merge_orderline); } else { @@ -2752,34 +3020,40 @@ class Order extends PosModel { } } set_orderline_options(orderline, options) { - if(options.quantity !== undefined){ + if (options.quantity !== undefined) { orderline.set_quantity(options.quantity); } - if (options.price_extra !== undefined){ + if (options.price_extra !== undefined) { orderline.price_extra = options.price_extra; - orderline.set_unit_price(orderline.product.get_price(this.pricelist, orderline.get_quantity(), options.price_extra)); + orderline.set_unit_price( + orderline.product.get_price( + this.pricelist, + orderline.get_quantity(), + options.price_extra + ) + ); this.fix_tax_included_price(orderline); } - if(options.price !== undefined){ + if (options.price !== undefined) { orderline.set_unit_price(options.price); this.fix_tax_included_price(orderline); } - if(options.lst_price !== undefined){ + if (options.lst_price !== undefined) { orderline.set_lst_price(options.lst_price); } - if(options.discount !== undefined){ + if (options.discount !== undefined) { orderline.set_discount(options.discount); } - if (options.description !== undefined){ + if (options.description !== undefined) { orderline.description += options.description; } - if(options.extras !== undefined){ + if (options.extras !== undefined) { for (var prop in options.extras) { orderline[prop] = options.extras[prop]; } @@ -2795,29 +3069,29 @@ class Order extends PosModel { orderline.tax_ids = options.tax_ids; } } - get_selected_orderline(){ + get_selected_orderline() { return this.selected_orderline; } - select_orderline(line){ - if(line){ - if(line !== this.selected_orderline){ + select_orderline(line) { + if (line) { + if (line !== this.selected_orderline) { // if line (new line to select) is not the same as the old // selected_orderline, then we set the old line to false, // and set the new line to true. Also, set the new line as // the selected_orderline. - if(this.selected_orderline){ + if (this.selected_orderline) { this.selected_orderline.set_selected(false); } this.selected_orderline = line; this.selected_orderline.set_selected(true); } - }else{ + } else { this.selected_orderline = undefined; } - this.pos.numpadMode = 'quantity'; + this.pos.numpadMode = "quantity"; } - deselect_orderline(){ - if(this.selected_orderline){ + deselect_orderline() { + if (this.selected_orderline) { this.selected_orderline.set_selected(false); this.selected_orderline = undefined; } @@ -2829,21 +3103,24 @@ class Order extends PosModel { if (this.electronic_payment_in_progress()) { return false; } else { - var newPaymentline = Payment.create({},{order: this, payment_method:payment_method, pos: this.pos}); + var newPaymentline = Payment.create( + {}, + { order: this, payment_method: payment_method, pos: this.pos } + ); this.paymentlines.add(newPaymentline); this.select_paymentline(newPaymentline); - if(this.pos.config.cash_rounding){ - this.selected_paymentline.set_amount(0); + if (this.pos.config.cash_rounding) { + this.selected_paymentline.set_amount(0); } newPaymentline.set_amount(this.get_due()); if (payment_method.payment_terminal) { - newPaymentline.set_payment_status('pending'); + newPaymentline.set_payment_status("pending"); } return newPaymentline; } } - get_paymentlines(){ + get_paymentlines() { return this.paymentlines; } /** @@ -2851,15 +3128,15 @@ class Order extends PosModel { * * @param {String} cid */ - get_paymentline (cid) { + get_paymentline(cid) { var lines = this.get_paymentlines(); return lines.find(function (line) { return line.cid === cid; }); } - remove_paymentline(line){ + remove_paymentline(line) { this.assert_editable(); - if(this.selected_paymentline === line){ + if (this.selected_paymentline === line) { this.select_paymentline(undefined); } this.paymentlines.remove(line); @@ -2867,80 +3144,97 @@ class Order extends PosModel { clean_empty_paymentlines() { var lines = this.paymentlines; var empty = []; - for ( var i = 0; i < lines.length; i++) { + for (var i = 0; i < lines.length; i++) { if (!lines[i].get_amount()) { empty.push(lines[i]); } } - for ( var i = 0; i < empty.length; i++) { + for (i = 0; i < empty.length; i++) { this.remove_paymentline(empty[i]); } } - select_paymentline(line){ - if(line !== this.selected_paymentline){ - if(this.selected_paymentline){ + select_paymentline(line) { + if (line !== this.selected_paymentline) { + if (this.selected_paymentline) { this.selected_paymentline.set_selected(false); } this.selected_paymentline = line; - if(this.selected_paymentline){ + if (this.selected_paymentline) { this.selected_paymentline.set_selected(true); } } } electronic_payment_in_progress() { - return this.get_paymentlines() - .some(function(pl) { - if (pl.payment_status) { - return !['done', 'reversed'].includes(pl.payment_status); - } else { - return false; - } - }); + return this.get_paymentlines().some(function (pl) { + if (pl.payment_status) { + return !["done", "reversed"].includes(pl.payment_status); + } else { + return false; + } + }); } /** * Stops a payment on the terminal if one is running */ - stop_electronic_payment () { + stop_electronic_payment() { var lines = this.get_paymentlines(); var line = lines.find(function (line) { var status = line.get_payment_status(); - return status && !['done', 'reversed', 'reversing', 'pending', 'retry'].includes(status); + return ( + status && !["done", "reversed", "reversing", "pending", "retry"].includes(status) + ); }); if (line) { - line.set_payment_status('waitingCancel'); - line.payment_method.payment_terminal.send_payment_cancel(this, line.cid).finally(function () { - line.set_payment_status('retry'); - }); + line.set_payment_status("waitingCancel"); + line.payment_method.payment_terminal + .send_payment_cancel(this, line.cid) + .finally(function () { + line.set_payment_status("retry"); + }); } } /* ---- Payment Status --- */ - get_subtotal(){ - return round_pr(this.orderlines.reduce((function(sum, orderLine){ - return sum + orderLine.get_display_price(); - }), 0), this.pos.currency.rounding); + get_subtotal() { + return round_pr( + this.orderlines.reduce(function (sum, orderLine) { + return sum + orderLine.get_display_price(); + }, 0), + this.pos.currency.rounding + ); } get_total_with_tax() { return this.get_total_without_tax() + this.get_total_tax(); } get_total_without_tax() { - return round_pr(this.orderlines.reduce((function(sum, orderLine) { - return sum + orderLine.get_price_without_tax(); - }), 0), this.pos.currency.rounding); + return round_pr( + this.orderlines.reduce(function (sum, orderLine) { + return sum + orderLine.get_price_without_tax(); + }, 0), + this.pos.currency.rounding + ); } _get_ignored_product_ids_total_discount() { return []; } get_total_discount() { - const ignored_product_ids = this._get_ignored_product_ids_total_discount() - return round_pr(this.orderlines.reduce((sum, orderLine) => { - if (!ignored_product_ids.includes(orderLine.product.id)) { - sum += (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity()); - if (orderLine.display_discount_policy() === 'without_discount'){ - sum += ((orderLine.get_lst_price() - orderLine.get_unit_price()) * orderLine.get_quantity()); + const ignored_product_ids = this._get_ignored_product_ids_total_discount(); + return round_pr( + this.orderlines.reduce((sum, orderLine) => { + if (!ignored_product_ids.includes(orderLine.product.id)) { + sum += + orderLine.get_unit_price() * + (orderLine.get_discount() / 100) * + orderLine.get_quantity(); + if (orderLine.display_discount_policy() === "without_discount") { + sum += + (orderLine.get_lst_price() - orderLine.get_unit_price()) * + orderLine.get_quantity(); + } } - } - return sum; - }, 0), this.pos.currency.rounding); + return sum; + }, 0), + this.pos.currency.rounding + ); } get_total_tax() { if (this.pos.company.tax_calculation_rounding_method === "round_globally") { @@ -2952,7 +3246,7 @@ class Order extends PosModel { this.orderlines.forEach(function (line) { var taxDetails = line.get_tax_details(); var taxIds = Object.keys(taxDetails); - for (var t = 0; t { + this.orderlines.forEach((line) => { var taxes_ids = this.tax_ids || line.get_product().taxes_id; for (var i = 0; i < taxes_ids.length; i++) { if (tax_set[taxes_ids[i]]) { @@ -3050,10 +3354,11 @@ class Order extends PosModel { } get_change(paymentline) { if (!paymentline) { - var change = this.get_total_paid() - this.get_total_with_tax() - this.get_rounding_applied(); + var change = + this.get_total_paid() - this.get_total_with_tax() - this.get_rounding_applied(); } else { - var change = -this.get_total_with_tax(); - var lines = this.paymentlines; + change = -this.get_total_with_tax(); + var lines = this.paymentlines; for (var i = 0; i < lines.length; i++) { change += lines[i].get_amount(); if (lines[i] === paymentline) { @@ -3061,13 +3366,14 @@ class Order extends PosModel { } } } - return round_pr(Math.max(0,change), this.pos.currency.rounding); + return round_pr(Math.max(0, change), this.pos.currency.rounding); } get_due(paymentline) { if (!paymentline) { - var due = this.get_total_with_tax() - this.get_total_paid() + this.get_rounding_applied(); + var due = + this.get_total_with_tax() - this.get_total_paid() + this.get_rounding_applied(); } else { - var due = this.get_total_with_tax(); + due = this.get_total_with_tax(); var lines = this.paymentlines; for (var i = 0; i < lines.length; i++) { if (lines[i] === paymentline) { @@ -3080,11 +3386,13 @@ class Order extends PosModel { return round_pr(due, this.pos.currency.rounding); } get_rounding_applied() { - if(this.pos.config.cash_rounding) { + if (this.pos.config.cash_rounding) { const only_cash = this.pos.config.only_round_cash_method; const paymentlines = this.get_paymentlines(); - const last_line = paymentlines ? paymentlines[paymentlines.length-1]: false; - const last_line_is_cash = last_line ? last_line.payment_method.is_cash_count == true: false; + const last_line = paymentlines ? paymentlines[paymentlines.length - 1] : false; + const last_line_is_cash = last_line + ? last_line.payment_method.is_cash_count == true + : false; if (!only_cash || (only_cash && last_line_is_cash)) { var remaining = this.get_total_with_tax() - this.get_total_paid(); var total = round_pr(remaining, this.pos.cash_rounding[0].rounding); @@ -3093,68 +3401,97 @@ class Order extends PosModel { var rounding_applied = total - remaining; rounding_applied *= sign; // because floor and ceil doesn't include decimals in calculation, we reuse the value of the half-up and adapt it. - if (utils.float_is_zero(rounding_applied, this.pos.currency.decimal_places)){ + if (utils.float_is_zero(rounding_applied, this.pos.currency.decimal_places)) { // https://xkcd.com/217/ return 0; - } else if(Math.abs(this.get_total_with_tax()) < this.pos.cash_rounding[0].rounding) { + } else if ( + Math.abs(this.get_total_with_tax()) < this.pos.cash_rounding[0].rounding + ) { return 0; - } else if(this.pos.cash_rounding[0].rounding_method === "UP" && rounding_applied < 0 && remaining > 0) { + } else if ( + this.pos.cash_rounding[0].rounding_method === "UP" && + rounding_applied < 0 && + remaining > 0 + ) { rounding_applied += this.pos.cash_rounding[0].rounding; - } - else if(this.pos.cash_rounding[0].rounding_method === "UP" && rounding_applied > 0 && remaining < 0) { + } else if ( + this.pos.cash_rounding[0].rounding_method === "UP" && + rounding_applied > 0 && + remaining < 0 + ) { rounding_applied -= this.pos.cash_rounding[0].rounding; - } - else if(this.pos.cash_rounding[0].rounding_method === "DOWN" && rounding_applied > 0 && remaining > 0){ + } else if ( + this.pos.cash_rounding[0].rounding_method === "DOWN" && + rounding_applied > 0 && + remaining > 0 + ) { rounding_applied -= this.pos.cash_rounding[0].rounding; - } - else if(this.pos.cash_rounding[0].rounding_method === "DOWN" && rounding_applied < 0 && remaining < 0){ + } else if ( + this.pos.cash_rounding[0].rounding_method === "DOWN" && + rounding_applied < 0 && + remaining < 0 + ) { rounding_applied += this.pos.cash_rounding[0].rounding; } return sign * rounding_applied; - } - else { + } else { return 0; } } return 0; } has_not_valid_rounding() { - if(!this.pos.config.cash_rounding || this.get_total_with_tax() < this.pos.cash_rounding[0].rounding) + if ( + !this.pos.config.cash_rounding || + this.get_total_with_tax() < this.pos.cash_rounding[0].rounding + ) { return false; + } const only_cash = this.pos.config.only_round_cash_method; var lines = this.paymentlines; - for(var i = 0; i < lines.length; i++) { + for (var i = 0; i < lines.length; i++) { var line = lines[i]; - if (only_cash && !line.payment_method.is_cash_count) + if (only_cash && !line.payment_method.is_cash_count) { continue; + } - if(!utils.float_is_zero(line.amount - round_pr(line.amount, this.pos.cash_rounding[0].rounding), 6)) + if ( + !utils.float_is_zero( + line.amount - round_pr(line.amount, this.pos.cash_rounding[0].rounding), + 6 + ) + ) { return line; + } } return false; } - is_paid(){ + is_paid() { return this.get_due() <= 0 && this.check_paymentlines_rounding(); } - is_paid_with_cash(){ - return !!this.paymentlines.find( function(pl){ + is_paid_with_cash() { + return !!this.paymentlines.find(function (pl) { return pl.payment_method.is_cash_count; }); } check_paymentlines_rounding() { - if(this.pos.config.cash_rounding) { + if (this.pos.config.cash_rounding) { var cash_rounding = this.pos.cash_rounding[0].rounding; var default_rounding = this.pos.currency.rounding; - for(var id in this.get_paymentlines()) { + for (var id in this.get_paymentlines()) { var line = this.get_paymentlines()[id]; - var diff = round_pr(round_pr(line.amount, cash_rounding) - round_pr(line.amount, default_rounding), default_rounding); - if(this.get_total_with_tax() < this.pos.cash_rounding[0].rounding) + var diff = round_pr( + round_pr(line.amount, cash_rounding) - round_pr(line.amount, default_rounding), + default_rounding + ); + if (this.get_total_with_tax() < this.pos.cash_rounding[0].rounding) { return true; - if(diff && line.payment_method.is_cash_count) { + } + if (diff && line.payment_method.is_cash_count) { return false; - } else if(!this.pos.config.only_round_cash_method && diff) { + } else if (!this.pos.config.only_round_cash_method && diff) { return false; } } @@ -3163,58 +3500,60 @@ class Order extends PosModel { return true; } get_total_cost() { - return this.orderlines.reduce((function(sum, orderLine) { + return this.orderlines.reduce(function (sum, orderLine) { return sum + orderLine.get_total_cost(); - }), 0) + }, 0); } /* ---- Invoice --- */ set_to_invoice(to_invoice) { this.assert_editable(); this.to_invoice = to_invoice; } - is_to_invoice(){ + is_to_invoice() { return this.to_invoice; } /* ---- Partner --- */ // the partner related to the current order. - set_partner(partner){ + set_partner(partner) { this.assert_editable(); this.partner = partner; } - get_partner(){ + get_partner() { return this.partner; } - get_partner_name(){ - let partner = this.partner; + get_partner_name() { + const partner = this.partner; return partner ? partner.name : ""; } - get_cardholder_name(){ - var card_payment_line = this.paymentlines.find(pl => pl.cardholder_name); + get_cardholder_name() { + var card_payment_line = this.paymentlines.find((pl) => pl.cardholder_name); return card_payment_line ? card_payment_line.cardholder_name : ""; } /* ---- Screen Status --- */ // the order also stores the screen status, as the PoS supports // different active screens per order. This method is used to // store the screen status. - set_screen_data(value){ - this.screen_data['value'] = value; + set_screen_data(value) { + this.screen_data["value"] = value; } //see set_screen_data - get_screen_data(){ - const screen = this.screen_data['value']; + get_screen_data() { + const screen = this.screen_data["value"]; // If no screen data is saved // no payment line -> product screen // with payment line -> payment screen if (!screen) { - if (this.get_paymentlines().length > 0) return { name: 'PaymentScreen' }; - return { name: 'ProductScreen' }; + if (this.get_paymentlines().length > 0) { + return { name: "PaymentScreen" }; + } + return { name: "ProductScreen" }; } if (!this.finalized && this.get_paymentlines().length > 0) { - return { name: 'PaymentScreen' }; + return { name: "PaymentScreen" }; } return screen; } - wait_for_push_order () { + wait_for_push_order() { return false; } /** @@ -3256,7 +3595,7 @@ class Order extends PosModel { this.assert_editable(); this.to_ship = to_ship; } - is_to_ship(){ + is_to_ship() { return this.to_ship; } getHasRefundLines() { @@ -3282,24 +3621,14 @@ class Order extends PosModel { if (this.pos.company.point_of_sale_use_ticket_qr_code) { const codeWriter = new window.ZXing.BrowserQRCodeSvgWriter(); // Use the unique access token to ensure the authenticity of the request. Use the order reference as a second check just in case. - const address = `${this.pos.base_url}/pos/ticket/validate?access_token=${this.access_token}` - let qr_code_svg = new XMLSerializer().serializeToString(codeWriter.write(address, 150, 150)); - return "data:image/svg+xml;base64,"+ window.btoa(qr_code_svg); + const address = `${this.pos.base_url}/pos/ticket/validate?access_token=${this.access_token}`; + const qr_code_svg = new XMLSerializer().serializeToString( + codeWriter.write(address, 150, 150) + ); + return "data:image/svg+xml;base64," + window.btoa(qr_code_svg); } else { return false; } } } Registries.Model.add(Order); - -return { - register_payment_method, - PosGlobalState, - Product, - Orderline, - Packlotline, - Payment, - Order, -}; - -}); diff --git a/addons/point_of_sale/static/src/js/payment.js b/addons/point_of_sale/static/src/js/payment.js index f81784696c3..8e5724184f7 100644 --- a/addons/point_of_sale/static/src/js/payment.js +++ b/addons/point_of_sale/static/src/js/payment.js @@ -1,20 +1,19 @@ -odoo.define('point_of_sale.PaymentInterface', function (require) { -"use strict"; +/** @odoo-module */ -var core = require('web.core'); +import core from "web.core"; /** * Implement this interface to support a new payment method in the POS: * - * var PaymentInterface = require('point_of_sale.PaymentInterface'); + import var PaymentInterface from "@point_of_sale/js/payment"; * var MyPayment = PaymentInterface.extend({ * ... * }) * * To connect the interface to the right payment methods register it: * - * var models = require('point_of_sale.models'); - * models.register_payment_method('my_payment', MyPayment); + * import { register_payment_method } models from "@point_of_sale/js/models"; + * register_payment_method('my_payment', MyPayment); * * my_payment is the technical name of the added selection in * use_payment_terminal. @@ -91,5 +90,4 @@ var PaymentInterface = core.Class.extend({ close: function () {}, }); -return PaymentInterface; -}); +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 5347ac0bf9d..da033984cf4 100644 --- a/addons/point_of_sale/static/src/js/pos_env.js +++ b/addons/point_of_sale/static/src/js/pos_env.js @@ -1,19 +1,19 @@ -/* @odoo-module alias=point_of_sale.env */ +/* @odoo-module */ // This module is basically web.env but with added fields // that are specific to point_of_sale and extensions. -import env from 'web.env'; -import concurrency from 'web.concurrency'; -import devices from 'point_of_sale.devices'; -import BarcodeReader from 'point_of_sale.BarcodeReader'; +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"; // Create new env object base on web.env. // Mutating this new object won't affect the original object. -let pos_env = Object.create(env); +const pos_env = Object.create(env); -pos_env.proxy_queue = new devices.JobQueue(); // used to prevent parallels communications to the proxy -pos_env.proxy = new devices.ProxyDevice({ env: pos_env }); // used to communicate to the hardware devices via a local proxy +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(); diff --git a/addons/point_of_sale/static/src/js/printers.js b/addons/point_of_sale/static/src/js/printers.js index e9d5c3d4518..7eb0eb6ff69 100644 --- a/addons/point_of_sale/static/src/js/printers.js +++ b/addons/point_of_sale/static/src/js/printers.js @@ -1,27 +1,26 @@ +/** @odoo-module */ /* global html2canvas */ -odoo.define('point_of_sale.Printer', function (require) { -"use strict"; -var Session = require('web.Session'); -var core = require('web.core'); -const { Gui } = require('point_of_sale.Gui'); +import Session from "web.Session"; +import core from "web.core"; +import { Gui } from "@point_of_sale/js/Gui"; var _t = core._t; // IMPROVEMENT: This is too much. We can get away from this class. -class PrintResult { +export class PrintResult { constructor({ successful, message }) { this.successful = successful; this.message = message; } } -class PrintResultGenerator { +export class PrintResultGenerator { IoTActionError() { return new PrintResult({ successful: false, message: { - title: _t('Connection to IoT Box failed'), - body: _t('Please check if the IoT Box is still connected.'), + title: _t("Connection to IoT Box failed"), + body: _t("Please check if the IoT Box is still connected."), }, }); } @@ -29,11 +28,12 @@ class PrintResultGenerator { return new PrintResult({ successful: false, message: { - title: _t('Connection to the printer failed'), - body: _t('Please check if the printer is still connected. \n' + - 'Some browsers don\'t allow HTTP calls from websites to devices in the network (for security reasons). ' + - 'If it is the case, you will need to follow Odoo\'s documentation for ' + - '\'Self-signed certificate for ePOS printers\' and \'Secure connection (HTTPS)\' to solve the issue' + title: _t("Connection to the printer failed"), + body: _t( + "Please check if the printer is still connected. \n" + + "Some browsers don't allow HTTP calls from websites to devices in the network (for security reasons). " + + "If it is the case, you will need to follow Odoo's documentation for " + + "'Self-signed certificate for ePOS printers' and 'Secure connection (HTTPS)' to solve the issue" ), }, }); @@ -45,7 +45,7 @@ class PrintResultGenerator { } } -var PrinterMixin = { +export const PrinterMixin = { init: function (pos) { this.receipt_queue = []; this.printResultGenerator = new PrintResultGenerator(); @@ -58,7 +58,7 @@ var PrinterMixin = { * @param {String} receipt: The receipt to be printed, in HTML * @returns {PrintResult} */ - print_receipt: async function(receipt) { + print_receipt: async function (receipt) { if (receipt) { this.receipt_queue.push(receipt); } @@ -88,7 +88,7 @@ var PrinterMixin = { * @param {DOMElement} canvas */ process_canvas: function (canvas) { - return canvas.toDataURL('image/jpeg').replace('data:image/jpeg;base64,',''); + return canvas.toDataURL("image/jpeg").replace("data:image/jpeg;base64,", ""); }, /** @@ -96,45 +96,47 @@ var PrinterMixin = { * @param {String} receipt: The receipt to be printed, in HTML */ htmlToImg: function (receipt) { - $('.pos-receipt-print').html(receipt); - this.receipt = $('.pos-receipt-print>.pos-receipt'); + $(".pos-receipt-print").html(receipt); + this.receipt = $(".pos-receipt-print>.pos-receipt"); // Odoo RTL support automatically flip left into right but html2canvas // won't work as expected if the receipt is aligned to the right of the // screen so we need to flip it back. - this.receipt.parent().css({ left: 0, right: 'auto' }); + this.receipt.parent().css({ left: 0, right: "auto" }); return html2canvas(this.receipt[0], { height: Math.ceil(this.receipt.outerHeight() + this.receipt.offset().top), width: Math.ceil(this.receipt.outerWidth() + 2 * this.receipt.offset().left), scale: 1, - }).then(canvas => { - $('.pos-receipt-print').empty(); + }).then((canvas) => { + $(".pos-receipt-print").empty(); return this.process_canvas(canvas); }); }, - _onIoTActionResult: function (data){ + _onIoTActionResult: function (data) { if (this.pos && (data === false || data.result === false)) { - Gui.showPopup('ErrorPopup',{ - 'title': _t('Connection to the printer failed'), - 'body': _t('Please check if the printer is still connected.'), + Gui.showPopup("ErrorPopup", { + title: _t("Connection to the printer failed"), + body: _t("Please check if the printer is still connected."), }); } }, _onIoTActionFail: function () { if (this.pos) { - Gui.showPopup('ErrorPopup',{ - 'title': _t('Connection to IoT Box failed'), - 'body': _t('Please check if the IoT Box is still connected.'), + Gui.showPopup("ErrorPopup", { + title: _t("Connection to IoT Box failed"), + body: _t("Please check if the IoT Box is still connected."), }); } }, -} +}; -var Printer = core.Class.extend(PrinterMixin, { +export const Printer = core.Class.extend(PrinterMixin, { init: function (url, pos) { PrinterMixin.init.call(this, pos); - this.connection = new Session(undefined, url || 'http://localhost:8069', { use_cors: true}); + this.connection = new Session(undefined, url || "http://localhost:8069", { + use_cors: true, + }); }, /** @@ -144,11 +146,13 @@ var Printer = core.Class.extend(PrinterMixin, { */ open_cashbox: function () { var self = this; - return this.connection.rpc('/hw_proxy/default_printer_action', { - data: { - action: 'cashbox' - } - }).then(self._onIoTActionResult.bind(self)) + return this.connection + .rpc("/hw_proxy/default_printer_action", { + data: { + action: "cashbox", + }, + }) + .then(self._onIoTActionResult.bind(self)) .guardedCatch(self._onIoTActionFail.bind(self)); }, @@ -157,19 +161,11 @@ var Printer = core.Class.extend(PrinterMixin, { * @param {String} img : The receipt to be printed, as an image */ send_printing_job: function (img) { - return this.connection.rpc('/hw_proxy/default_printer_action', { + return this.connection.rpc("/hw_proxy/default_printer_action", { data: { - action: 'print_receipt', + action: "print_receipt", receipt: img, - } + }, }); }, }); - -return { - PrinterMixin: PrinterMixin, - Printer: Printer, - PrintResult, - PrintResultGenerator, -} -}); diff --git a/addons/point_of_sale/static/src/js/utils.js b/addons/point_of_sale/static/src/js/utils.js index a8eca669471..afefdc5e6bb 100644 --- a/addons/point_of_sale/static/src/js/utils.js +++ b/addons/point_of_sale/static/src/js/utils.js @@ -1,102 +1,100 @@ -odoo.define('point_of_sale.utils', function (require) { - 'use strict'; +/** @odoo-module */ +import { ConnectionAbortedError, ConnectionLostError } from "@web/core/network/rpc_service"; - const { ConnectionAbortedError, ConnectionLostError } = require('@web/core/network/rpc_service'); - - function getFileAsText(file) { - return new Promise((resolve, reject) => { - if (!file) { - reject(); - } else { - const reader = new FileReader(); - reader.addEventListener('load', function () { - resolve(reader.result); - }); - reader.addEventListener('abort', reject); - reader.addEventListener('error', reject); - reader.readAsText(file); - } - }); - } - - /** - * This global variable is used by nextFrame to store the timer and - * be able to cancel it before another request for animation frame. - */ - let timer = null; - - /** - * Wait for the next animation frame to finish. - */ - const nextFrame = () => { - return new Promise((resolve) => { - cancelAnimationFrame(timer); - timer = requestAnimationFrame(() => { - resolve(); +export function getFileAsText(file) { + return new Promise((resolve, reject) => { + if (!file) { + reject(); + } else { + const reader = new FileReader(); + reader.addEventListener("load", function () { + resolve(reader.result); }); - }); - }; - - function isConnectionError(error) { - const _error = identifyError(error); - return _error instanceof ConnectionAbortedError || _error instanceof ConnectionLostError; - } - - function identifyError(error) { - if (!error) return error; - let errorToHandle; - if (error.legacy) { - // error.message is either RPCError or ConnectionLostError - errorToHandle = error.message; - } else if (error.event && error.event.type == 'abort') { - // Check if there is event and if the event type is abort. - // If so, then it's supposed to be a ConnectionAbortedError, - // however, it was stripped in the patch of rpc in `mapLegacyEnvToWowlEnv`. - // We recreate the error object here so that in the actual handler, - // ConnectionAbortedError and ConnectionLostError are handled properly. - errorToHandle = new ConnectionAbortedError(error.message); - } else if (error instanceof Error) { - errorToHandle = error; + reader.addEventListener("abort", reject); + reader.addEventListener("error", reject); + reader.readAsText(file); } - return errorToHandle || error; - } + }); +} - /** - * Creates a batched version of a callback so that all calls to it in the same - * microtick will only call the original callback once. - * - * @param callback the callback to batch - * @returns a batched version of the original callback - */ - function batched(callback) { - let called = false; - return async () => { - // This await blocks all calls to the callback here, then releases them sequentially - // in the next microtick. This line decides the granularity of the batch. - await Promise.resolve(); - if (!called) { - called = true; - // so that only the first call to the batched function calls the original callback. - // Schedule this before calling the callback so that calls to the batched function - // within the callback will proceed only after resetting called to false, and have - // a chance to execute the callback again - Promise.resolve().then(() => called = false); - callback(); - } - }; - } +/** + * This global variable is used by nextFrame to store the timer and + * be able to cancel it before another request for animation frame. + */ +let timer = null; - /* - * comes from o_spreadsheet.js - * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript - * */ - function uuidv4() { - // mainly for jest and other browsers that do not have the crypto functionality - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - let r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); +/** + * Wait for the next animation frame to finish. + */ +export const nextFrame = () => { + return new Promise((resolve) => { + cancelAnimationFrame(timer); + timer = requestAnimationFrame(() => { + resolve(); }); - }; + }); +}; - return { getFileAsText, nextFrame, identifyError, isConnectionError, batched, uuidv4 }; -}); +export function isConnectionError(error) { + const _error = identifyError(error); + return _error instanceof ConnectionAbortedError || _error instanceof ConnectionLostError; +} + +export function identifyError(error) { + if (!error) { + return error; + } + let errorToHandle; + if (error.legacy) { + // error.message is either RPCError or ConnectionLostError + errorToHandle = error.message; + } else if (error.event && error.event.type == "abort") { + // Check if there is event and if the event type is abort. + // If so, then it's supposed to be a ConnectionAbortedError, + // however, it was stripped in the patch of rpc in `mapLegacyEnvToWowlEnv`. + // We recreate the error object here so that in the actual handler, + // ConnectionAbortedError and ConnectionLostError are handled properly. + errorToHandle = new ConnectionAbortedError(error.message); + } else if (error instanceof Error) { + errorToHandle = error; + } + return errorToHandle || error; +} + +/** + * Creates a batched version of a callback so that all calls to it in the same + * microtick will only call the original callback once. + * + * @param callback the callback to batch + * @returns a batched version of the original callback + */ +export function batched(callback) { + let called = false; + return async () => { + // This await blocks all calls to the callback here, then releases them sequentially + // in the next microtick. This line decides the granularity of the batch. + await Promise.resolve(); + if (!called) { + called = true; + // so that only the first call to the batched function calls the original callback. + // Schedule this before calling the callback so that calls to the batched function + // within the callback will proceed only after resetting called to false, and have + // a chance to execute the callback again + Promise.resolve().then(() => (called = false)); + callback(); + } + }; +} + +/* + * comes from o_spreadsheet.js + * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript + * */ +export function uuidv4() { + // mainly for jest and other browsers that do not have the crypto functionality + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0, + v = c == "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} diff --git a/addons/point_of_sale/static/tests/tours/Chrome.tour.js b/addons/point_of_sale/static/tests/tours/Chrome.tour.js index 9e7feba2ea6..9ce3f3845b9 100644 --- a/addons/point_of_sale/static/tests/tours/Chrome.tour.js +++ b/addons/point_of_sale/static/tests/tours/Chrome.tour.js @@ -1,104 +1,102 @@ -odoo.define('point_of_sale.tour.Chrome', function (require) { - 'use strict'; +/** @odoo-module */ - const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); - const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); - const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); - const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); - const { Chrome } = require('point_of_sale.tour.ChromeTourMethods'); - const { getSteps, startSteps } = require('point_of_sale.tour.utils'); - var Tour = require('web_tour.tour'); +import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods"; +import { ReceiptScreen } from "@point_of_sale/../tests/tours/helpers/ReceiptScreenTourMethods"; +import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods"; +import { TicketScreen } from "@point_of_sale/../tests/tours/helpers/TicketScreenTourMethods"; +import { Chrome } from "@point_of_sale/../tests/tours/helpers/ChromeTourMethods"; +import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils"; +import Tour from "web_tour.tour"; - startSteps(); +startSteps(); - // Order 1 is at Product Screen - ProductScreen.do.confirmOpeningPopup(); - ProductScreen.do.clickHomeCategory(); - ProductScreen.exec.addOrderline('Desk Pad', '1', '2', '2.0'); - Chrome.do.clickTicketButton(); - TicketScreen.check.checkStatus('-0001', 'Ongoing'); +// Order 1 is at Product Screen +ProductScreen.do.confirmOpeningPopup(); +ProductScreen.do.clickHomeCategory(); +ProductScreen.exec.addOrderline("Desk Pad", "1", "2", "2.0"); +Chrome.do.clickTicketButton(); +TicketScreen.check.checkStatus("-0001", "Ongoing"); - // Order 2 is at Payment Screen - TicketScreen.do.clickNewTicket(); - ProductScreen.exec.addOrderline('Monitor Stand', '3', '4', '12.0'); - ProductScreen.do.clickPayButton(); - PaymentScreen.check.isShown(); - Chrome.do.clickTicketButton(); - TicketScreen.check.checkStatus('-0002', 'Payment'); +// Order 2 is at Payment Screen +TicketScreen.do.clickNewTicket(); +ProductScreen.exec.addOrderline("Monitor Stand", "3", "4", "12.0"); +ProductScreen.do.clickPayButton(); +PaymentScreen.check.isShown(); +Chrome.do.clickTicketButton(); +TicketScreen.check.checkStatus("-0002", "Payment"); - // Order 3 is at Receipt Screen - TicketScreen.do.clickNewTicket(); - ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6', '30.0'); - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.check.remainingIs('0.0'); - PaymentScreen.check.validateButtonIsHighlighted(true); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.isShown(); - Chrome.do.clickTicketButton(); - TicketScreen.check.checkStatus('-0003', 'Receipt'); +// Order 3 is at Receipt Screen +TicketScreen.do.clickNewTicket(); +ProductScreen.exec.addOrderline("Whiteboard Pen", "5", "6", "30.0"); +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.check.remainingIs("0.0"); +PaymentScreen.check.validateButtonIsHighlighted(true); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.isShown(); +Chrome.do.clickTicketButton(); +TicketScreen.check.checkStatus("-0003", "Receipt"); - // Select order 1, should be at Product Screen - TicketScreen.do.selectOrder('-0001'); - ProductScreen.check.productIsDisplayed('Desk Pad'); - ProductScreen.check.selectedOrderlineHas('Desk Pad', '1.0', '2.0'); +// Select order 1, should be at Product Screen +TicketScreen.do.selectOrder("-0001"); +ProductScreen.check.productIsDisplayed("Desk Pad"); +ProductScreen.check.selectedOrderlineHas("Desk Pad", "1.0", "2.0"); - // Select order 2, should be at Payment Screen - Chrome.do.clickTicketButton(); - TicketScreen.do.selectOrder('-0002'); - PaymentScreen.check.emptyPaymentlines('12.0'); - PaymentScreen.check.validateButtonIsHighlighted(false); +// Select order 2, should be at Payment Screen +Chrome.do.clickTicketButton(); +TicketScreen.do.selectOrder("-0002"); +PaymentScreen.check.emptyPaymentlines("12.0"); +PaymentScreen.check.validateButtonIsHighlighted(false); - // Select order 3, should be at Receipt Screen - Chrome.do.clickTicketButton(); - TicketScreen.do.selectOrder('-0003'); - ReceiptScreen.check.totalAmountContains('30.0'); +// Select order 3, should be at Receipt Screen +Chrome.do.clickTicketButton(); +TicketScreen.do.selectOrder("-0003"); +ReceiptScreen.check.totalAmountContains("30.0"); - // Pay order 1, with change - Chrome.do.clickTicketButton(); - TicketScreen.do.selectOrder('-0001'); - ProductScreen.check.isShown(); - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Cash'); - PaymentScreen.do.pressNumpad('2 0'); - PaymentScreen.check.remainingIs('0.0'); - PaymentScreen.check.validateButtonIsHighlighted(true); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.totalAmountContains('2.0'); +// Pay order 1, with change +Chrome.do.clickTicketButton(); +TicketScreen.do.selectOrder("-0001"); +ProductScreen.check.isShown(); +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickPaymentMethod("Cash"); +PaymentScreen.do.pressNumpad("2 0"); +PaymentScreen.check.remainingIs("0.0"); +PaymentScreen.check.validateButtonIsHighlighted(true); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.totalAmountContains("2.0"); - // Order 1 now should have Receipt status - Chrome.do.clickTicketButton(); - TicketScreen.check.checkStatus('-0001', 'Receipt'); +// Order 1 now should have Receipt status +Chrome.do.clickTicketButton(); +TicketScreen.check.checkStatus("-0001", "Receipt"); - // Select order 3, should still be at Receipt Screen - // and the total amount doesn't change. - TicketScreen.do.selectOrder('-0003'); - ReceiptScreen.check.totalAmountContains('30.0'); +// Select order 3, should still be at Receipt Screen +// and the total amount doesn't change. +TicketScreen.do.selectOrder("-0003"); +ReceiptScreen.check.totalAmountContains("30.0"); - // click next screen on order 3 - // then delete the new empty order - ReceiptScreen.do.clickNextOrder(); - ProductScreen.check.orderIsEmpty(); - Chrome.do.clickTicketButton(); - TicketScreen.do.deleteOrder('-0004'); - TicketScreen.do.deleteOrder('-0001'); +// click next screen on order 3 +// then delete the new empty order +ReceiptScreen.do.clickNextOrder(); +ProductScreen.check.orderIsEmpty(); +Chrome.do.clickTicketButton(); +TicketScreen.do.deleteOrder("-0004"); +TicketScreen.do.deleteOrder("-0001"); - // After deleting order 1 above, order 2 became - // the 2nd-row order and it has payment status - TicketScreen.check.nthRowContains(2, 'Payment') - TicketScreen.do.deleteOrder('-0002'); - Chrome.do.confirmPopup(); - TicketScreen.do.clickNewTicket(); +// After deleting order 1 above, order 2 became +// the 2nd-row order and it has payment status +TicketScreen.check.nthRowContains(2, "Payment"); +TicketScreen.do.deleteOrder("-0002"); +Chrome.do.confirmPopup(); +TicketScreen.do.clickNewTicket(); - // Invoice an order - ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6'); - ProductScreen.do.clickPartnerButton(); - ProductScreen.do.clickCustomer('Nicole Ford'); - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.do.clickInvoiceButton(); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.isShown(); +// Invoice an order +ProductScreen.exec.addOrderline("Whiteboard Pen", "5", "6"); +ProductScreen.do.clickPartnerButton(); +ProductScreen.do.clickCustomer("Nicole Ford"); +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.do.clickInvoiceButton(); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.isShown(); - Tour.register('ChromeTour', { test: true, url: '/pos/ui' }, getSteps()); -}); +Tour.register("ChromeTour", { test: true, url: "/pos/ui" }, getSteps()); diff --git a/addons/point_of_sale/static/tests/tours/FixedPriceNegativeQty.tour.js b/addons/point_of_sale/static/tests/tours/FixedPriceNegativeQty.tour.js new file mode 100644 index 00000000000..58daa865a78 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/FixedPriceNegativeQty.tour.js @@ -0,0 +1,25 @@ +/** @odoo-module */ + +import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods"; +import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods"; +import { ReceiptScreen } from "@point_of_sale/../tests/tours/helpers/ReceiptScreenTourMethods"; +import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils"; +import Tour from "web_tour.tour"; + +startSteps(); + +ProductScreen.do.clickHomeCategory(); + +ProductScreen.do.clickDisplayedProduct("Zero Amount Product"); +ProductScreen.check.selectedOrderlineHas("Zero Amount Product", "1.0", "1.0"); +ProductScreen.do.pressNumpad("+/- 1"); +ProductScreen.check.selectedOrderlineHas("Zero Amount Product", "-1.0", "-1.0"); + +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.check.remainingIs("0.00"); +PaymentScreen.do.clickValidate(); + +ReceiptScreen.check.receiptIsThere(); + +Tour.register("FixedTaxNegativeQty", { test: true, url: "/pos/ui" }, getSteps()); diff --git a/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js index 4330f58f3a0..67e517d432d 100644 --- a/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js @@ -1,84 +1,82 @@ -odoo.define('point_of_sale.tour.PaymentScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); - const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); - const { getSteps, startSteps } = require('point_of_sale.tour.utils'); - var Tour = require('web_tour.tour'); +import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods"; +import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods"; +import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils"; +import Tour from "web_tour.tour"; - startSteps(); +startSteps(); - ProductScreen.exec.addOrderline('Letter Tray', '10'); - ProductScreen.check.selectedOrderlineHas('Letter Tray', '10.0'); - ProductScreen.do.clickPayButton(); - PaymentScreen.check.emptyPaymentlines('52.8'); +ProductScreen.exec.addOrderline("Letter Tray", "10"); +ProductScreen.check.selectedOrderlineHas("Letter Tray", "10.0"); +ProductScreen.do.clickPayButton(); +PaymentScreen.check.emptyPaymentlines("52.8"); - PaymentScreen.do.clickPaymentMethod('Cash'); - PaymentScreen.do.pressNumpad('1 1'); - PaymentScreen.check.selectedPaymentlineHas('Cash', '11.00'); - PaymentScreen.check.remainingIs('41.8'); - PaymentScreen.check.changeIs('0.0'); - PaymentScreen.check.validateButtonIsHighlighted(false); - // remove the selected paymentline with multiple backspace presses - PaymentScreen.do.pressNumpad('Backspace Backspace'); - PaymentScreen.check.selectedPaymentlineHas('Cash', '0.00'); - PaymentScreen.do.pressNumpad('Backspace'); - PaymentScreen.check.emptyPaymentlines('52.8'); +PaymentScreen.do.clickPaymentMethod("Cash"); +PaymentScreen.do.pressNumpad("1 1"); +PaymentScreen.check.selectedPaymentlineHas("Cash", "11.00"); +PaymentScreen.check.remainingIs("41.8"); +PaymentScreen.check.changeIs("0.0"); +PaymentScreen.check.validateButtonIsHighlighted(false); +// remove the selected paymentline with multiple backspace presses +PaymentScreen.do.pressNumpad("Backspace Backspace"); +PaymentScreen.check.selectedPaymentlineHas("Cash", "0.00"); +PaymentScreen.do.pressNumpad("Backspace"); +PaymentScreen.check.emptyPaymentlines("52.8"); - // Pay with bank, the selected line should have full amount - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.check.remainingIs('0.0'); - PaymentScreen.check.changeIs('0.0'); - PaymentScreen.check.validateButtonIsHighlighted(true); - // remove the line using the delete button - PaymentScreen.do.clickPaymentlineDelButton('Bank', '52.8'); +// Pay with bank, the selected line should have full amount +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.check.remainingIs("0.0"); +PaymentScreen.check.changeIs("0.0"); +PaymentScreen.check.validateButtonIsHighlighted(true); +// remove the line using the delete button +PaymentScreen.do.clickPaymentlineDelButton("Bank", "52.8"); - // Use +10 and +50 to increment the amount of the paymentline - PaymentScreen.do.clickPaymentMethod('Cash'); - PaymentScreen.do.pressNumpad('+10'); - PaymentScreen.check.remainingIs('42.8'); - PaymentScreen.check.changeIs('0.0'); - PaymentScreen.check.validateButtonIsHighlighted(false); - PaymentScreen.do.pressNumpad('+50'); - PaymentScreen.check.remainingIs('0.0'); - PaymentScreen.check.changeIs('7.2'); - PaymentScreen.check.validateButtonIsHighlighted(true); - PaymentScreen.do.clickPaymentlineDelButton('Cash', '60.0'); +// Use +10 and +50 to increment the amount of the paymentline +PaymentScreen.do.clickPaymentMethod("Cash"); +PaymentScreen.do.pressNumpad("+10"); +PaymentScreen.check.remainingIs("42.8"); +PaymentScreen.check.changeIs("0.0"); +PaymentScreen.check.validateButtonIsHighlighted(false); +PaymentScreen.do.pressNumpad("+50"); +PaymentScreen.check.remainingIs("0.0"); +PaymentScreen.check.changeIs("7.2"); +PaymentScreen.check.validateButtonIsHighlighted(true); +PaymentScreen.do.clickPaymentlineDelButton("Cash", "60.0"); - // Multiple paymentlines - PaymentScreen.do.clickPaymentMethod('Cash'); - PaymentScreen.do.pressNumpad('1'); - PaymentScreen.check.remainingIs('51.8'); - PaymentScreen.check.changeIs('0.0'); - PaymentScreen.check.validateButtonIsHighlighted(false); - PaymentScreen.do.clickPaymentMethod('Cash'); - PaymentScreen.do.pressNumpad('5'); - PaymentScreen.check.remainingIs('46.8'); - PaymentScreen.check.changeIs('0.0'); - PaymentScreen.check.validateButtonIsHighlighted(false); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.do.pressNumpad('2 0'); - PaymentScreen.check.remainingIs('26.8'); - PaymentScreen.check.changeIs('0.0'); - PaymentScreen.check.validateButtonIsHighlighted(false); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.check.remainingIs('0.0'); - PaymentScreen.check.changeIs('0.0'); - PaymentScreen.check.validateButtonIsHighlighted(true); +// Multiple paymentlines +PaymentScreen.do.clickPaymentMethod("Cash"); +PaymentScreen.do.pressNumpad("1"); +PaymentScreen.check.remainingIs("51.8"); +PaymentScreen.check.changeIs("0.0"); +PaymentScreen.check.validateButtonIsHighlighted(false); +PaymentScreen.do.clickPaymentMethod("Cash"); +PaymentScreen.do.pressNumpad("5"); +PaymentScreen.check.remainingIs("46.8"); +PaymentScreen.check.changeIs("0.0"); +PaymentScreen.check.validateButtonIsHighlighted(false); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.do.pressNumpad("2 0"); +PaymentScreen.check.remainingIs("26.8"); +PaymentScreen.check.changeIs("0.0"); +PaymentScreen.check.validateButtonIsHighlighted(false); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.check.remainingIs("0.0"); +PaymentScreen.check.changeIs("0.0"); +PaymentScreen.check.validateButtonIsHighlighted(true); - Tour.register('PaymentScreenTour', { test: true, url: '/pos/ui' }, getSteps()); +Tour.register("PaymentScreenTour", { test: true, url: "/pos/ui" }, getSteps()); - startSteps(); +startSteps(); - ProductScreen.do.clickHomeCategory(); - ProductScreen.exec.addOrderline('Letter Tray', '1', '10'); - ProductScreen.do.clickPayButton(); +ProductScreen.do.clickHomeCategory(); +ProductScreen.exec.addOrderline("Letter Tray", "1", "10"); +ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.do.pressNumpad('1 0 0 0'); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.do.pressNumpad("1 0 0 0"); - PaymentScreen.check.remainingIs('0.0'); - PaymentScreen.check.changeIs('0.0'); +PaymentScreen.check.remainingIs("0.0"); +PaymentScreen.check.changeIs("0.0"); - Tour.register('PaymentScreenTour2', { test: true, url: '/pos/ui' }, getSteps()); -}); +Tour.register("PaymentScreenTour2", { test: true, url: "/pos/ui" }, getSteps()); diff --git a/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js b/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js index d092e7a11f7..e29d81e505d 100644 --- a/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js +++ b/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js @@ -1,67 +1,77 @@ -odoo.define('point_of_sale.tour.ProductConfigurator', function (require) { - 'use strict'; +/** @odoo-module */ - const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); - const { ProductConfigurator } = require('point_of_sale.tour.ProductConfiguratorTourMethods'); - const { getSteps, startSteps } = require('point_of_sale.tour.utils'); - var Tour = require('web_tour.tour'); +import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods"; +import { ProductConfigurator } from "@point_of_sale/../tests/tours/helpers/ProductConfiguratorTourMethods"; +import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils"; +import Tour from "web_tour.tour"; - // signal to start generating steps - // when finished, steps can be taken from getSteps - startSteps(); +// signal to start generating steps +// when finished, steps can be taken from getSteps +startSteps(); - ProductScreen.do.confirmOpeningPopup(); - // Go by default to home category - ProductScreen.do.clickHomeCategory(); +ProductScreen.do.confirmOpeningPopup(); +// Go by default to home category +ProductScreen.do.clickHomeCategory(); - // Click on Configurable Chair product - ProductScreen.do.clickDisplayedProduct('Configurable Chair'); - ProductConfigurator.check.isShown(); +// Click on Configurable Chair product +ProductScreen.do.clickDisplayedProduct("Configurable Chair"); +ProductConfigurator.check.isShown(); - // Cancel configuration, not product should be in order - ProductConfigurator.do.cancelAttributes(); - ProductScreen.check.orderIsEmpty(); +// Cancel configuration, not product should be in order +ProductConfigurator.do.cancelAttributes(); +ProductScreen.check.orderIsEmpty(); - // Click on Configurable Chair product - ProductScreen.do.clickDisplayedProduct('Configurable Chair'); - ProductConfigurator.check.isShown(); +// Click on Configurable Chair product +ProductScreen.do.clickDisplayedProduct("Configurable Chair"); +ProductConfigurator.check.isShown(); - // Pick Color - ProductConfigurator.do.pickColor('Red'); +// Pick Color +ProductConfigurator.do.pickColor("Red"); - // Pick Radio - ProductConfigurator.do.pickSelect('Metal'); +// Pick Radio +ProductConfigurator.do.pickSelect("Metal"); - // Pick Select - ProductConfigurator.do.pickRadio('Other'); +// Pick Select +ProductConfigurator.do.pickRadio("Other"); - // Fill in custom attribute - ProductConfigurator.do.fillCustomAttribute('Custom Fabric'); +// Fill in custom attribute +ProductConfigurator.do.fillCustomAttribute("Custom Fabric"); - // Confirm configuration - ProductConfigurator.do.confirmAttributes(); +// Confirm configuration +ProductConfigurator.do.confirmAttributes(); - // Check that the product has been added to the order with correct attributes and price - ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '1.0', '11.0'); +// Check that the product has been added to the order with correct attributes and price +ProductScreen.check.selectedOrderlineHas( + "Configurable Chair (Red, Metal, Other: Custom Fabric)", + "1.0", + "11.0" +); - // Orderlines with the same attributes should be merged - ProductScreen.do.clickHomeCategory(); - ProductScreen.do.clickDisplayedProduct('Configurable Chair'); - ProductConfigurator.do.pickColor('Red'); - ProductConfigurator.do.pickSelect('Metal'); - ProductConfigurator.do.pickRadio('Other'); - ProductConfigurator.do.fillCustomAttribute('Custom Fabric'); - ProductConfigurator.do.confirmAttributes(); - ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '2.0', '22.0'); +// Orderlines with the same attributes should be merged +ProductScreen.do.clickHomeCategory(); +ProductScreen.do.clickDisplayedProduct("Configurable Chair"); +ProductConfigurator.do.pickColor("Red"); +ProductConfigurator.do.pickSelect("Metal"); +ProductConfigurator.do.pickRadio("Other"); +ProductConfigurator.do.fillCustomAttribute("Custom Fabric"); +ProductConfigurator.do.confirmAttributes(); +ProductScreen.check.selectedOrderlineHas( + "Configurable Chair (Red, Metal, Other: Custom Fabric)", + "2.0", + "22.0" +); - // Orderlines with different attributes shouldn't be merged - ProductScreen.do.clickHomeCategory(); - ProductScreen.do.clickDisplayedProduct('Configurable Chair'); - ProductConfigurator.do.pickColor('Blue'); - ProductConfigurator.do.pickSelect('Metal'); - ProductConfigurator.do.pickRadio('Leather'); - ProductConfigurator.do.confirmAttributes(); - ProductScreen.check.selectedOrderlineHas('Configurable Chair (Blue, Metal, Leather)', '1.0', '10.0'); +// Orderlines with different attributes shouldn't be merged +ProductScreen.do.clickHomeCategory(); +ProductScreen.do.clickDisplayedProduct("Configurable Chair"); +ProductConfigurator.do.pickColor("Blue"); +ProductConfigurator.do.pickSelect("Metal"); +ProductConfigurator.do.pickRadio("Leather"); +ProductConfigurator.do.confirmAttributes(); +ProductScreen.check.selectedOrderlineHas( + "Configurable Chair (Blue, Metal, Leather)", + "1.0", + "10.0" +); - Tour.register('ProductConfiguratorTour', { test: true, url: '/pos/ui' }, getSteps()); -}); +Tour.register("ProductConfiguratorTour", { test: true, url: "/pos/ui" }, getSteps()); diff --git a/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js b/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js index 8fc7c15b2a0..0311e1151fe 100644 --- a/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js @@ -1,163 +1,132 @@ -odoo.define('point_of_sale.tour.ProductScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); - const { TextAreaPopup } = require('point_of_sale.tour.TextAreaPopupTourMethods'); - const { getSteps, startSteps } = require('point_of_sale.tour.utils'); - var Tour = require('web_tour.tour'); +import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods"; +import { TextAreaPopup } from "@point_of_sale/../tests/tours/helpers/TextAreaPopupTourMethods"; +import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils"; +import Tour from "web_tour.tour"; - // signal to start generating steps - // when finished, steps can be taken from getSteps - startSteps(); +// signal to start generating steps +// when finished, steps can be taken from getSteps +startSteps(); - // Go by default to home category - ProductScreen.do.clickHomeCategory(); +// Go by default to home category +ProductScreen.do.clickHomeCategory(); - // Clicking product multiple times should increment quantity - ProductScreen.do.clickDisplayedProduct('Desk Organizer'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.0', '5.10'); - ProductScreen.do.clickDisplayedProduct('Desk Organizer'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '2.0', '10.20'); +// Clicking product multiple times should increment quantity +ProductScreen.do.clickDisplayedProduct("Desk Organizer"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "1.0", "5.10"); +ProductScreen.do.clickDisplayedProduct("Desk Organizer"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "2.0", "10.20"); - // Clicking product should add new orderline and select the orderline - // If orderline exists, increment the quantity - ProductScreen.do.clickDisplayedProduct('Letter Tray'); - ProductScreen.check.selectedOrderlineHas('Letter Tray', '1.0', '5.28'); - ProductScreen.do.clickDisplayedProduct('Desk Organizer'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '3.0', '15.30'); +// Clicking product should add new orderline and select the orderline +// If orderline exists, increment the quantity +ProductScreen.do.clickDisplayedProduct("Letter Tray"); +ProductScreen.check.selectedOrderlineHas("Letter Tray", "1.0", "5.28"); +ProductScreen.do.clickDisplayedProduct("Desk Organizer"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "3.0", "15.30"); - // Check effects of clicking numpad buttons - ProductScreen.do.clickOrderline('Letter Tray', '1'); - ProductScreen.check.selectedOrderlineHas('Letter Tray', '1.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Letter Tray', '0.0', '0.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '3', '15.30'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '0.0', '0.0'); - ProductScreen.do.pressNumpad('1'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.0', '5.1'); - ProductScreen.do.pressNumpad('2'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '12.0', '61.2'); - ProductScreen.do.pressNumpad('3'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.0', '627.3'); - ProductScreen.do.pressNumpad('. 5'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '629.85'); - ProductScreen.do.pressNumpad('Price'); - ProductScreen.do.pressNumpad('1'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '123.5'); - ProductScreen.do.pressNumpad('1 .'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '1,358.5'); - ProductScreen.do.pressNumpad('Disc'); - ProductScreen.do.pressNumpad('5 .'); - ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '1,290.58'); - ProductScreen.do.pressNumpad('Qty'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.orderIsEmpty(); +// Check effects of clicking numpad buttons +ProductScreen.do.clickOrderline("Letter Tray", "1"); +ProductScreen.check.selectedOrderlineHas("Letter Tray", "1.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Letter Tray", "0.0", "0.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "3", "15.30"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "0.0", "0.0"); +ProductScreen.do.pressNumpad("1"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "1.0", "5.1"); +ProductScreen.do.pressNumpad("2"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "12.0", "61.2"); +ProductScreen.do.pressNumpad("3"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "123.0", "627.3"); +ProductScreen.do.pressNumpad(". 5"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "123.5", "629.85"); +ProductScreen.do.pressNumpad("Price"); +ProductScreen.do.pressNumpad("1"); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "123.5", "123.5"); +ProductScreen.do.pressNumpad("1 ."); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "123.5", "1,358.5"); +ProductScreen.do.pressNumpad("Disc"); +ProductScreen.do.pressNumpad("5 ."); +ProductScreen.check.selectedOrderlineHas("Desk Organizer", "123.5", "1,290.58"); +ProductScreen.do.pressNumpad("Qty"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.orderIsEmpty(); - // Check different subcategories - ProductScreen.do.clickSubcategory('Desks'); - ProductScreen.check.productIsDisplayed('Desk Pad'); - ProductScreen.do.clickHomeCategory(); - ProductScreen.do.clickSubcategory('Miscellaneous'); - ProductScreen.check.productIsDisplayed('Whiteboard Pen'); - ProductScreen.do.clickHomeCategory(); - ProductScreen.do.clickSubcategory('Chairs'); - ProductScreen.check.productIsDisplayed('Letter Tray'); - ProductScreen.do.clickHomeCategory(); - - // Add two orderlines and update quantity - ProductScreen.do.clickDisplayedProduct('Whiteboard Pen'); - ProductScreen.do.clickDisplayedProduct('Wall Shelf Unit'); - ProductScreen.do.clickOrderline('Whiteboard Pen', '1.0'); - ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.0'); - ProductScreen.do.pressNumpad('2'); - ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '2.0'); - ProductScreen.do.clickOrderline('Wall Shelf Unit', '1.0'); - ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '1.0'); - ProductScreen.do.pressNumpad('2'); - ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '2.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '0.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '2.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '0.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.orderIsEmpty(); +// Check different subcategories +ProductScreen.do.clickSubcategory("Desks"); +ProductScreen.check.productIsDisplayed("Desk Pad"); +ProductScreen.do.clickHomeCategory(); +ProductScreen.do.clickSubcategory("Miscellaneous"); +ProductScreen.check.productIsDisplayed("Whiteboard Pen"); +ProductScreen.do.clickHomeCategory(); +ProductScreen.do.clickSubcategory("Chairs"); +ProductScreen.check.productIsDisplayed("Letter Tray"); +ProductScreen.do.clickHomeCategory(); - // Add multiple orderlines then delete each of them until empty - ProductScreen.do.clickDisplayedProduct('Whiteboard Pen'); - ProductScreen.do.clickDisplayedProduct('Wall Shelf Unit'); - ProductScreen.do.clickDisplayedProduct('Small Shelf'); - ProductScreen.do.clickDisplayedProduct('Magnetic Board'); - ProductScreen.do.clickDisplayedProduct('Monitor Stand'); - ProductScreen.do.clickOrderline('Whiteboard Pen', '1.0'); - ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '0.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0'); - ProductScreen.do.clickOrderline('Wall Shelf Unit', '1.0'); - ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '1.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '0.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0'); - ProductScreen.do.clickOrderline('Small Shelf', '1.0'); - ProductScreen.check.selectedOrderlineHas('Small Shelf', '1.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Small Shelf', '0.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0'); - ProductScreen.do.clickOrderline('Magnetic Board', '1.0'); - ProductScreen.check.selectedOrderlineHas('Magnetic Board', '1.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Magnetic Board', '0.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.selectedOrderlineHas('Monitor Stand', '0.0'); - ProductScreen.do.pressNumpad('Backspace'); - ProductScreen.check.orderIsEmpty(); +// Add two orderlines and update quantity +ProductScreen.do.clickDisplayedProduct("Whiteboard Pen"); +ProductScreen.do.clickDisplayedProduct("Wall Shelf Unit"); +ProductScreen.do.clickOrderline("Whiteboard Pen", "1.0"); +ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "1.0"); +ProductScreen.do.pressNumpad("2"); +ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "2.0"); +ProductScreen.do.clickOrderline("Wall Shelf Unit", "1.0"); +ProductScreen.check.selectedOrderlineHas("Wall Shelf Unit", "1.0"); +ProductScreen.do.pressNumpad("2"); +ProductScreen.check.selectedOrderlineHas("Wall Shelf Unit", "2.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Wall Shelf Unit", "0.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "2.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "0.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.orderIsEmpty(); - // Test OrderlineCustomerNoteButton - ProductScreen.do.clickDisplayedProduct('Desk Organizer'); - ProductScreen.do.clickOrderlineCustomerNoteButton(); - TextAreaPopup.check.isShown(); - TextAreaPopup.do.inputText('Test customer note'); - TextAreaPopup.do.clickConfirm(); - ProductScreen.check.orderlineHasCustomerNote('Desk Organizer', '1', 'Test customer note'); +// Add multiple orderlines then delete each of them until empty +ProductScreen.do.clickDisplayedProduct("Whiteboard Pen"); +ProductScreen.do.clickDisplayedProduct("Wall Shelf Unit"); +ProductScreen.do.clickDisplayedProduct("Small Shelf"); +ProductScreen.do.clickDisplayedProduct("Magnetic Board"); +ProductScreen.do.clickDisplayedProduct("Monitor Stand"); +ProductScreen.do.clickOrderline("Whiteboard Pen", "1.0"); +ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "1.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "0.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Monitor Stand", "1.0"); +ProductScreen.do.clickOrderline("Wall Shelf Unit", "1.0"); +ProductScreen.check.selectedOrderlineHas("Wall Shelf Unit", "1.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Wall Shelf Unit", "0.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Monitor Stand", "1.0"); +ProductScreen.do.clickOrderline("Small Shelf", "1.0"); +ProductScreen.check.selectedOrderlineHas("Small Shelf", "1.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Small Shelf", "0.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Monitor Stand", "1.0"); +ProductScreen.do.clickOrderline("Magnetic Board", "1.0"); +ProductScreen.check.selectedOrderlineHas("Magnetic Board", "1.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Magnetic Board", "0.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Monitor Stand", "1.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.selectedOrderlineHas("Monitor Stand", "0.0"); +ProductScreen.do.pressNumpad("Backspace"); +ProductScreen.check.orderIsEmpty(); +// Test OrderlineCustomerNoteButton +ProductScreen.do.clickDisplayedProduct("Desk Organizer"); +ProductScreen.do.clickOrderlineCustomerNoteButton(); +TextAreaPopup.check.isShown(); +TextAreaPopup.do.inputText("Test customer note"); +TextAreaPopup.do.clickConfirm(); +ProductScreen.check.orderlineHasCustomerNote("Desk Organizer", "1", "Test customer note"); - Tour.register('ProductScreenTour', { test: true, url: '/pos/ui' }, getSteps()); -}); - -odoo.define('point_of_sale.tour.FixedPriceNegativeQty', function (require) { - 'use strict'; - - const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); - const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); - const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); - const { getSteps, startSteps } = require('point_of_sale.tour.utils'); - var Tour = require('web_tour.tour'); - - startSteps(); - - ProductScreen.do.clickHomeCategory(); - - ProductScreen.do.clickDisplayedProduct('Zero Amount Product'); - ProductScreen.check.selectedOrderlineHas('Zero Amount Product', '1.0', '1.0'); - ProductScreen.do.pressNumpad('+/- 1'); - ProductScreen.check.selectedOrderlineHas('Zero Amount Product', '-1.0', '-1.0'); - - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.check.remainingIs('0.00'); - PaymentScreen.do.clickValidate(); - - ReceiptScreen.check.receiptIsThere(); - - Tour.register('FixedTaxNegativeQty', { test: true, url: '/pos/ui' }, getSteps()); -}); +Tour.register("ProductScreenTour", { test: true, url: "/pos/ui" }, getSteps()); diff --git a/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js b/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js index 08306fc60e5..95a53a85a50 100644 --- a/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js @@ -1,69 +1,67 @@ -odoo.define('point_of_sale.tour.ReceiptScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); - const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); - const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); - const { NumberPopup } = require('point_of_sale.tour.NumberPopupTourMethods'); - const { getSteps, startSteps } = require('point_of_sale.tour.utils'); - const Tour = require('web_tour.tour'); +import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods"; +import { ReceiptScreen } from "@point_of_sale/../tests/tours/helpers/ReceiptScreenTourMethods"; +import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods"; +import { NumberPopup } from "@point_of_sale/../tests/tours/helpers/NumberPopupTourMethods"; +import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils"; +import Tour from "web_tour.tour"; - startSteps(); +startSteps(); - // press close button in receipt screen - ProductScreen.exec.addOrderline('Letter Tray', '10', '5'); - ProductScreen.check.selectedOrderlineHas('Letter Tray', '10'); - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.check.validateButtonIsHighlighted(true); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.receiptIsThere(); - // letter tray has 10% tax (search SRC) - ReceiptScreen.check.totalAmountContains('55.0'); - ReceiptScreen.do.clickNextOrder(); +// press close button in receipt screen +ProductScreen.exec.addOrderline("Letter Tray", "10", "5"); +ProductScreen.check.selectedOrderlineHas("Letter Tray", "10"); +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.check.validateButtonIsHighlighted(true); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.receiptIsThere(); +// letter tray has 10% tax (search SRC) +ReceiptScreen.check.totalAmountContains("55.0"); +ReceiptScreen.do.clickNextOrder(); - // send email in receipt screen - ProductScreen.do.clickHomeCategory(); - ProductScreen.exec.addOrderline('Desk Pad', '6', '5', '30.0'); - ProductScreen.exec.addOrderline('Whiteboard Pen', '6', '6', '36.0'); - ProductScreen.exec.addOrderline('Monitor Stand', '6', '1', '6.0'); - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Cash'); - PaymentScreen.do.pressNumpad('7 0'); - PaymentScreen.check.remainingIs('2.0'); - PaymentScreen.do.pressNumpad('0'); - PaymentScreen.check.remainingIs('0.00'); - PaymentScreen.check.changeIs('628.0'); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.receiptIsThere(); - ReceiptScreen.check.totalAmountContains('72.0'); - ReceiptScreen.do.setEmail('test@receiptscreen.com'); - ReceiptScreen.do.clickSend(); - ReceiptScreen.check.emailIsSuccessful(); - ReceiptScreen.do.clickNextOrder(); +// send email in receipt screen +ProductScreen.do.clickHomeCategory(); +ProductScreen.exec.addOrderline("Desk Pad", "6", "5", "30.0"); +ProductScreen.exec.addOrderline("Whiteboard Pen", "6", "6", "36.0"); +ProductScreen.exec.addOrderline("Monitor Stand", "6", "1", "6.0"); +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickPaymentMethod("Cash"); +PaymentScreen.do.pressNumpad("7 0"); +PaymentScreen.check.remainingIs("2.0"); +PaymentScreen.do.pressNumpad("0"); +PaymentScreen.check.remainingIs("0.00"); +PaymentScreen.check.changeIs("628.0"); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.receiptIsThere(); +ReceiptScreen.check.totalAmountContains("72.0"); +ReceiptScreen.do.setEmail("test@receiptscreen.com"); +ReceiptScreen.do.clickSend(); +ReceiptScreen.check.emailIsSuccessful(); +ReceiptScreen.do.clickNextOrder(); - // order with tip - // check if tip amount is displayed - ProductScreen.exec.addOrderline('Desk Pad', '6', '5'); - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickTipButton(); - NumberPopup.do.pressNumpad('1'); - NumberPopup.check.inputShownIs('1'); - NumberPopup.do.clickConfirm(); - PaymentScreen.check.emptyPaymentlines('31.0'); - PaymentScreen.do.clickPaymentMethod('Cash'); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.receiptIsThere(); - ReceiptScreen.check.totalAmountContains('$ 30.00 + $ 1.00 tip'); - ReceiptScreen.do.clickNextOrder(); +// order with tip +// check if tip amount is displayed +ProductScreen.exec.addOrderline("Desk Pad", "6", "5"); +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickTipButton(); +NumberPopup.do.pressNumpad("1"); +NumberPopup.check.inputShownIs("1"); +NumberPopup.do.clickConfirm(); +PaymentScreen.check.emptyPaymentlines("31.0"); +PaymentScreen.do.clickPaymentMethod("Cash"); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.receiptIsThere(); +ReceiptScreen.check.totalAmountContains("$ 30.00 + $ 1.00 tip"); +ReceiptScreen.do.clickNextOrder(); - // Test customer note in receipt - ProductScreen.exec.addOrderline('Desk Pad', '1', '5'); - ProductScreen.exec.addCustomerNote('Test customer note') - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.customerNoteIsThere('Test customer note'); +// Test customer note in receipt +ProductScreen.exec.addOrderline("Desk Pad", "1", "5"); +ProductScreen.exec.addCustomerNote("Test customer note"); +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.customerNoteIsThere("Test customer note"); - Tour.register('ReceiptScreenTour', { test: true, url: '/pos/ui' }, getSteps()); -}); +Tour.register("ReceiptScreenTour", { test: true, url: "/pos/ui" }, getSteps()); diff --git a/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js index ac36c73ac83..81da2cf1dd9 100644 --- a/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js @@ -1,142 +1,140 @@ -odoo.define('point_of_sale.tour.TicketScreen', function (require) { - 'use strict'; +/** @odoo-module */ - const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); - const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); - const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); - const { PartnerListScreen } = require('point_of_sale.tour.PartnerListScreenTourMethods'); - const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); - const { ErrorPopup } = require('point_of_sale.tour.ErrorPopupTourMethods'); - const { Chrome } = require('point_of_sale.tour.ChromeTourMethods'); - const { getSteps, startSteps } = require('point_of_sale.tour.utils'); - var Tour = require('web_tour.tour'); +import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods"; +import { ReceiptScreen } from "@point_of_sale/../tests/tours/helpers/ReceiptScreenTourMethods"; +import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods"; +import { PartnerListScreen } from "@point_of_sale/../tests/tours/helpers/PartnerListScreenTourMethods"; +import { TicketScreen } from "@point_of_sale/../tests/tours/helpers/TicketScreenTourMethods"; +import { ErrorPopup } from "@point_of_sale/../tests/tours/helpers/ErrorPopupTourMethods"; +import { Chrome } from "@point_of_sale/../tests/tours/helpers/ChromeTourMethods"; +import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils"; +import Tour from "web_tour.tour"; - startSteps(); +startSteps(); - ProductScreen.do.confirmOpeningPopup(); - ProductScreen.do.clickHomeCategory(); - ProductScreen.exec.addOrderline('Desk Pad', '1', '2'); - ProductScreen.do.clickPartnerButton(); - ProductScreen.do.clickCustomer('Nicole Ford'); - Chrome.do.clickTicketButton(); - TicketScreen.check.nthRowContains(2, 'Nicole Ford'); - TicketScreen.do.clickNewTicket(); - ProductScreen.exec.addOrderline('Desk Pad', '1', '3'); - ProductScreen.do.clickPartnerButton(); - ProductScreen.do.clickCustomer('Brandon Freeman'); - ProductScreen.do.clickPayButton(); - PaymentScreen.check.isShown(); - Chrome.do.clickTicketButton(); - TicketScreen.check.nthRowContains(3, 'Brandon Freeman'); - TicketScreen.do.clickNewTicket(); - ProductScreen.exec.addOrderline('Desk Pad', '2', '4'); - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.isShown(); - Chrome.do.clickTicketButton(); - TicketScreen.check.nthRowContains(4, 'Receipt'); - TicketScreen.do.selectFilter('Receipt'); - TicketScreen.check.nthRowContains(2, 'Receipt'); - TicketScreen.do.selectFilter('Payment'); - TicketScreen.check.nthRowContains(2, 'Payment'); - TicketScreen.do.selectFilter('Ongoing'); - TicketScreen.check.nthRowContains(2, 'Ongoing'); - TicketScreen.do.selectFilter('All active orders'); - TicketScreen.check.nthRowContains(4, 'Receipt'); - TicketScreen.do.search('Customer', 'Nicole'); - TicketScreen.check.nthRowContains(2, 'Nicole'); - TicketScreen.do.search('Customer', 'Brandon'); - TicketScreen.check.nthRowContains(2, 'Brandon'); - TicketScreen.do.search('Receipt Number', '-0003'); - TicketScreen.check.nthRowContains(2, 'Receipt'); - // Close the TicketScreen to see the current order which is in ReceiptScreen. - // This is just to remove the search string in the search bar. - TicketScreen.do.clickDiscard(); - ReceiptScreen.check.isShown(); - // Open again the TicketScreen to check the Paid filter. - Chrome.do.clickTicketButton(); - TicketScreen.do.selectFilter('Paid'); - TicketScreen.check.nthRowContains(2, '-0003'); - // Pay the order that was in PaymentScreen. - TicketScreen.do.selectFilter('Payment'); - TicketScreen.do.selectOrder('-0002'); - PaymentScreen.do.clickPaymentMethod('Cash'); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.isShown(); - ReceiptScreen.do.clickNextOrder(); - ProductScreen.check.isShown(); - // Check that the Paid filter will show the 2 synced orders. - Chrome.do.clickTicketButton(); - TicketScreen.do.selectFilter('Paid'); - TicketScreen.check.nthRowContains(2, 'Brandon Freeman'); - TicketScreen.check.nthRowContains(3, '-0003'); - // Invoice order - TicketScreen.do.selectOrder('-0003'); - TicketScreen.check.orderWidgetIsNotEmpty(); - TicketScreen.do.clickControlButton('Invoice'); - Chrome.do.confirmPopup(); - PartnerListScreen.check.isShown(); - PartnerListScreen.do.clickPartner('Colleen Diaz'); - TicketScreen.check.partnerIs('Colleen Diaz'); - // Reprint receipt - TicketScreen.do.clickControlButton('Print Receipt'); - ReceiptScreen.check.isShown(); - ReceiptScreen.do.clickBack(); - // When going back, the ticket screen should be in its previous state. - TicketScreen.check.filterIs('Paid'); +ProductScreen.do.confirmOpeningPopup(); +ProductScreen.do.clickHomeCategory(); +ProductScreen.exec.addOrderline("Desk Pad", "1", "2"); +ProductScreen.do.clickPartnerButton(); +ProductScreen.do.clickCustomer("Nicole Ford"); +Chrome.do.clickTicketButton(); +TicketScreen.check.nthRowContains(2, "Nicole Ford"); +TicketScreen.do.clickNewTicket(); +ProductScreen.exec.addOrderline("Desk Pad", "1", "3"); +ProductScreen.do.clickPartnerButton(); +ProductScreen.do.clickCustomer("Brandon Freeman"); +ProductScreen.do.clickPayButton(); +PaymentScreen.check.isShown(); +Chrome.do.clickTicketButton(); +TicketScreen.check.nthRowContains(3, "Brandon Freeman"); +TicketScreen.do.clickNewTicket(); +ProductScreen.exec.addOrderline("Desk Pad", "2", "4"); +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.isShown(); +Chrome.do.clickTicketButton(); +TicketScreen.check.nthRowContains(4, "Receipt"); +TicketScreen.do.selectFilter("Receipt"); +TicketScreen.check.nthRowContains(2, "Receipt"); +TicketScreen.do.selectFilter("Payment"); +TicketScreen.check.nthRowContains(2, "Payment"); +TicketScreen.do.selectFilter("Ongoing"); +TicketScreen.check.nthRowContains(2, "Ongoing"); +TicketScreen.do.selectFilter("All active orders"); +TicketScreen.check.nthRowContains(4, "Receipt"); +TicketScreen.do.search("Customer", "Nicole"); +TicketScreen.check.nthRowContains(2, "Nicole"); +TicketScreen.do.search("Customer", "Brandon"); +TicketScreen.check.nthRowContains(2, "Brandon"); +TicketScreen.do.search("Receipt Number", "-0003"); +TicketScreen.check.nthRowContains(2, "Receipt"); +// Close the TicketScreen to see the current order which is in ReceiptScreen. +// This is just to remove the search string in the search bar. +TicketScreen.do.clickDiscard(); +ReceiptScreen.check.isShown(); +// Open again the TicketScreen to check the Paid filter. +Chrome.do.clickTicketButton(); +TicketScreen.do.selectFilter("Paid"); +TicketScreen.check.nthRowContains(2, "-0003"); +// Pay the order that was in PaymentScreen. +TicketScreen.do.selectFilter("Payment"); +TicketScreen.do.selectOrder("-0002"); +PaymentScreen.do.clickPaymentMethod("Cash"); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.isShown(); +ReceiptScreen.do.clickNextOrder(); +ProductScreen.check.isShown(); +// Check that the Paid filter will show the 2 synced orders. +Chrome.do.clickTicketButton(); +TicketScreen.do.selectFilter("Paid"); +TicketScreen.check.nthRowContains(2, "Brandon Freeman"); +TicketScreen.check.nthRowContains(3, "-0003"); +// Invoice order +TicketScreen.do.selectOrder("-0003"); +TicketScreen.check.orderWidgetIsNotEmpty(); +TicketScreen.do.clickControlButton("Invoice"); +Chrome.do.confirmPopup(); +PartnerListScreen.check.isShown(); +PartnerListScreen.do.clickPartner("Colleen Diaz"); +TicketScreen.check.partnerIs("Colleen Diaz"); +// Reprint receipt +TicketScreen.do.clickControlButton("Print Receipt"); +ReceiptScreen.check.isShown(); +ReceiptScreen.do.clickBack(); +// When going back, the ticket screen should be in its previous state. +TicketScreen.check.filterIs("Paid"); - // Test refund // - TicketScreen.do.clickDiscard(); - ProductScreen.check.isShown(); - ProductScreen.check.orderIsEmpty(); - ProductScreen.do.clickRefund(); - // Filter should be automatically 'Paid'. - TicketScreen.check.filterIs('Paid'); - TicketScreen.do.selectOrder('-0003'); - TicketScreen.check.partnerIs('Colleen Diaz'); - TicketScreen.do.clickOrderline('Desk Pad'); - TicketScreen.do.pressNumpad('3'); - // Error should show because 2 is more than the number - // that can be refunded. - ErrorPopup.do.clickConfirm(); - TicketScreen.do.clickDiscard(); - ProductScreen.check.isShown(); - ProductScreen.check.orderIsEmpty(); - ProductScreen.do.clickRefund(); - TicketScreen.do.selectOrder('-0003'); - TicketScreen.do.clickOrderline('Desk Pad'); - TicketScreen.do.pressNumpad('1'); - TicketScreen.check.toRefundTextContains('To Refund: 1.00'); - TicketScreen.do.confirmRefund(); - ProductScreen.check.isShown(); - ProductScreen.check.selectedOrderlineHas('Desk Pad', '-1.00'); - // Try changing the refund line to positive number. - // Error popup should show. - ProductScreen.do.pressNumpad('2'); - ErrorPopup.do.clickConfirm(); - // Change the refund line quantity to -3 -- not allowed - // so error popup. - ProductScreen.do.pressNumpad('+/- 3'); - ErrorPopup.do.clickConfirm(); - // Change the refund line quantity to -2 -- allowed. - ProductScreen.do.pressNumpad('+/- 2'); - ProductScreen.check.selectedOrderlineHas('Desk Pad', '-2.00'); - // Check if the amount being refunded changed to 2. - ProductScreen.do.clickRefund(); - TicketScreen.do.selectOrder('-0003'); - TicketScreen.check.toRefundTextContains('Refunding 2.00'); - TicketScreen.do.clickDiscard(); - // Pay the refund order. - ProductScreen.do.clickPayButton(); - PaymentScreen.do.clickPaymentMethod('Bank'); - PaymentScreen.do.clickValidate(); - ReceiptScreen.check.isShown(); - ReceiptScreen.do.clickNextOrder(); - // Check refunded quantity. - ProductScreen.do.clickRefund(); - TicketScreen.do.selectOrder('-0003'); - TicketScreen.check.refundedNoteContains('2.00 Refunded'); +// Test refund // +TicketScreen.do.clickDiscard(); +ProductScreen.check.isShown(); +ProductScreen.check.orderIsEmpty(); +ProductScreen.do.clickRefund(); +// Filter should be automatically 'Paid'. +TicketScreen.check.filterIs("Paid"); +TicketScreen.do.selectOrder("-0003"); +TicketScreen.check.partnerIs("Colleen Diaz"); +TicketScreen.do.clickOrderline("Desk Pad"); +TicketScreen.do.pressNumpad("3"); +// Error should show because 2 is more than the number +// that can be refunded. +ErrorPopup.do.clickConfirm(); +TicketScreen.do.clickDiscard(); +ProductScreen.check.isShown(); +ProductScreen.check.orderIsEmpty(); +ProductScreen.do.clickRefund(); +TicketScreen.do.selectOrder("-0003"); +TicketScreen.do.clickOrderline("Desk Pad"); +TicketScreen.do.pressNumpad("1"); +TicketScreen.check.toRefundTextContains("To Refund: 1.00"); +TicketScreen.do.confirmRefund(); +ProductScreen.check.isShown(); +ProductScreen.check.selectedOrderlineHas("Desk Pad", "-1.00"); +// Try changing the refund line to positive number. +// Error popup should show. +ProductScreen.do.pressNumpad("2"); +ErrorPopup.do.clickConfirm(); +// Change the refund line quantity to -3 -- not allowed +// so error popup. +ProductScreen.do.pressNumpad("+/- 3"); +ErrorPopup.do.clickConfirm(); +// Change the refund line quantity to -2 -- allowed. +ProductScreen.do.pressNumpad("+/- 2"); +ProductScreen.check.selectedOrderlineHas("Desk Pad", "-2.00"); +// Check if the amount being refunded changed to 2. +ProductScreen.do.clickRefund(); +TicketScreen.do.selectOrder("-0003"); +TicketScreen.check.toRefundTextContains("Refunding 2.00"); +TicketScreen.do.clickDiscard(); +// Pay the refund order. +ProductScreen.do.clickPayButton(); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.do.clickValidate(); +ReceiptScreen.check.isShown(); +ReceiptScreen.do.clickNextOrder(); +// Check refunded quantity. +ProductScreen.do.clickRefund(); +TicketScreen.do.selectOrder("-0003"); +TicketScreen.check.refundedNoteContains("2.00 Refunded"); - Tour.register('TicketScreenTour', { test: true, url: '/pos/ui' }, getSteps()); -}); +Tour.register("TicketScreenTour", { test: true, url: "/pos/ui" }, getSteps()); diff --git a/addons/point_of_sale/static/tests/tours/acceptance_tour.js b/addons/point_of_sale/static/tests/tours/acceptance_tour.js new file mode 100644 index 00000000000..4ef89b9469a --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/acceptance_tour.js @@ -0,0 +1,244 @@ +/** @odoo-module */ + +import Tour from "web_tour.tour"; + +function add_product_to_order(product_name) { + return [ + { + content: "buy " + product_name, + trigger: '.product-list .product-name:contains("' + product_name + '")', + }, + { + content: "the " + product_name + " have been added to the order", + trigger: '.order .product-name:contains("' + product_name + '")', + run: function () {}, + }, + ]; +} + +function set_fiscal_position_on_order(fp_name) { + return [ + { + content: "set fiscal position", + trigger: ".control-button.o_fiscal_position_button", + }, + { + content: "choose fiscal position " + fp_name + " to add to the order", + trigger: '.popups .popup .selection .selection-item:contains("' + fp_name + '")', + }, + { + content: "the fiscal position " + fp_name + " has been set to the order", + trigger: '.control-button.o_fiscal_position_button:contains("' + fp_name + '")', + run: function () {}, + }, + ]; +} + +function press_payment_numpad(val) { + return [ + { + content: `press ${val} on payment screen numpad`, + trigger: `.payment-numpad .input-button:contains("${val}"):visible`, + }, + ]; +} + +function press_product_numpad(val) { + return [ + { + content: `press ${val} on product screen numpad`, + trigger: `.numpad .input-button:contains("${val}"):visible`, + }, + ]; +} + +function selected_payment_has(name, val) { + return [ + { + content: `selected payment is ${name} and has ${val}`, + trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}")`, + extra_trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}") ~ .payment-amount:contains("${val}")`, + run: function () {}, + }, + ]; +} + +function selected_orderline_has({ product, price = null, quantity = null }) { + const result = []; + if (price !== null) { + result.push({ + content: `Selected line has product '${product}' and price '${price}'`, + trigger: `.order-container .orderlines .orderline.selected .product-name:contains("${product}") ~ span.price:contains("${price}")`, + run: function () {}, + }); + } + if (quantity !== null) { + result.push({ + content: `Selected line has product '${product}' and quantity '${quantity}'`, + trigger: `.order-container .orderlines .orderline.selected .product-name:contains('${product}') ~ .info-list .info em:contains('${quantity}')`, + run: function () {}, + }); + } + return result; +} + +function verify_order_total(total_str) { + return [ + { + content: "order total contains " + total_str, + trigger: '.order .total .value:contains("' + total_str + '")', + run: function () {}, // it's a check + }, + ]; +} + +function goto_payment_screen_and_select_payment_method() { + return [ + { + content: "go to payment screen", + trigger: ".button.pay", + }, + { + content: "pay with cash", + trigger: '.paymentmethod:contains("Cash")', + }, + ]; +} + +function finish_order() { + return [ + { + content: "validate the order", + trigger: ".payment-screen .button.next.highlight:visible", + }, + { + content: "verify that the order has been successfully sent to the backend", + trigger: ".js_connected:visible", + run: function () {}, + }, + { + content: "click Next Order", + trigger: ".receipt-screen .button.next.highlight:visible", + }, + { + content: "check if we left the receipt screen", + trigger: ".pos-content .screen:not(:has(.receipt-screen))", + run: function () {}, + }, + ]; +} + +var steps = [ + { + content: "waiting for loading to finish", + trigger: "body:not(:has(.loader))", + run: function () {}, + }, + { + // Leave category displayed by default + content: "click category switch", + trigger: ".breadcrumb-home", + }, +]; + +steps = steps.concat(add_product_to_order("Desk Organizer")); +steps = steps.concat(verify_order_total("5.10")); + +steps = steps.concat(add_product_to_order("Desk Organizer")); +steps = steps.concat(verify_order_total("10.20")); +steps = steps.concat(goto_payment_screen_and_select_payment_method()); + +/* add payment line of only 5.20 + status: + order-total := 10.20 + total-payment := 11.70 + expect: + remaining := 0.00 + change := 1.50 + */ +steps = steps.concat(press_payment_numpad("5")); +steps = steps.concat(selected_payment_has("Cash", "5.0")); +steps = steps.concat([ + { + content: "verify remaining", + trigger: '.payment-status-remaining .amount:contains("5.20")', + run: function () {}, + }, + { + content: "verify change", + trigger: '.payment-status-change .amount:contains("0.00")', + run: function () {}, + }, +]); + +/* make additional payment line of 6.50 + status: + order-total := 10.20 + total-payment := 11.70 + expect: + remaining := 0.00 + change := 1.50 + */ +steps = steps.concat([ + { + content: "pay with cash", + trigger: '.paymentmethod:contains("Cash")', + }, +]); +steps = steps.concat(selected_payment_has("Cash", "5.2")); +steps = steps.concat(press_payment_numpad("6")); +steps = steps.concat(selected_payment_has("Cash", "6.0")); +steps = steps.concat([ + { + content: "verify remaining", + trigger: '.payment-status-remaining .amount:contains("0.00")', + run: function () {}, + }, + { + content: "verify change", + trigger: '.payment-status-change .amount:contains("0.80")', + run: function () {}, + }, +]); + +steps = steps.concat(finish_order()); + +// test opw-672118 orderline subtotal rounding +steps = steps.concat(add_product_to_order("Desk Organizer")); +steps = steps.concat(selected_orderline_has({ product: "Desk Organizer", quantity: "1.0" })); +steps = steps.concat(press_product_numpad(".")); +steps = steps.concat( + selected_orderline_has({ product: "Desk Organizer", quantity: "0.0", price: "0.0" }) +); +steps = steps.concat(press_product_numpad("9")); +steps = steps.concat( + selected_orderline_has({ product: "Desk Organizer", quantity: "0.9", price: "4.59" }) +); +steps = steps.concat(press_product_numpad("9")); +steps = steps.concat( + selected_orderline_has({ product: "Desk Organizer", quantity: "0.99", price: "5.05" }) +); +steps = steps.concat(goto_payment_screen_and_select_payment_method()); +steps = steps.concat(selected_payment_has("Cash", "5.05")); +steps = steps.concat(finish_order()); + +// Test fiscal position one2many map (align with backend) +steps = steps.concat(add_product_to_order("Letter Tray")); +steps = steps.concat(selected_orderline_has({ product: "Letter Tray", quantity: "1.0" })); +steps = steps.concat(verify_order_total("5.28")); +steps = steps.concat(set_fiscal_position_on_order("FP-POS-2M")); +steps = steps.concat(verify_order_total("5.52")); + +steps = steps.concat([ + { + content: "open closing the Point of Sale frontend popup", + trigger: ".header-button", + }, + { + content: "close the Point of Sale frontend", + trigger: ".close-pos-popup .button:contains('Discard')", + run: function () {}, //it's a check, + }, +]); + +Tour.register("pos_basic_order", { test: true, url: "/pos/ui" }, steps); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js index 5dc93867ddd..c43325fbb55 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js @@ -1,29 +1,28 @@ -odoo.define('point_of_sale.tour.ChromeTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - confirmPopup() { - return [ - { - content: 'confirm popup', - trigger: '.popups .modal-dialog .button.confirm', - }, - ]; - } - clickTicketButton() { - return [ - { - trigger: '.pos-topheader .ticket-button', - }, - { - trigger: '.subwindow .ticket-screen', - run: () => {}, - }, - ]; - } +class Do { + confirmPopup() { + return [ + { + content: "confirm popup", + trigger: ".popups .modal-dialog .button.confirm", + }, + ]; } - - return createTourMethods('Chrome', Do); -}); + clickTicketButton() { + return [ + { + trigger: ".pos-topheader .ticket-button", + }, + { + trigger: ".subwindow .ticket-screen", + run: () => {}, + }, + ]; + } +} +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("Chrome", Do)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js index 3d8c07cf1f2..c7be9e04b9f 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js @@ -1,30 +1,30 @@ -odoo.define('point_of_sale.tour.ErrorPopupTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - clickConfirm() { - return [ - { - content: 'click confirm button', - trigger: '.popup-error .footer .cancel', - }, - ]; - } +class Do { + clickConfirm() { + return [ + { + content: "click confirm button", + trigger: ".popup-error .footer .cancel", + }, + ]; } +} - class Check { - isShown() { - return [ - { - content: 'error popup is shown', - trigger: '.modal-dialog .popup-error', - run: () => {}, - }, - ]; - } +class Check { + isShown() { + return [ + { + content: "error popup is shown", + trigger: ".modal-dialog .popup-error", + run: () => {}, + }, + ]; } +} - return createTourMethods('ErrorPopup', Do, Check); -}); +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("ErrorPopup", Do, Check)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js index c12d0d02edd..3a9724a9ed9 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js @@ -1,72 +1,72 @@ -odoo.define('point_of_sale.tour.NumberPopupTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - /** - * Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive - * fast inputs. Fast inputs is the case in tours. - * - * @param {String} keys space-separated input keys - */ - pressNumpad(keys) { - const numberChars = '0 1 2 3 4 5 6 7 8 9 C'.split(' '); - const modeButtons = '+1 +10 +2 +20 +5 +50'.split(' '); - const decimalSeparators = ', .'.split(' '); - function generateStep(key) { - let trigger; - if (numberChars.includes(key)) { - trigger = `.popup-numpad .number-char:contains("${key}")`; - } else if (modeButtons.includes(key)) { - trigger = `.popup-numpad .mode-button:contains("${key}")`; - } else if (key === 'Backspace') { - trigger = `.popup-numpad .numpad-backspace`; - } else if (decimalSeparators.includes(key)) { - trigger = `.popup-numpad .number-char.dot`; - } - return { - content: `'${key}' pressed in numpad`, - trigger, - }; +class Do { + /** + * Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive + * fast inputs. Fast inputs is the case in tours. + * + * @param {String} keys space-separated input keys + */ + pressNumpad(keys) { + const numberChars = "0 1 2 3 4 5 6 7 8 9 C".split(" "); + const modeButtons = "+1 +10 +2 +20 +5 +50".split(" "); + const decimalSeparators = ", .".split(" "); + function generateStep(key) { + let trigger; + if (numberChars.includes(key)) { + trigger = `.popup-numpad .number-char:contains("${key}")`; + } else if (modeButtons.includes(key)) { + trigger = `.popup-numpad .mode-button:contains("${key}")`; + } else if (key === "Backspace") { + trigger = `.popup-numpad .numpad-backspace`; + } else if (decimalSeparators.includes(key)) { + trigger = `.popup-numpad .number-char.dot`; } - return keys.split(' ').map(generateStep); - } - clickConfirm() { - return [ - { - content: 'click confirm button', - trigger: '.popup-number .footer .confirm', - }, - ]; + return { + content: `'${key}' pressed in numpad`, + trigger, + }; } + return keys.split(" ").map(generateStep); } - - class Check { - isShown() { - return [ - { - content: 'number popup is shown', - trigger: '.modal-dialog .popup-number', - run: () => {}, - }, - ]; - } - inputShownIs(val) { - return [ - { - content: 'number input element check', - trigger: '.modal-dialog .popup-number .popup-input', - run: () => {}, - }, - { - content: `input shown is '${val}'`, - trigger: `.modal-dialog .popup-number .popup-input:contains("${val}")`, - run: () => {}, - }, - ]; - } + clickConfirm() { + return [ + { + content: "click confirm button", + trigger: ".popup-number .footer .confirm", + }, + ]; } +} - return createTourMethods('NumberPopup', Do, Check); -}); +class Check { + isShown() { + return [ + { + content: "number popup is shown", + trigger: ".modal-dialog .popup-number", + run: () => {}, + }, + ]; + } + inputShownIs(val) { + return [ + { + content: "number input element check", + trigger: ".modal-dialog .popup-number .popup-input", + run: () => {}, + }, + { + content: `input shown is '${val}'`, + trigger: `.modal-dialog .popup-number .popup-input:contains("${val}")`, + run: () => {}, + }, + ]; + } +} + +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("NumberPopup", Do, Check)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/PartnerListScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/PartnerListScreenTourMethods.js index affaf7a4778..72b93a7c805 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/PartnerListScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/PartnerListScreenTourMethods.js @@ -1,32 +1,32 @@ -odoo.define('point_of_sale.tour.PartnerListScreenTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - clickPartner(name) { - return [ - { - content: `click partner '${name}' from partner list screen`, - trigger: `.partnerlist-screen .partner-list-contents .partner-line td:contains("${name}")`, - }, - ]; - } +class Do { + clickPartner(name) { + return [ + { + content: `click partner '${name}' from partner list screen`, + trigger: `.partnerlist-screen .partner-list-contents .partner-line td:contains("${name}")`, + }, + ]; } +} - class Check { - isShown() { - return [ - { - content: 'partner list screen is shown', - trigger: '.pos-content .partnerlist-screen', - run: () => {}, - }, - ]; - } +class Check { + isShown() { + return [ + { + content: "partner list screen is shown", + trigger: ".pos-content .partnerlist-screen", + run: () => {}, + }, + ]; } +} - class Execute {} +class Execute {} - return createTourMethods('PartnerListScreen', Do, Check, Execute); -}); +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("PartnerListScreen", Do, Check, Execute)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js index 89beb0fef07..98baeca8c61 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js @@ -1,207 +1,205 @@ -odoo.define('point_of_sale.tour.PaymentScreenTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - clickPaymentMethod(name) { - return [ - { - content: `click '${name}' payment method`, - trigger: `.paymentmethods .button.paymentmethod:contains("${name}")`, - }, - ]; - } +class Do { + clickPaymentMethod(name) { + return [ + { + content: `click '${name}' payment method`, + trigger: `.paymentmethods .button.paymentmethod:contains("${name}")`, + }, + ]; + } - /** - * Delete the paymentline having the given payment method name and amount. - * @param {String} name payment method - * @param {String} amount - */ - clickPaymentlineDelButton(name, amount) { - return [ - { - content: `delete ${name} paymentline with ${amount} amount`, - trigger: `.paymentlines .paymentline .payment-name:contains("${name}") ~ .delete-button`, - }, - ]; - } + /** + * Delete the paymentline having the given payment method name and amount. + * @param {String} name payment method + * @param {String} amount + */ + clickPaymentlineDelButton(name, amount) { + return [ + { + content: `delete ${name} paymentline with ${amount} amount`, + trigger: `.paymentlines .paymentline .payment-name:contains("${name}") ~ .delete-button`, + }, + ]; + } - clickEmailButton() { - return [ - { - content: `click email button`, - trigger: `.payment-buttons .js_email`, - }, - ]; - } + clickEmailButton() { + return [ + { + content: `click email button`, + trigger: `.payment-buttons .js_email`, + }, + ]; + } - clickInvoiceButton() { - return [{ content: 'click invoice button', trigger: '.payment-buttons .js_invoice' }]; - } + clickInvoiceButton() { + return [{ content: "click invoice button", trigger: ".payment-buttons .js_invoice" }]; + } - clickValidate() { - return [ - { - content: 'validate payment', - trigger: `.payment-screen .button.next.highlight`, - }, - ]; - } + clickValidate() { + return [ + { + content: "validate payment", + trigger: `.payment-screen .button.next.highlight`, + }, + ]; + } - /** - * Press the numpad in sequence based on the given space-separated keys. - * Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive - * fast inputs. Fast inputs is the case in tours. - * - * @param {String} keys space-separated numpad keys - */ - pressNumpad(keys) { - const numberChars = '. +/- 0 1 2 3 4 5 6 7 8 9'.split(' '); - const modeButtons = '+10 +20 +50'.split(' '); - function generateStep(key) { - let trigger; - if (numberChars.includes(key)) { - trigger = `.payment-numpad .number-char:contains("${key}")`; - } else if (modeButtons.includes(key)) { - trigger = `.payment-numpad .mode-button:contains("${key}")`; - } else if (key === 'Backspace') { - trigger = `.payment-numpad .number-char img[alt="Backspace"]`; - } - return { - content: `'${key}' pressed in payment numpad`, - trigger, - }; + /** + * Press the numpad in sequence based on the given space-separated keys. + * Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive + * fast inputs. Fast inputs is the case in tours. + * + * @param {String} keys space-separated numpad keys + */ + pressNumpad(keys) { + const numberChars = ". +/- 0 1 2 3 4 5 6 7 8 9".split(" "); + const modeButtons = "+10 +20 +50".split(" "); + function generateStep(key) { + let trigger; + if (numberChars.includes(key)) { + trigger = `.payment-numpad .number-char:contains("${key}")`; + } else if (modeButtons.includes(key)) { + trigger = `.payment-numpad .mode-button:contains("${key}")`; + } else if (key === "Backspace") { + trigger = `.payment-numpad .number-char img[alt="Backspace"]`; } - return keys.split(' ').map(generateStep); - } - - clickBack() { - return [ - { - content: 'click back button', - trigger: '.payment-screen .button.back', - }, - ]; - } - - clickTipButton() { - return [ - { - trigger: '.payment-screen .button.js_tip', - }, - ] + return { + content: `'${key}' pressed in payment numpad`, + trigger, + }; } + return keys.split(" ").map(generateStep); } - class Check { - isShown() { - return [ - { - content: 'payment screen is shown', - trigger: '.pos .payment-screen', - run: () => {}, - }, - ]; - } - /** - * Check if change is the provided amount. - * @param {String} amount - */ - changeIs(amount) { - return [ - { - content: `change is ${amount}`, - trigger: `.payment-status-change .amount:contains("${amount}")`, - run: () => {}, - }, - ]; - } - - /** - * Check if the remaining is the provided amount. - * @param {String} amount - */ - remainingIs(amount) { - return [ - { - content: `remaining amount is ${amount}`, - trigger: `.payment-status-remaining .amount:contains("${amount}")`, - run: () => {}, - }, - ]; - } - - /** - * Check if validate button is highlighted. - * @param {Boolean} isHighlighted - */ - validateButtonIsHighlighted(isHighlighted = true) { - return [ - { - content: `validate button is ${ - isHighlighted ? 'highlighted' : 'not highligted' - }`, - trigger: isHighlighted - ? `.payment-screen .button.next.highlight` - : `.payment-screen .button.next:not(:has(.highlight))`, - run: () => {}, - }, - ]; - } - - /** - * Check if the paymentlines are empty. Also provide the amount to pay. - * @param {String} amountToPay - */ - emptyPaymentlines(amountToPay) { - return [ - { - content: `there are no paymentlines`, - trigger: `.paymentlines-empty`, - run: () => {}, - }, - { - content: `amount to pay is '${amountToPay}'`, - trigger: `.paymentlines-empty .total:contains("${amountToPay}")`, - run: () => {}, - }, - ]; - } - - /** - * Check if the selected paymentline has the given payment method and amount. - * @param {String} paymentMethodName - * @param {String} amount - */ - selectedPaymentlineHas(paymentMethodName, amount) { - return [ - { - content: `line paid via '${paymentMethodName}' is selected`, - trigger: `.paymentlines .paymentline.selected .payment-name:contains("${paymentMethodName}")`, - run: () => {}, - }, - { - content: `amount tendered in the line is '${amount}'`, - trigger: `.paymentlines .paymentline.selected .payment-amount:contains("${amount}")`, - run: () => {}, - }, - ]; - } + clickBack() { + return [ + { + content: "click back button", + trigger: ".payment-screen .button.back", + }, + ]; } - class Execute { - pay(method, amount) { - const steps = []; - steps.push(...this._do.clickPaymentMethod(method)); - for (let char of amount.split('')) { - steps.push(...this._do.pressNumpad(char)); - } - steps.push(...this._check.validateButtonIsHighlighted()); - steps.push(...this._do.clickValidate()); - return steps; - } + clickTipButton() { + return [ + { + trigger: ".payment-screen .button.js_tip", + }, + ]; + } +} + +class Check { + isShown() { + return [ + { + content: "payment screen is shown", + trigger: ".pos .payment-screen", + run: () => {}, + }, + ]; + } + /** + * Check if change is the provided amount. + * @param {String} amount + */ + changeIs(amount) { + return [ + { + content: `change is ${amount}`, + trigger: `.payment-status-change .amount:contains("${amount}")`, + run: () => {}, + }, + ]; } - return createTourMethods('PaymentScreen', Do, Check, Execute); -}); + /** + * Check if the remaining is the provided amount. + * @param {String} amount + */ + remainingIs(amount) { + return [ + { + content: `remaining amount is ${amount}`, + trigger: `.payment-status-remaining .amount:contains("${amount}")`, + run: () => {}, + }, + ]; + } + + /** + * Check if validate button is highlighted. + * @param {Boolean} isHighlighted + */ + validateButtonIsHighlighted(isHighlighted = true) { + return [ + { + content: `validate button is ${isHighlighted ? "highlighted" : "not highligted"}`, + trigger: isHighlighted + ? `.payment-screen .button.next.highlight` + : `.payment-screen .button.next:not(:has(.highlight))`, + run: () => {}, + }, + ]; + } + + /** + * Check if the paymentlines are empty. Also provide the amount to pay. + * @param {String} amountToPay + */ + emptyPaymentlines(amountToPay) { + return [ + { + content: `there are no paymentlines`, + trigger: `.paymentlines-empty`, + run: () => {}, + }, + { + content: `amount to pay is '${amountToPay}'`, + trigger: `.paymentlines-empty .total:contains("${amountToPay}")`, + run: () => {}, + }, + ]; + } + + /** + * Check if the selected paymentline has the given payment method and amount. + * @param {String} paymentMethodName + * @param {String} amount + */ + selectedPaymentlineHas(paymentMethodName, amount) { + return [ + { + content: `line paid via '${paymentMethodName}' is selected`, + trigger: `.paymentlines .paymentline.selected .payment-name:contains("${paymentMethodName}")`, + run: () => {}, + }, + { + content: `amount tendered in the line is '${amount}'`, + trigger: `.paymentlines .paymentline.selected .payment-amount:contains("${amount}")`, + run: () => {}, + }, + ]; + } +} + +class Execute { + pay(method, amount) { + const steps = []; + steps.push(...this._do.clickPaymentMethod(method)); + for (const char of amount.split("")) { + steps.push(...this._do.pressNumpad(char)); + } + steps.push(...this._check.validateButtonIsHighlighted()); + steps.push(...this._do.clickValidate()); + return steps; + } +} + +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("PaymentScreen", Do, Check, Execute)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js index 19fcbb2b48d..18ce019f8f6 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js @@ -1,77 +1,77 @@ -odoo.define('point_of_sale.tour.ProductConfiguratorTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - pickRadio(name) { - return [ - { - content: `picking radio attribute with name ${name}`, - trigger: `.product-configurator-popup .attribute-name-cell label[name='${name}']`, - }, - ]; - } - - pickSelect(name) { - return [ - { - content: `picking select attribute with name ${name}`, - trigger: `.product-configurator-popup .configurator_select:has(option:contains('${name}'))`, - run: `text ${name}`, - }, - ]; - } - - pickColor(name) { - return [ - { - content: `picking color attribute with name ${name}`, - trigger: `.product-configurator-popup .configurator_color[data-color='${name}']`, - }, - ]; - } - - fillCustomAttribute(value) { - return [ - { - content: `filling custom attribute with value ${value}`, - trigger: `.product-configurator-popup .custom_value`, - run: `text ${value}`, - }, - ]; - } - - confirmAttributes() { - return [ - { - content: `confirming product configuration`, - trigger: `.product-configurator-popup .button.confirm`, - }, - ]; - } - - cancelAttributes() { - return [ - { - content: `canceling product configuration`, - trigger: `.product-configurator-popup .button.cancel`, - }, - ]; - } +class Do { + pickRadio(name) { + return [ + { + content: `picking radio attribute with name ${name}`, + trigger: `.product-configurator-popup .attribute-name-cell label[name='${name}']`, + }, + ]; } - class Check { - isShown() { - return [ - { - content: 'product configurator is shown', - trigger: '.product-configurator-popup:not(:has(.oe_hidden))', - run: () => {}, - }, - ]; - } + pickSelect(name) { + return [ + { + content: `picking select attribute with name ${name}`, + trigger: `.product-configurator-popup .configurator_select:has(option:contains('${name}'))`, + run: `text ${name}`, + }, + ]; } - return createTourMethods('ProductConfigurator', Do, Check); -}); + pickColor(name) { + return [ + { + content: `picking color attribute with name ${name}`, + trigger: `.product-configurator-popup .configurator_color[data-color='${name}']`, + }, + ]; + } + + fillCustomAttribute(value) { + return [ + { + content: `filling custom attribute with value ${value}`, + trigger: `.product-configurator-popup .custom_value`, + run: `text ${value}`, + }, + ]; + } + + confirmAttributes() { + return [ + { + content: `confirming product configuration`, + trigger: `.product-configurator-popup .button.confirm`, + }, + ]; + } + + cancelAttributes() { + return [ + { + content: `canceling product configuration`, + trigger: `.product-configurator-popup .button.cancel`, + }, + ]; + } +} + +class Check { + isShown() { + return [ + { + content: "product configurator is shown", + trigger: ".product-configurator-popup:not(:has(.oe_hidden))", + run: () => {}, + }, + ]; + } +} + +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("ProductConfigurator", Do, Check)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js index f082dd8bf5a..d9123790487 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js @@ -1,293 +1,295 @@ -odoo.define('point_of_sale.tour.ProductScreenTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); - const { TextAreaPopup } = require('point_of_sale.tour.TextAreaPopupTourMethods'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; +import { TextAreaPopup } from "@point_of_sale/../tests/tours/helpers/TextAreaPopupTourMethods"; - class Do { - clickDisplayedProduct(name) { - return [ - { - content: `click product '${name}'`, - trigger: `.product-list .product-name:contains("${name}")`, - }, - ]; - } - - clickOrderline(name, quantity) { - return [ - { - content: `selecting orderline with product '${name}' and quantity '${quantity}'`, - trigger: `.order .orderline:not(:has(.selected)) .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, - }, - { - content: `orderline with product '${name}' and quantity '${quantity}' has been selected`, - trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, - run: () => {}, - }, - ]; - } - - clickSubcategory(name) { - return [ - { - content: `selecting '${name}' subcategory`, - trigger: `.products-widget > .products-widget-control .category-simple-button:contains("${name}")`, - }, - { - content: `'${name}' subcategory selected`, - trigger: `.breadcrumbs .breadcrumb-button:contains("${name}")`, - run: () => {}, - }, - ]; - } - - clickHomeCategory() { - return [ - { - content: `click Home subcategory`, - trigger: `.breadcrumbs .breadcrumb-home`, - }, - ]; - } - - /** - * Press the numpad in sequence based on the given space-separated keys. - * NOTE: Maximum of 2 characters because NumberBuffer only allows 2 consecutive - * fast inputs. Fast inputs is the case in tours. - * - * @param {String} keys space-separated numpad keys - */ - pressNumpad(keys) { - const numberChars = '. 0 1 2 3 4 5 6 7 8 9'.split(' '); - const modeButtons = 'Qty Price Disc'.split(' '); - function generateStep(key) { - let trigger; - if (numberChars.includes(key)) { - trigger = `.numpad .number-char:contains("${key}")`; - } else if (modeButtons.includes(key)) { - trigger = `.numpad .mode-button:contains("${key}")`; - } else if (key === 'Backspace') { - trigger = `.numpad .numpad-backspace`; - } else if (key === '+/-') { - trigger = `.numpad .numpad-minus`; - } - return { - content: `'${key}' pressed in product screen numpad`, - trigger, - }; - } - return keys.split(' ').map(generateStep); - } - - clickPayButton(shouldCheck = true) { - const steps = [{ content: 'click pay button', trigger: '.product-screen .actionpad .button.pay' }]; - if (shouldCheck) { - steps.push({ - content: 'now in payment screen', - trigger: '.pos-content .payment-screen', - run: () => {}, - }); - } - return steps; - } - - clickPartnerButton() { - return [ - { content: 'click customer button', trigger: '.actionpad .button.set-partner' }, - { - content: 'partner screen is shown', - trigger: '.pos-content .partnerlist-screen', - run: () => {}, - }, - ]; - } - - clickCustomer(name) { - return [ - { - content: `select customer '${name}'`, - trigger: `.partnerlist-screen .partner-line td:contains("${name}")`, - }, - ]; - } - - clickOrderlineCustomerNoteButton() { - return [ - { - content: 'click customer note button', - trigger: '.control-buttons .control-button span:contains("Customer Note")', - } - ] - } - clickRefund() { - return [ - { - trigger: '.control-button:contains("Refund")', - }, - ]; - } - confirmOpeningPopup() { - return [{ trigger: '.opening-cash-control .button:contains("Open session")' }]; - } - clickPricelistButton() { - return [{ trigger: '.o_pricelist_button' }]; - } - selectPriceList(name) { - return [ - { - content: `select price list '${name}'`, - trigger: `.selection-item:contains("${name}")`, - }, - ]; - } +class Do { + clickDisplayedProduct(name) { + return [ + { + content: `click product '${name}'`, + trigger: `.product-list .product-name:contains("${name}")`, + }, + ]; } - class Check { - isShown() { - return [ - { - content: 'product screen is shown', - trigger: '.product-screen', - run: () => {}, - }, - ]; - } - selectedOrderlineHas(name, quantity, price) { - const res = [ - { - // check first if the order widget is there and has orderlines - content: 'order widget has orderlines', - trigger: '.order .orderlines', - run: () => {}, - }, - { - content: `'${name}' is selected`, - trigger: `.order .orderline.selected .product-name:contains("${name}")`, - run: function () {}, // it's a check - }, - ]; - if (quantity) { - res.push({ - content: `selected line has ${quantity} quantity`, - trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, - run: function () {}, // it's a check - }); - } - if (price) { - res.push({ - content: `selected line has total price of ${price}`, - trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .price:contains("${price}")`, - run: function () {}, // it's a check - }); - } - return res; - } - orderIsEmpty() { - return [ - { - content: `order is empty`, - trigger: `.order .order-empty`, - run: () => {}, - }, - ]; - } - - productIsDisplayed(name) { - return [ - { - content: `'${name}' should be displayed`, - trigger: `.product-list .product-name:contains("${name}")`, - run: () => {}, - }, - ]; - } - totalAmountIs(amount) { - return [ - { - content: `order total amount is '${amount}'`, - trigger: `.order-container .order .summary .value:contains("${amount}")`, - run: () => {}, - } - ] - } - modeIsActive(mode) { - return [ - { - content: `'${mode}' is active`, - trigger: `.numpad button.selected-mode:contains('${mode}')`, - run: function () {}, - }, - ]; - } - orderlineHasCustomerNote(name, quantity, note) { - return [ - { - content: `line has ${quantity} quantity`, - trigger: `.order .orderline .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, - run: function () {}, // it's a check - }, - { - content: `line has '${note}' as customer note`, - trigger: `.order .orderline .info-list .orderline-note:contains("${note}")`, - run: function () {}, // it's a check - }, - ] - } + clickOrderline(name, quantity) { + return [ + { + content: `selecting orderline with product '${name}' and quantity '${quantity}'`, + trigger: `.order .orderline:not(:has(.selected)) .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, + }, + { + content: `orderline with product '${name}' and quantity '${quantity}' has been selected`, + trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, + run: () => {}, + }, + ]; } - class Execute { - /** - * Create an orderline for the given `productName` and `quantity`. - * - If `unitPrice` is provided, price of the product of the created line - * is changed to that value. - * - If `expectedTotal` is provided, the created orderline (which is the currently - * selected orderline) is checked if it contains the correct quantity and total - * price. - * - * @param {string} productName - * @param {string} quantity - * @param {string} unitPrice - * @param {string} expectedTotal - */ - addOrderline(productName, quantity, unitPrice = undefined, expectedTotal = undefined) { - const res = this._do.clickDisplayedProduct(productName); - if (unitPrice) { - res.push(...this._do.pressNumpad('Price')); - res.push(...this._check.modeIsActive('Price')); - res.push(...this._do.pressNumpad(unitPrice.toString().split('').join(' '))); - res.push(...this._do.pressNumpad('Qty')); - res.push(...this._check.modeIsActive('Qty')); - } - for (let char of (quantity.toString() == '1' ? '' : quantity.toString())) { - if ('.0123456789'.includes(char)) { - res.push(...this._do.pressNumpad(char)); - } else if ('-'.includes(char)) { - res.push(...this._do.pressNumpad('+/-')); - } - } - if (expectedTotal) { - res.push(...this._check.selectedOrderlineHas(productName, quantity, expectedTotal)); - } else { - res.push(...this._check.selectedOrderlineHas(productName, quantity)); - } - return res; - } - addMultiOrderlines(...list) { - const steps = []; - for (let [product, qty, price] of list) { - steps.push(...this.addOrderline(product, qty, price)); - } - return steps; - } - addCustomerNote(note) { - const res = []; - res.push(...this._do.clickOrderlineCustomerNoteButton()); - res.push(...TextAreaPopup._do.inputText(note)); - res.push(...TextAreaPopup._do.clickConfirm()); - return res; - } + clickSubcategory(name) { + return [ + { + content: `selecting '${name}' subcategory`, + trigger: `.products-widget > .products-widget-control .category-simple-button:contains("${name}")`, + }, + { + content: `'${name}' subcategory selected`, + trigger: `.breadcrumbs .breadcrumb-button:contains("${name}")`, + run: () => {}, + }, + ]; } - return createTourMethods('ProductScreen', Do, Check, Execute); -}); + clickHomeCategory() { + return [ + { + content: `click Home subcategory`, + trigger: `.breadcrumbs .breadcrumb-home`, + }, + ]; + } + + /** + * Press the numpad in sequence based on the given space-separated keys. + * NOTE: Maximum of 2 characters because NumberBuffer only allows 2 consecutive + * fast inputs. Fast inputs is the case in tours. + * + * @param {String} keys space-separated numpad keys + */ + pressNumpad(keys) { + const numberChars = ". 0 1 2 3 4 5 6 7 8 9".split(" "); + const modeButtons = "Qty Price Disc".split(" "); + function generateStep(key) { + let trigger; + if (numberChars.includes(key)) { + trigger = `.numpad .number-char:contains("${key}")`; + } else if (modeButtons.includes(key)) { + trigger = `.numpad .mode-button:contains("${key}")`; + } else if (key === "Backspace") { + trigger = `.numpad .numpad-backspace`; + } else if (key === "+/-") { + trigger = `.numpad .numpad-minus`; + } + return { + content: `'${key}' pressed in product screen numpad`, + trigger, + }; + } + return keys.split(" ").map(generateStep); + } + + clickPayButton(shouldCheck = true) { + const steps = [ + { content: "click pay button", trigger: ".product-screen .actionpad .button.pay" }, + ]; + if (shouldCheck) { + steps.push({ + content: "now in payment screen", + trigger: ".pos-content .payment-screen", + run: () => {}, + }); + } + return steps; + } + + clickPartnerButton() { + return [ + { content: "click customer button", trigger: ".actionpad .button.set-partner" }, + { + content: "partner screen is shown", + trigger: ".pos-content .partnerlist-screen", + run: () => {}, + }, + ]; + } + + clickCustomer(name) { + return [ + { + content: `select customer '${name}'`, + trigger: `.partnerlist-screen .partner-line td:contains("${name}")`, + }, + ]; + } + + clickOrderlineCustomerNoteButton() { + return [ + { + content: "click customer note button", + trigger: '.control-buttons .control-button span:contains("Customer Note")', + }, + ]; + } + clickRefund() { + return [ + { + trigger: '.control-button:contains("Refund")', + }, + ]; + } + confirmOpeningPopup() { + return [{ trigger: '.opening-cash-control .button:contains("Open session")' }]; + } + clickPricelistButton() { + return [{ trigger: ".o_pricelist_button" }]; + } + selectPriceList(name) { + return [ + { + content: `select price list '${name}'`, + trigger: `.selection-item:contains("${name}")`, + }, + ]; + } +} + +class Check { + isShown() { + return [ + { + content: "product screen is shown", + trigger: ".product-screen", + run: () => {}, + }, + ]; + } + selectedOrderlineHas(name, quantity, price) { + const res = [ + { + // check first if the order widget is there and has orderlines + content: "order widget has orderlines", + trigger: ".order .orderlines", + run: () => {}, + }, + { + content: `'${name}' is selected`, + trigger: `.order .orderline.selected .product-name:contains("${name}")`, + run: function () {}, // it's a check + }, + ]; + if (quantity) { + res.push({ + content: `selected line has ${quantity} quantity`, + trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, + run: function () {}, // it's a check + }); + } + if (price) { + res.push({ + content: `selected line has total price of ${price}`, + trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .price:contains("${price}")`, + run: function () {}, // it's a check + }); + } + return res; + } + orderIsEmpty() { + return [ + { + content: `order is empty`, + trigger: `.order .order-empty`, + run: () => {}, + }, + ]; + } + + productIsDisplayed(name) { + return [ + { + content: `'${name}' should be displayed`, + trigger: `.product-list .product-name:contains("${name}")`, + run: () => {}, + }, + ]; + } + totalAmountIs(amount) { + return [ + { + content: `order total amount is '${amount}'`, + trigger: `.order-container .order .summary .value:contains("${amount}")`, + run: () => {}, + }, + ]; + } + modeIsActive(mode) { + return [ + { + content: `'${mode}' is active`, + trigger: `.numpad button.selected-mode:contains('${mode}')`, + run: function () {}, + }, + ]; + } + orderlineHasCustomerNote(name, quantity, note) { + return [ + { + content: `line has ${quantity} quantity`, + trigger: `.order .orderline .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, + run: function () {}, // it's a check + }, + { + content: `line has '${note}' as customer note`, + trigger: `.order .orderline .info-list .orderline-note:contains("${note}")`, + run: function () {}, // it's a check + }, + ]; + } +} + +class Execute { + /** + * Create an orderline for the given `productName` and `quantity`. + * - If `unitPrice` is provided, price of the product of the created line + * is changed to that value. + * - If `expectedTotal` is provided, the created orderline (which is the currently + * selected orderline) is checked if it contains the correct quantity and total + * price. + * + * @param {string} productName + * @param {string} quantity + * @param {string} unitPrice + * @param {string} expectedTotal + */ + addOrderline(productName, quantity, unitPrice = undefined, expectedTotal = undefined) { + const res = this._do.clickDisplayedProduct(productName); + if (unitPrice) { + res.push(...this._do.pressNumpad("Price")); + res.push(...this._check.modeIsActive("Price")); + res.push(...this._do.pressNumpad(unitPrice.toString().split("").join(" "))); + res.push(...this._do.pressNumpad("Qty")); + res.push(...this._check.modeIsActive("Qty")); + } + for (const char of quantity.toString() == "1" ? "" : quantity.toString()) { + if (".0123456789".includes(char)) { + res.push(...this._do.pressNumpad(char)); + } else if ("-".includes(char)) { + res.push(...this._do.pressNumpad("+/-")); + } + } + if (expectedTotal) { + res.push(...this._check.selectedOrderlineHas(productName, quantity, expectedTotal)); + } else { + res.push(...this._check.selectedOrderlineHas(productName, quantity)); + } + return res; + } + addMultiOrderlines(...list) { + const steps = []; + for (const [product, qty, price] of list) { + steps.push(...this.addOrderline(product, qty, price)); + } + return steps; + } + addCustomerNote(note) { + const res = []; + res.push(...this._do.clickOrderlineCustomerNoteButton()); + res.push(...TextAreaPopup._do.inputText(note)); + res.push(...TextAreaPopup._do.clickConfirm()); + return res; + } +} + +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("ProductScreen", Do, Check, Execute)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js index db8e9dadedf..5aa24fb4dda 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js @@ -1,94 +1,94 @@ -odoo.define('point_of_sale.tour.ReceiptScreenTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - clickNextOrder() { - return [ - { - content: 'go to next screen', - trigger: '.receipt-screen .button.next.highlight', - }, - ]; - } - setEmail(email) { - return [ - { - trigger: '.receipt-screen .input-email input', - run: `text ${email}`, - }, - ]; - } - clickSend(isHighlighted = true) { - return [ - { - trigger: `.receipt-screen .input-email .send${isHighlighted ? '.highlight' : ''}`, - }, - ]; - } - clickBack() { - return [ - { - trigger: '.receipt-screen .button.back', - }, - ]; - } +class Do { + clickNextOrder() { + return [ + { + content: "go to next screen", + trigger: ".receipt-screen .button.next.highlight", + }, + ]; + } + setEmail(email) { + return [ + { + trigger: ".receipt-screen .input-email input", + run: `text ${email}`, + }, + ]; + } + clickSend(isHighlighted = true) { + return [ + { + trigger: `.receipt-screen .input-email .send${isHighlighted ? ".highlight" : ""}`, + }, + ]; + } + clickBack() { + return [ + { + trigger: ".receipt-screen .button.back", + }, + ]; + } +} + +class Check { + isShown() { + return [ + { + content: "receipt screen is shown", + trigger: ".pos .receipt-screen", + run: () => {}, + }, + ]; } - class Check { - isShown() { - return [ - { - content: 'receipt screen is shown', - trigger: '.pos .receipt-screen', - run: () => {}, - }, - ]; - } - - receiptIsThere() { - return [ - { - content: 'there should be the receipt', - trigger: '.receipt-screen .pos-receipt', - run: () => {}, - }, - ]; - } - - totalAmountContains(value) { - return [ - { - trigger: `.receipt-screen .top-content h1:contains("${value}")`, - run: () => {}, - }, - ]; - } - - emailIsSuccessful() { - return [ - { - trigger: `.receipt-screen .notice .successful`, - run: () => {}, - }, - ]; - } - - customerNoteIsThere(note) { - return [ - { - trigger: `.receipt-screen .orderlines .pos-receipt-left-padding:contains("${note}")` - } - ] - } + receiptIsThere() { + return [ + { + content: "there should be the receipt", + trigger: ".receipt-screen .pos-receipt", + run: () => {}, + }, + ]; } - class Execute { - nextOrder() { - return [...this._check.isShown(), ...this._do.clickNextOrder()]; - } + totalAmountContains(value) { + return [ + { + trigger: `.receipt-screen .top-content h1:contains("${value}")`, + run: () => {}, + }, + ]; } - return createTourMethods('ReceiptScreen', Do, Check, Execute); -}); + emailIsSuccessful() { + return [ + { + trigger: `.receipt-screen .notice .successful`, + run: () => {}, + }, + ]; + } + + customerNoteIsThere(note) { + return [ + { + trigger: `.receipt-screen .orderlines .pos-receipt-left-padding:contains("${note}")`, + }, + ]; + } +} + +class Execute { + nextOrder() { + return [...this._check.isShown(), ...this._do.clickNextOrder()]; + } +} + +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("ReceiptScreen", Do, Check, Execute)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js index bbe4fc2d369..b750405a0ec 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js @@ -1,39 +1,39 @@ -odoo.define('point_of_sale.tour.SelectionPopupTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - clickItem(name) { - return [ - { - content: `click selection '${name}'`, - trigger: `.selection-item:contains("${name}")`, - }, - ]; - } +class Do { + clickItem(name) { + return [ + { + content: `click selection '${name}'`, + trigger: `.selection-item:contains("${name}")`, + }, + ]; } +} - class Check { - hasSelectionItem(name) { - return [ - { - content: `selection popup has '${name}'`, - trigger: `.selection-item:contains("${name}")`, - run: () => {}, - }, - ]; - } - isShown() { - return [ - { - content: 'selection popup is shown', - trigger: '.modal-dialog .popup-selection', - run: () => {}, - }, - ]; - } +class Check { + hasSelectionItem(name) { + return [ + { + content: `selection popup has '${name}'`, + trigger: `.selection-item:contains("${name}")`, + run: () => {}, + }, + ]; } + isShown() { + return [ + { + content: "selection popup is shown", + trigger: ".modal-dialog .popup-selection", + run: () => {}, + }, + ]; + } +} - return createTourMethods('SelectionPopup', Do, Check); -}); +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("SelectionPopup", Do, Check)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/TextAreaPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/TextAreaPopupTourMethods.js index 94f001808c6..19023920112 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/TextAreaPopupTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/TextAreaPopupTourMethods.js @@ -1,39 +1,39 @@ -odoo.define('point_of_sale.tour.TextAreaPopupTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - inputText(val) { - return [ - { - content: `input text '${val}'`, - trigger: `.modal-dialog .popup-textarea textarea`, - run: `text ${val}`, - }, - ]; - } - clickConfirm() { - return [ - { - content: 'confirm text input popup', - trigger: '.modal-dialog .confirm', - }, - ]; - } +class Do { + inputText(val) { + return [ + { + content: `input text '${val}'`, + trigger: `.modal-dialog .popup-textarea textarea`, + run: `text ${val}`, + }, + ]; } - - class Check { - isShown() { - return [ - { - content: 'text input popup is shown', - trigger: '.modal-dialog .popup-textarea', - run: () => {}, - }, - ]; - } + clickConfirm() { + return [ + { + content: "confirm text input popup", + trigger: ".modal-dialog .confirm", + }, + ]; } +} - return createTourMethods('TextAreaPopup', Do, Check); -}); +class Check { + isShown() { + return [ + { + content: "text input popup is shown", + trigger: ".modal-dialog .popup-textarea", + run: () => {}, + }, + ]; + } +} + +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("TextAreaPopup", Do, Check)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/TextInputPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/TextInputPopupTourMethods.js index 4c98f1d3147..bfac5804b72 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/TextInputPopupTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/TextInputPopupTourMethods.js @@ -1,39 +1,39 @@ -odoo.define('point_of_sale.tour.TextInputPopupTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - inputText(val) { - return [ - { - content: `input text '${val}'`, - trigger: `.modal-dialog .popup-textinput input`, - run: `text ${val}`, - }, - ]; - } - clickConfirm() { - return [ - { - content: 'confirm text input popup', - trigger: '.modal-dialog .confirm', - }, - ]; - } +class Do { + inputText(val) { + return [ + { + content: `input text '${val}'`, + trigger: `.modal-dialog .popup-textinput input`, + run: `text ${val}`, + }, + ]; } - - class Check { - isShown() { - return [ - { - content: 'text input popup is shown', - trigger: '.modal-dialog .popup-textinput', - run: () => {}, - }, - ]; - } + clickConfirm() { + return [ + { + content: "confirm text input popup", + trigger: ".modal-dialog .confirm", + }, + ]; } +} - return createTourMethods('TextInputPopup', Do, Check); -}); +class Check { + isShown() { + return [ + { + content: "text input popup is shown", + trigger: ".modal-dialog .popup-textinput", + run: () => {}, + }, + ]; + } +} + +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("TextInputPopup", Do, Check)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js index 297042bf397..e19bac32957 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js @@ -1,195 +1,195 @@ -odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) { - 'use strict'; +/** @odoo-module */ - const { createTourMethods } = require('point_of_sale.tour.utils'); +import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils"; - class Do { - clickNewTicket() { - return [{ trigger: '.ticket-screen .highlight' }]; - } - clickDiscard() { - return [{ trigger: '.ticket-screen button.discard' }]; - } - selectOrder(orderName) { - return [ - { - trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}")`, - }, - ]; - } - deleteOrder(orderName) { - return [ - { - trigger: `.ticket-screen .orders > .order-row > .col:contains("${orderName}") ~ .col[name="delete"]`, - }, - ]; - } - selectFilter(name) { - return [ - { - trigger: `.pos-search-bar .filter`, - }, - { - trigger: `.pos-search-bar .filter ul`, - run: () => {}, - }, - { - trigger: `.pos-search-bar .filter ul li:contains("${name}")`, - }, - ]; - } - search(field, searchWord) { - return [ - { - trigger: '.pos-search-bar input', - run: `text ${searchWord}`, - }, - { - /** - * Manually trigger keyup event to show the search field list - * because the previous step do not trigger keyup event. - */ - trigger: '.pos-search-bar input', - run: function () { - document - .querySelector('.pos-search-bar input') - .dispatchEvent(new KeyboardEvent('keyup', { key: '' })); - }, - }, - { - trigger: `.pos-search-bar .search ul li:contains("${field}")`, - }, - ]; - } - settleTips() { - return [ - { - trigger: '.ticket-screen .buttons .settle-tips', - }, - ]; - } - clickControlButton(name) { - return [ - { - trigger: `.ticket-screen .control-button:contains("${name}")`, - }, - ]; - } - clickOrderline(name) { - return [ - { - trigger: `.ticket-screen .orderline:not(:has(.selected)) .product-name:contains("${name}")`, - }, - { - trigger: `.ticket-screen .orderline.selected .product-name:contains("${name}")`, - run: () => {}, - }, - ]; - } - pressNumpad(key) { - let trigger; - if ('.0123456789'.includes(key)) { - trigger = `.numpad .number-char:contains("${key}")`; - } else if (key === 'Backspace') { - trigger = `.numpad .numpad-backspace`; - } else if (key === '+/-') { - trigger = `.numpad .numpad-minus`; - } - return [ - { - trigger, - }, - ]; - } - confirmRefund() { - return [ - { - trigger: '.ticket-screen .button.pay', - }, - ]; - } +class Do { + clickNewTicket() { + return [{ trigger: ".ticket-screen .highlight" }]; } - - class Check { - checkStatus(orderName, status) { - return [ - { - trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}") ~ .col:nth-child(6):contains(${status})`, - run: () => {}, - }, - ]; - } - /** - * Check if the nth row contains the given string. - * Note that 1st row is the header-row. - */ - nthRowContains(n, string) { - return [ - { - trigger: `.ticket-screen .orders > .order-row:nth-child(${n}):contains("${string}")`, - run: () => {}, - }, - ]; - } - noNewTicketButton() { - return [ - { - trigger: '.ticket-screen .controls .buttons:nth-child(1):has(.discard)', - run: () => {}, - }, - ]; - } - orderWidgetIsNotEmpty() { - return [ - { - trigger: '.ticket-screen:not(:has(.order-empty))', - run: () => {}, - }, - ]; - } - filterIs(name) { - return [ - { - trigger: `.ticket-screen .pos-search-bar .filter span:contains("${name}")`, - run: () => {}, - }, - ]; - } - partnerIs(name) { - return [ - { - trigger: `.ticket-screen .set-partner:contains("${name}")`, - run: () => {}, - }, - ]; - } - toRefundTextContains(text) { - return [ - { - trigger: `.ticket-screen .to-refund-highlight:contains("${text}")`, - run: () => {}, - }, - ]; - } - refundedNoteContains(text) { - return [ - { - trigger: `.ticket-screen .refund-note:contains("${text}")`, - run: () => {}, - }, - ]; - } - tipContains(amount) { - return [ - { - trigger: `.ticket-screen .tip-cell:contains("${amount}")`, - run: () => {}, - }, - ]; - } + clickDiscard() { + return [{ trigger: ".ticket-screen button.discard" }]; } + selectOrder(orderName) { + return [ + { + trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}")`, + }, + ]; + } + deleteOrder(orderName) { + return [ + { + trigger: `.ticket-screen .orders > .order-row > .col:contains("${orderName}") ~ .col[name="delete"]`, + }, + ]; + } + selectFilter(name) { + return [ + { + trigger: `.pos-search-bar .filter`, + }, + { + trigger: `.pos-search-bar .filter ul`, + run: () => {}, + }, + { + trigger: `.pos-search-bar .filter ul li:contains("${name}")`, + }, + ]; + } + search(field, searchWord) { + return [ + { + trigger: ".pos-search-bar input", + run: `text ${searchWord}`, + }, + { + /** + * Manually trigger keyup event to show the search field list + * because the previous step do not trigger keyup event. + */ + trigger: ".pos-search-bar input", + run: function () { + document + .querySelector(".pos-search-bar input") + .dispatchEvent(new KeyboardEvent("keyup", { key: "" })); + }, + }, + { + trigger: `.pos-search-bar .search ul li:contains("${field}")`, + }, + ]; + } + settleTips() { + return [ + { + trigger: ".ticket-screen .buttons .settle-tips", + }, + ]; + } + clickControlButton(name) { + return [ + { + trigger: `.ticket-screen .control-button:contains("${name}")`, + }, + ]; + } + clickOrderline(name) { + return [ + { + trigger: `.ticket-screen .orderline:not(:has(.selected)) .product-name:contains("${name}")`, + }, + { + trigger: `.ticket-screen .orderline.selected .product-name:contains("${name}")`, + run: () => {}, + }, + ]; + } + pressNumpad(key) { + let trigger; + if (".0123456789".includes(key)) { + trigger = `.numpad .number-char:contains("${key}")`; + } else if (key === "Backspace") { + trigger = `.numpad .numpad-backspace`; + } else if (key === "+/-") { + trigger = `.numpad .numpad-minus`; + } + return [ + { + trigger, + }, + ]; + } + confirmRefund() { + return [ + { + trigger: ".ticket-screen .button.pay", + }, + ]; + } +} - class Execute {} +class Check { + checkStatus(orderName, status) { + return [ + { + trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}") ~ .col:nth-child(6):contains(${status})`, + run: () => {}, + }, + ]; + } + /** + * Check if the nth row contains the given string. + * Note that 1st row is the header-row. + */ + nthRowContains(n, string) { + return [ + { + trigger: `.ticket-screen .orders > .order-row:nth-child(${n}):contains("${string}")`, + run: () => {}, + }, + ]; + } + noNewTicketButton() { + return [ + { + trigger: ".ticket-screen .controls .buttons:nth-child(1):has(.discard)", + run: () => {}, + }, + ]; + } + orderWidgetIsNotEmpty() { + return [ + { + trigger: ".ticket-screen:not(:has(.order-empty))", + run: () => {}, + }, + ]; + } + filterIs(name) { + return [ + { + trigger: `.ticket-screen .pos-search-bar .filter span:contains("${name}")`, + run: () => {}, + }, + ]; + } + partnerIs(name) { + return [ + { + trigger: `.ticket-screen .set-partner:contains("${name}")`, + run: () => {}, + }, + ]; + } + toRefundTextContains(text) { + return [ + { + trigger: `.ticket-screen .to-refund-highlight:contains("${text}")`, + run: () => {}, + }, + ]; + } + refundedNoteContains(text) { + return [ + { + trigger: `.ticket-screen .refund-note:contains("${text}")`, + run: () => {}, + }, + ]; + } + tipContains(amount) { + return [ + { + trigger: `.ticket-screen .tip-cell:contains("${amount}")`, + run: () => {}, + }, + ]; + } +} - return createTourMethods('TicketScreen', Do, Check, Execute); -}); +class Execute {} + +// FIXME: this is a horrible hack to export an object as named exports. +// eslint-disable-next-line no-undef +Object.assign(__exports, createTourMethods("TicketScreen", Do, Check, Execute)); diff --git a/addons/point_of_sale/static/tests/tours/helpers/utils.js b/addons/point_of_sale/static/tests/tours/helpers/utils.js index e8fcc5913bd..9c483c58447 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/utils.js +++ b/addons/point_of_sale/static/tests/tours/helpers/utils.js @@ -1,153 +1,146 @@ -odoo.define('point_of_sale.tour.utils', function (require) { - 'use strict'; +/** @odoo-module */ +import config from "web.config"; - const config = require('web.config'); +/** + * USAGE + * ----- + * + * ``` + * import const { startSteps, getSteps, createTourMethods } from "@point_of_sale/js/utils"; + * import const { Other } from "@point_of_sale/../tests/OtherMethods"; + * + * // 1. Define classes Do, Check and Execute having methods that + * // each return array of tour steps. + * class Do { + * click() { + * return [{ content: 'click button', trigger: '.button' }]; + * } + * } + * class Check { + * isHighligted() { + * return [{ content: 'button is highlighted', trigger: '.button.highlight', run: () => {} }]; + * } + * } + * // Notice that Execute has access to methods defined in Do and Check classes + * // Also, we can compose steps from other module. + * class Execute { + * complexSteps() { + * return [...this._do.click(), ...this._check.isHighlighted(), ...Other._exec.complicatedSteps()]; + * } + * } + * + * // 2. Instantiate these class definitions using `createTourMethods`. + * // The returned object gives access to the defined methods above + * // thru the do, check and exec properties. + * // - do gives access to the methods defined in Do class + * // - check gives access to the methods defined in Check class + * // - exec gives access to the methods defined in Execute class + * const Screen = createTourMethods('Screen', Do, Check, Execute); + * + * // 3. Call `startSteps` to start empty steps. + * startSteps(); + * + * // 4. Call the tour methods to populate the steps created by `startSteps`. + * Screen.do.click(); // return of this method call is added to steps created by startSteps + * Screen.check.isHighlighted() // same as above + * Screen.exec.complexSteps() // same as above + * + * // 5. Call `getSteps` which returns the generated tour steps. + * const steps = getSteps(); + * ``` + */ +let steps = []; - /** - * USAGE - * ----- - * - * ``` - * const { startSteps, getSteps, createTourMethods } = require('point_of_sale.utils'); - * const { Other } = require('point_of_sale.tour.OtherMethods'); - * - * // 1. Define classes Do, Check and Execute having methods that - * // each return array of tour steps. - * class Do { - * click() { - * return [{ content: 'click button', trigger: '.button' }]; - * } - * } - * class Check { - * isHighligted() { - * return [{ content: 'button is highlighted', trigger: '.button.highlight', run: () => {} }]; - * } - * } - * // Notice that Execute has access to methods defined in Do and Check classes - * // Also, we can compose steps from other module. - * class Execute { - * complexSteps() { - * return [...this._do.click(), ...this._check.isHighlighted(), ...Other._exec.complicatedSteps()]; - * } - * } - * - * // 2. Instantiate these class definitions using `createTourMethods`. - * // The returned object gives access to the defined methods above - * // thru the do, check and exec properties. - * // - do gives access to the methods defined in Do class - * // - check gives access to the methods defined in Check class - * // - exec gives access to the methods defined in Execute class - * const Screen = createTourMethods('Screen', Do, Check, Execute); - * - * // 3. Call `startSteps` to start empty steps. - * startSteps(); - * - * // 4. Call the tour methods to populate the steps created by `startSteps`. - * Screen.do.click(); // return of this method call is added to steps created by startSteps - * Screen.check.isHighlighted() // same as above - * Screen.exec.complexSteps() // same as above - * - * // 5. Call `getSteps` which returns the generated tour steps. - * const steps = getSteps(); - * ``` - */ - let steps = []; +export function startSteps() { + // always start by waiting for loading to finish + steps = [ + { + content: "wait for loading to finish", + trigger: "body:not(:has(.loader))", + run: function () {}, + }, + ]; +} - function startSteps() { - // always start by waiting for loading to finish - steps = [ - { - content: 'wait for loading to finish', - trigger: 'body:not(:has(.loader))', - run: function () {}, - }, - ]; - } +export function getSteps() { + return steps; +} - function getSteps() { - return steps; - } +// this is the method decorator +// when the method is called, the generated steps are added +// to steps +const methodProxyHandler = { + apply(target, thisArg, args) { + const res = target.call(thisArg, ...args); + if (config.isDebug()) { + // This step is added before the real steps. + // Very useful when debugging because we know which + // method call failed and what were the parameters. + const constructor = thisArg.constructor.name.split(" ")[1]; + const methodName = target.name.split(" ")[1]; + const argList = args.map((a) => (typeof a === "string" ? `'${a}'` : `${a}`)).join(", "); + steps.push({ + content: `DOING "${constructor}.${methodName}(${argList})"`, + trigger: ".pos", + run: () => {}, + }); + } + steps.push(...res); + return res; + }, +}; - // this is the method decorator - // when the method is called, the generated steps are added - // to steps - const methodProxyHandler = { - apply(target, thisArg, args) { - const res = target.call(thisArg, ...args); - if (config.isDebug()) { - // This step is added before the real steps. - // Very useful when debugging because we know which - // method call failed and what were the parameters. - const constructor = thisArg.constructor.name.split(' ')[1]; - const methodName = target.name.split(' ')[1]; - const argList = args - .map((a) => (typeof a === 'string' ? `'${a}'` : `${a}`)) - .join(', '); - steps.push({ - content: `DOING "${constructor}.${methodName}(${argList})"`, - trigger: '.pos', - run: () => {}, - }); - } - steps.push(...res); - return res; +// we proxy get of the method to decorate the method call +const proxyHandler = { + get(target, key) { + const method = target[key]; + if (!method) { + throw new Error(`Tour method '${key}' is not available.`); + } + return new Proxy(method.bind(target), methodProxyHandler); + }, +}; + +/** + * Creates an object with `do`, `check` and `exec` properties which are instances of + * the given `Do`, `Check` and `Execute` classes, respectively. Calling methods + * automatically adds the returned steps to the steps created by `startSteps`. + * + * There are however underscored version (_do, _check, _exec). + * Calling methods thru the underscored version does not automatically + * add the returned steps to the current steps array. Useful when composing + * steps from other methods. + * + * @param {String} name + * @param {Function} Do class containing methods which return array of tour steps + * @param {Function} Check similar to Do class but the steps are mainly for checking + * @param {Function} Execute class containing methods which return array of tour steps + * but has access to methods of Do and Check classes via .do and .check, + * respectively. Here, we define methods that return tour steps based + * on the combination of steps from Do and Check. + */ +export function createTourMethods(name, Do, Check = class {}, Execute = class {}) { + Object.defineProperty(Do, "name", { value: `${name}.do` }); + Object.defineProperty(Check, "name", { value: `${name}.check` }); + Object.defineProperty(Execute, "name", { + value: `${name}.exec`, + }); + const methods = { do: new Do(), check: new Check(), exec: new Execute() }; + // Allow Execute to have access to methods defined in Do and Check + // via do and exec, respectively. + methods.exec._do = methods.do; + methods.exec._check = methods.check; + return { + Do, + Check, + Execute, + [name]: { + do: new Proxy(methods.do, proxyHandler), + check: new Proxy(methods.check, proxyHandler), + exec: new Proxy(methods.exec, proxyHandler), + _do: methods.do, + _check: methods.check, + _exec: methods.exec, }, }; - - // we proxy get of the method to decorate the method call - const proxyHandler = { - get(target, key) { - const method = target[key]; - if (!method) { - throw new Error(`Tour method '${key}' is not available.`); - } - return new Proxy(method.bind(target), methodProxyHandler); - }, - }; - - /** - * Creates an object with `do`, `check` and `exec` properties which are instances of - * the given `Do`, `Check` and `Execute` classes, respectively. Calling methods - * automatically adds the returned steps to the steps created by `startSteps`. - * - * There are however underscored version (_do, _check, _exec). - * Calling methods thru the underscored version does not automatically - * add the returned steps to the current steps array. Useful when composing - * steps from other methods. - * - * @param {String} name - * @param {Function} Do class containing methods which return array of tour steps - * @param {Function} Check similar to Do class but the steps are mainly for checking - * @param {Function} Execute class containing methods which return array of tour steps - * but has access to methods of Do and Check classes via .do and .check, - * respectively. Here, we define methods that return tour steps based - * on the combination of steps from Do and Check. - */ - function createTourMethods(name, Do, Check = class {}, Execute = class {}) { - Object.defineProperty(Do, 'name', { value: `${name}.do` }); - Object.defineProperty(Check, 'name', { value: `${name}.check` }); - Object.defineProperty(Execute, 'name', { - value: `${name}.exec`, - }); - const methods = { do: new Do(), check: new Check(), exec: new Execute() }; - // Allow Execute to have access to methods defined in Do and Check - // via do and exec, respectively. - methods.exec._do = methods.do; - methods.exec._check = methods.check; - return { - Do, - Check, - Execute, - [name]: { - do: new Proxy(methods.do, proxyHandler), - check: new Proxy(methods.check, proxyHandler), - exec: new Proxy(methods.exec, proxyHandler), - _do: methods.do, - _check: methods.check, - _exec: methods.exec, - }, - }; - } - - return { startSteps, getSteps, createTourMethods }; -}); +} diff --git a/addons/point_of_sale/static/tests/tours/point_of_sale.js b/addons/point_of_sale/static/tests/tours/point_of_sale.js deleted file mode 100644 index fe2b376f90a..00000000000 --- a/addons/point_of_sale/static/tests/tours/point_of_sale.js +++ /dev/null @@ -1,408 +0,0 @@ -/* global posmodel */ -odoo.define('point_of_sale.tour.pricelist', function (require) { - "use strict"; - - var Tour = require('web_tour.tour'); - var utils = require('web.utils'); - var round_di = utils.round_decimals; - - function assert (condition, message) { - if (! condition) { - throw message || "Assertion failed"; - } - } - - function assertProductPrice(product, pricelist_name, quantity, expected_price) { - return function () { - var pricelist = _.findWhere(posmodel.pricelists, {name: pricelist_name}); - var frontend_price = product.get_price(pricelist, quantity); - frontend_price = round_di(frontend_price, posmodel.dp['Product Price']); - - var diff = Math.abs( expected_price - frontend_price ); - - assert(diff < 0.001, - JSON.stringify({ - product: product.id, - product_display_name: product.display_name, - pricelist_name: pricelist_name, - quantity: quantity - }) + ' DOESN\'T MATCH -> ' + expected_price + ' != ' + frontend_price); - - return Promise.resolve(); - }; - } - - // The global posmodel is only present when the posmodel is instanciated - // So, wait for everythiong to be loaded - var steps = [{ // Leave category displayed by default - content: 'waiting for loading to finish', - extra_trigger: 'body .pos:not(:has(.loader))', // Pos has finished loading - trigger: 'body:not(:has(.o_loading_indicator))', // WebClient has finished Loading - run: function () { - var product_wall_shelf = posmodel.db.search_product_in_category(0, 'Wall Shelf Unit')[0]; - var product_small_shelf = posmodel.db.search_product_in_category(0, 'Small Shelf')[0]; - var product_magnetic_board = posmodel.db.search_product_in_category(0, 'Magnetic Board')[0]; - var product_monitor_stand = posmodel.db.search_product_in_category(0, 'Monitor Stand')[0]; - var product_desk_pad = posmodel.db.search_product_in_category(0, 'Desk Pad')[0]; - var product_letter_tray = posmodel.db.search_product_in_category(0, 'Letter Tray')[0]; - var product_whiteboard = posmodel.db.search_product_in_category(0, 'Whiteboard')[0]; - - assertProductPrice(product_letter_tray, 'Public Pricelist', 0, 4.8)() - .then(assertProductPrice(product_letter_tray, 'Public Pricelist', 1, 4.8)) - .then(assertProductPrice(product_letter_tray, 'Fixed', 1, 1)) - .then(assertProductPrice(product_wall_shelf, 'Fixed', 1, 2)) - .then(assertProductPrice(product_small_shelf, 'Fixed', 1, 13.95)) - .then(assertProductPrice(product_wall_shelf, 'Percentage', 1, 0)) - .then(assertProductPrice(product_small_shelf, 'Percentage', 1, 0.03)) - .then(assertProductPrice(product_magnetic_board, 'Percentage', 1, 1.98)) - .then(assertProductPrice(product_wall_shelf, 'Formula', 1, 6.86)) - .then(assertProductPrice(product_small_shelf, 'Formula', 1, 2.99)) - .then(assertProductPrice(product_magnetic_board, 'Formula', 1, 11.98)) - .then(assertProductPrice(product_monitor_stand, 'Formula', 1, 8.19)) - .then(assertProductPrice(product_desk_pad, 'Formula', 1, 6.98)) - .then(assertProductPrice(product_wall_shelf, 'min_quantity ordering', 1, 2)) - .then(assertProductPrice(product_wall_shelf, 'min_quantity ordering', 2, 1)) - .then(assertProductPrice(product_letter_tray, 'Category vs no category', 1, 2)) - .then(assertProductPrice(product_letter_tray, 'Category', 1, 2)) - .then(assertProductPrice(product_wall_shelf, 'Product template', 1, 1)) - .then(assertProductPrice(product_wall_shelf, 'Dates', 1, 2)) - .then(assertProductPrice(product_small_shelf, 'Pricelist base rounding', 1, 13.95)) - .then(assertProductPrice(product_whiteboard, 'Public Pricelist', 1, 3.2)) - .then(function () { - $('.pos').addClass('done-testing'); - }); - }, - }, { - trigger: '.opening-cash-control .button:contains("Open session")', - }]; - - steps = steps.concat([{ - content: "wait for unit tests to finish", - trigger: ".pos.done-testing", - run: function () {}, // it's a check - }, { - content: "click category switch", - trigger: ".breadcrumb-home", - run: 'click', - }, { - content: "click pricelist button", - trigger: ".control-button.o_pricelist_button", - }, { - content: "verify default pricelist is set", - trigger: ".selection-item.selected:contains('Public Pricelist')", - run: function () {}, // it's a check - }, { - content: "select fixed pricelist", - trigger: ".selection-item:contains('Fixed')", - }, { - content: "open partner list", - trigger: "button.set-partner", - }, { - content: "select Deco Addict", - trigger: ".partner-line:contains('Deco Addict')", - }, { - content: "click pricelist button", - trigger: ".control-button.o_pricelist_button", - }, { - content: "verify pricelist changed", - trigger: ".selection-item.selected:contains('Public Pricelist')", - run: function () {}, // it's a check - }, { - content: "cancel pricelist dialog", - trigger: ".button.cancel:visible", - }, { - content: "open customer list", - trigger: "button.set-partner", - }, { - content: "select Lumber Inc", - trigger: ".partner-line:contains('Lumber Inc')", - }, { - content: "click pricelist button", - trigger: ".control-button.o_pricelist_button", - }, { - content: "verify pricelist remained public pricelist ('Not loaded' is not available)", - trigger: ".selection-item.selected:contains('Public Pricelist')", - run: function () {}, // it's a check - }, { - content: "cancel pricelist dialog", - trigger: ".button.cancel:visible", - }, { - content: "click pricelist button", - trigger: ".control-button.o_pricelist_button", - }, { - content: "select fixed pricelist", - trigger: ".selection-item:contains('min_quantity ordering')", - }, { - content: "order 1 kg shelf", - trigger: ".product:contains('Wall Shelf')", - }, { - content: "change qty to 2 kg", - trigger: ".numpad button.input-button:visible:contains('2')", - }, { - content: "qty of Wall Shelf line should be 2", - trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf')", - extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf') ~ .info-list .info em:contains('2.0')", - run: function() {}, - }, { - content: "verify that unit price of shelf changed to $1", - trigger: ".total > .value:contains('$ 2.00')", - run: function() {}, - }, { - content: "order different shelf", - trigger: ".product:contains('Small Shelf')", - }, { - content: "Small Shelf line should be selected with quantity 1", - trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')", - extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .info-list .info em:contains('1.0')", - run: function() {} - }, { - content: "change to price mode", - trigger: ".numpad button:contains('Price')", - }, { - content: "make sure price mode is activated", - trigger: ".numpad button.selected-mode:contains('Price')", - run: function() {}, - }, { - content: "manually override the unit price of these shelf to $5", - trigger: ".numpad button.input-button:visible:contains('5')", - }, { - content: "Small Shelf line should be selected with unit price of 5", - trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')", - extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .price:contains('5.0')", - }, { - content: "change back to qty mode", - trigger: ".numpad button:contains('Qty')", - }, { - content: "make sure qty mode is activated", - trigger: ".numpad button.selected-mode:contains('Qty')", - run: function() {}, - }, { - content: "click pricelist button", - trigger: ".control-button.o_pricelist_button", - }, { - content: "select public pricelist", - trigger: ".selection-item:contains('Public Pricelist')", - }, { - content: "verify that the boni shelf have been recomputed and the shelf have not (their price was manually overridden)", - trigger: ".total > .value:contains('$ 8.96')", - }, { - content: "click pricelist button", - trigger: ".control-button.o_pricelist_button", - }, { - content: "select fixed pricelist", - trigger: ".selection-item:contains('min_quantity ordering')", - }, { - content: "close the Point of Sale frontend", - trigger: ".header-button", - }, { - content: "confirm closing the frontend", - trigger: ".header-button", - run: function() {}, //it's a check, - }]); - - Tour.register('pos_pricelist', { test: true, url: '/pos/ui' }, steps); -}); - -odoo.define('point_of_sale.tour.acceptance', function (require) { - "use strict"; - - var Tour = require("web_tour.tour"); - - function add_product_to_order(product_name) { - return [{ - content: 'buy ' + product_name, - trigger: '.product-list .product-name:contains("' + product_name + '")', - }, { - content: 'the ' + product_name + ' have been added to the order', - trigger: '.order .product-name:contains("' + product_name + '")', - run: function () {}, - }]; - } - - function set_fiscal_position_on_order(fp_name) { - return [{ - content: 'set fiscal position', - trigger: '.control-button.o_fiscal_position_button', - }, { - content: 'choose fiscal position ' + fp_name + ' to add to the order', - trigger: '.popups .popup .selection .selection-item:contains("' + fp_name + '")', - }, { - content: 'the fiscal position ' + fp_name + ' has been set to the order', - trigger: '.control-button.o_fiscal_position_button:contains("' + fp_name + '")', - run: function () {}, - }]; - } - - function press_payment_numpad(val) { - return [{ - content: `press ${val} on payment screen numpad`, - trigger: `.payment-numpad .input-button:contains("${val}"):visible`, - }] - } - - function press_product_numpad(val) { - return [{ - content: `press ${val} on product screen numpad`, - trigger: `.numpad .input-button:contains("${val}"):visible`, - }] - } - - function selected_payment_has(name, val) { - return [{ - content: `selected payment is ${name} and has ${val}`, - trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}")`, - extra_trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}") ~ .payment-amount:contains("${val}")`, - run: function () {}, - }] - } - - function selected_orderline_has({ product, price = null, quantity = null }) { - const result = []; - if (price !== null) { - result.push({ - content: `Selected line has product '${product}' and price '${price}'`, - trigger: `.order-container .orderlines .orderline.selected .product-name:contains("${product}") ~ span.price:contains("${price}")`, - run: function () {}, - }); - } - if (quantity !== null) { - result.push({ - content: `Selected line has product '${product}' and quantity '${quantity}'`, - trigger: `.order-container .orderlines .orderline.selected .product-name:contains('${product}') ~ .info-list .info em:contains('${quantity}')`, - run: function () {}, - }); - } - return result; - } - - function verify_order_total(total_str) { - return [{ - content: 'order total contains ' + total_str, - trigger: '.order .total .value:contains("' + total_str + '")', - run: function () {}, // it's a check - }]; - } - - function goto_payment_screen_and_select_payment_method() { - return [{ - content: "go to payment screen", - trigger: '.button.pay', - }, { - content: "pay with cash", - trigger: '.paymentmethod:contains("Cash")', - }]; - } - - function finish_order() { - return [{ - content: "validate the order", - trigger: '.payment-screen .button.next.highlight:visible', - }, { - content: "verify that the order has been successfully sent to the backend", - trigger: ".js_connected:visible", - run: function () {}, - }, { - content: "click Next Order", - trigger: '.receipt-screen .button.next.highlight:visible', - }, { - content: "check if we left the receipt screen", - trigger: '.pos-content .screen:not(:has(.receipt-screen))', - run: function () {}, - }]; - } - - var steps = [{ - content: 'waiting for loading to finish', - trigger: 'body:not(:has(.loader))', - run: function () {}, - }, { // Leave category displayed by default - content: "click category switch", - trigger: ".breadcrumb-home", - }]; - - steps = steps.concat(add_product_to_order('Desk Organizer')); - steps = steps.concat(verify_order_total('5.10')); - - steps = steps.concat(add_product_to_order('Desk Organizer')); - steps = steps.concat(verify_order_total('10.20')); - steps = steps.concat(goto_payment_screen_and_select_payment_method()); - - /* add payment line of only 5.20 - status: - order-total := 10.20 - total-payment := 11.70 - expect: - remaining := 0.00 - change := 1.50 - */ - steps = steps.concat(press_payment_numpad('5')); - steps = steps.concat(selected_payment_has('Cash', '5.0')); - steps = steps.concat([{ - content: "verify remaining", - trigger: '.payment-status-remaining .amount:contains("5.20")', - run: function () {}, - }, { - content: "verify change", - trigger: '.payment-status-change .amount:contains("0.00")', - run: function () {}, - }]); - - /* make additional payment line of 6.50 - status: - order-total := 10.20 - total-payment := 11.70 - expect: - remaining := 0.00 - change := 1.50 - */ - steps = steps.concat([{ - content: "pay with cash", - trigger: '.paymentmethod:contains("Cash")', - }]); - steps = steps.concat(selected_payment_has('Cash', '5.2')); - steps = steps.concat(press_payment_numpad('6')) - steps = steps.concat(selected_payment_has('Cash', '6.0')); - steps = steps.concat([{ - content: "verify remaining", - trigger: '.payment-status-remaining .amount:contains("0.00")', - run: function () {}, - }, { - content: "verify change", - trigger: '.payment-status-change .amount:contains("0.80")', - run: function () {}, - }]); - - steps = steps.concat(finish_order()); - - // test opw-672118 orderline subtotal rounding - steps = steps.concat(add_product_to_order('Desk Organizer')); - steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '1.0'})); - steps = steps.concat(press_product_numpad('.')) - steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.0', price: '0.0'})); - steps = steps.concat(press_product_numpad('9')) - steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.9', price: '4.59'})); - steps = steps.concat(press_product_numpad('9')) - steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.99', price: '5.05'})); - steps = steps.concat(goto_payment_screen_and_select_payment_method()); - steps = steps.concat(selected_payment_has('Cash', '5.05')); - steps = steps.concat(finish_order()); - - // Test fiscal position one2many map (align with backend) - steps = steps.concat(add_product_to_order('Letter Tray')); - steps = steps.concat(selected_orderline_has({product: 'Letter Tray', quantity: '1.0'})); - steps = steps.concat(verify_order_total('5.28')); - steps = steps.concat(set_fiscal_position_on_order('FP-POS-2M')); - steps = steps.concat(verify_order_total('5.52')); - - steps = steps.concat([{ - content: "open closing the Point of Sale frontend popup", - trigger: ".header-button", - }, { - content: "close the Point of Sale frontend", - trigger: ".close-pos-popup .button:contains('Discard')", - run: function() {}, //it's a check, - }]); - - Tour.register('pos_basic_order', { test: true, url: '/pos/ui' }, steps); - -}); diff --git a/addons/point_of_sale/static/tests/tours/pricelist_tour.js b/addons/point_of_sale/static/tests/tours/pricelist_tour.js new file mode 100644 index 00000000000..9b68bd03e71 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/pricelist_tour.js @@ -0,0 +1,265 @@ +/** @odoo-module */ +/* global posmodel */ + +import Tour from "web_tour.tour"; +import utils from "web.utils"; +var round_di = utils.round_decimals; + +function assert(condition, message) { + if (!condition) { + throw message || "Assertion failed"; + } +} + +function assertProductPrice(product, pricelist_name, quantity, expected_price) { + return function () { + var pricelist = _.findWhere(posmodel.pricelists, { name: pricelist_name }); + var frontend_price = product.get_price(pricelist, quantity); + frontend_price = round_di(frontend_price, posmodel.dp["Product Price"]); + + var diff = Math.abs(expected_price - frontend_price); + + assert( + diff < 0.001, + JSON.stringify({ + product: product.id, + product_display_name: product.display_name, + pricelist_name: pricelist_name, + quantity: quantity, + }) + + " DOESN'T MATCH -> " + + expected_price + + " != " + + frontend_price + ); + + return Promise.resolve(); + }; +} + +// The global posmodel is only present when the posmodel is instanciated +// So, wait for everythiong to be loaded +var steps = [ + { + // Leave category displayed by default + content: "waiting for loading to finish", + extra_trigger: "body .pos:not(:has(.loader))", // Pos has finished loading + trigger: "body:not(:has(.o_loading_indicator))", // WebClient has finished Loading + run: function () { + var product_wall_shelf = posmodel.db.search_product_in_category( + 0, + "Wall Shelf Unit" + )[0]; + var product_small_shelf = posmodel.db.search_product_in_category(0, "Small Shelf")[0]; + var product_magnetic_board = posmodel.db.search_product_in_category( + 0, + "Magnetic Board" + )[0]; + var product_monitor_stand = posmodel.db.search_product_in_category( + 0, + "Monitor Stand" + )[0]; + var product_desk_pad = posmodel.db.search_product_in_category(0, "Desk Pad")[0]; + var product_letter_tray = posmodel.db.search_product_in_category(0, "Letter Tray")[0]; + var product_whiteboard = posmodel.db.search_product_in_category(0, "Whiteboard")[0]; + + assertProductPrice(product_letter_tray, "Public Pricelist", 0, 4.8)() + .then(assertProductPrice(product_letter_tray, "Public Pricelist", 1, 4.8)) + .then(assertProductPrice(product_letter_tray, "Fixed", 1, 1)) + .then(assertProductPrice(product_wall_shelf, "Fixed", 1, 2)) + .then(assertProductPrice(product_small_shelf, "Fixed", 1, 13.95)) + .then(assertProductPrice(product_wall_shelf, "Percentage", 1, 0)) + .then(assertProductPrice(product_small_shelf, "Percentage", 1, 0.03)) + .then(assertProductPrice(product_magnetic_board, "Percentage", 1, 1.98)) + .then(assertProductPrice(product_wall_shelf, "Formula", 1, 6.86)) + .then(assertProductPrice(product_small_shelf, "Formula", 1, 2.99)) + .then(assertProductPrice(product_magnetic_board, "Formula", 1, 11.98)) + .then(assertProductPrice(product_monitor_stand, "Formula", 1, 8.19)) + .then(assertProductPrice(product_desk_pad, "Formula", 1, 6.98)) + .then(assertProductPrice(product_wall_shelf, "min_quantity ordering", 1, 2)) + .then(assertProductPrice(product_wall_shelf, "min_quantity ordering", 2, 1)) + .then(assertProductPrice(product_letter_tray, "Category vs no category", 1, 2)) + .then(assertProductPrice(product_letter_tray, "Category", 1, 2)) + .then(assertProductPrice(product_wall_shelf, "Product template", 1, 1)) + .then(assertProductPrice(product_wall_shelf, "Dates", 1, 2)) + .then(assertProductPrice(product_small_shelf, "Pricelist base rounding", 1, 13.95)) + .then(assertProductPrice(product_whiteboard, "Public Pricelist", 1, 3.2)) + .then(function () { + $(".pos").addClass("done-testing"); + }); + }, + }, + { + trigger: '.opening-cash-control .button:contains("Open session")', + }, +]; + +steps = steps.concat([ + { + content: "wait for unit tests to finish", + trigger: ".pos.done-testing", + run: function () {}, // it's a check + }, + { + content: "click category switch", + trigger: ".breadcrumb-home", + run: "click", + }, + { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, + { + content: "verify default pricelist is set", + trigger: ".selection-item.selected:contains('Public Pricelist')", + run: function () {}, // it's a check + }, + { + content: "select fixed pricelist", + trigger: ".selection-item:contains('Fixed')", + }, + { + content: "open partner list", + trigger: "button.set-partner", + }, + { + content: "select Deco Addict", + trigger: ".partner-line:contains('Deco Addict')", + }, + { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, + { + content: "verify pricelist changed", + trigger: ".selection-item.selected:contains('Public Pricelist')", + run: function () {}, // it's a check + }, + { + content: "cancel pricelist dialog", + trigger: ".button.cancel:visible", + }, + { + content: "open customer list", + trigger: "button.set-partner", + }, + { + content: "select Lumber Inc", + trigger: ".partner-line:contains('Lumber Inc')", + }, + { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, + { + content: "verify pricelist remained public pricelist ('Not loaded' is not available)", + trigger: ".selection-item.selected:contains('Public Pricelist')", + run: function () {}, // it's a check + }, + { + content: "cancel pricelist dialog", + trigger: ".button.cancel:visible", + }, + { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, + { + content: "select fixed pricelist", + trigger: ".selection-item:contains('min_quantity ordering')", + }, + { + content: "order 1 kg shelf", + trigger: ".product:contains('Wall Shelf')", + }, + { + content: "change qty to 2 kg", + trigger: ".numpad button.input-button:visible:contains('2')", + }, + { + content: "qty of Wall Shelf line should be 2", + trigger: + ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf')", + extra_trigger: + ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf') ~ .info-list .info em:contains('2.0')", + run: function () {}, + }, + { + content: "verify that unit price of shelf changed to $1", + trigger: ".total > .value:contains('$ 2.00')", + run: function () {}, + }, + { + content: "order different shelf", + trigger: ".product:contains('Small Shelf')", + }, + { + content: "Small Shelf line should be selected with quantity 1", + trigger: + ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')", + extra_trigger: + ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .info-list .info em:contains('1.0')", + run: function () {}, + }, + { + content: "change to price mode", + trigger: ".numpad button:contains('Price')", + }, + { + content: "make sure price mode is activated", + trigger: ".numpad button.selected-mode:contains('Price')", + run: function () {}, + }, + { + content: "manually override the unit price of these shelf to $5", + trigger: ".numpad button.input-button:visible:contains('5')", + }, + { + content: "Small Shelf line should be selected with unit price of 5", + trigger: + ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')", + extra_trigger: + ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .price:contains('5.0')", + }, + { + content: "change back to qty mode", + trigger: ".numpad button:contains('Qty')", + }, + { + content: "make sure qty mode is activated", + trigger: ".numpad button.selected-mode:contains('Qty')", + run: function () {}, + }, + { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, + { + content: "select public pricelist", + trigger: ".selection-item:contains('Public Pricelist')", + }, + { + content: + "verify that the boni shelf have been recomputed and the shelf have not (their price was manually overridden)", + trigger: ".total > .value:contains('$ 8.96')", + }, + { + content: "click pricelist button", + trigger: ".control-button.o_pricelist_button", + }, + { + content: "select fixed pricelist", + trigger: ".selection-item:contains('min_quantity ordering')", + }, + { + content: "close the Point of Sale frontend", + trigger: ".header-button", + }, + { + content: "confirm closing the frontend", + trigger: ".header-button", + run: function () {}, //it's a check, + }, +]); + +Tour.register("pos_pricelist", { test: true, url: "/pos/ui" }, steps); 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 7001a440433..58520aa13f2 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 @@ -1,34 +1,38 @@ -odoo.define('point_of_sale.test_env', async function (require) { - 'use strict'; +/** @odoo-module */ +// FIXME unused module? +/** + * Many components in PoS are dependent on the PosGlobalState instance (pos). + * Therefore, for unit tests that require pos in the Components' env, we + * prepared here a test env maker (makePosTestEnv) based on + * makeTestEnvironment of web. + */ - /** - * Many components in PoS are dependent on the PosGlobalState instance (pos). - * Therefore, for unit tests that require pos in the Components' env, we - * prepared here a test env maker (makePosTestEnv) based on - * makeTestEnvironment of web. - */ +import makeTestEnvironment from "web.test_env"; +import env from "@point_of_sale/js/pos_env"; +import { PosGlobalState } from "@point_of_sale/js/models"; - const makeTestEnvironment = require('web.test_env'); - const env = require('point_of_sale.env'); - const { PosGlobalState } = require('point_of_sale.models'); - const cleanup = require("@web/../tests/helpers/cleanup"); +// eslint-disable-next-line no-undef +const cleanup = require("@web/../tests/helpers/cleanup"); +// We override this method in the pos unit tests to prevent the unnecessary error in the web tests. +// FIXME: this is bonkers, we're overriding which method is exported by the module, this is super +// sensitive to load order. STOP DOING THIS ASAP. +cleanup.registerCleanup = () => {}; - // We override this method in the pos unit tests to prevent the unnecessary error in the web tests. - cleanup.registerCleanup = () => {} +const makePosTestEnv = env.session.is_bound + .then(() => { + const pos = PosGlobalState.create({ env }); + return pos.load_server_data().then(() => pos); + }) + .then((pos) => { + /** + * @param {Object} env default env + * @param {Function} providedRPC mock rpc + * @param {Function} providedDoAction mock do_action + */ + return function makePosTestEnv(env = {}, providedRPC = null, providedDoAction = null) { + env = Object.assign(env, { pos }); + return makeTestEnvironment(env, providedRPC); + }; + }); - await env.session.is_bound; - const pos = PosGlobalState.create({ env }); - await pos.load_server_data(); - - /** - * @param {Object} env default env - * @param {Function} providedRPC mock rpc - * @param {Function} providedDoAction mock do_action - */ - function makePosTestEnv(env = {}, providedRPC = null, providedDoAction = null) { - env = Object.assign(env, { pos }); - return makeTestEnvironment(env, providedRPC); - } - - return makePosTestEnv; -}); +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 index 4b2217cb1ec..33029faceee 100644 --- a/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js +++ b/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js @@ -1,414 +1,412 @@ -odoo.define('point_of_sale.tests.ComponentRegistry', function(require) { - 'use strict'; +/** @odoo-module */ - const Registries = require('point_of_sale.Registries'); +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); - - let A1 = x => - class extends x { - constructor() { - super(); - assert.step('A1'); - } - }; - Registries.Component.extend(A, A1); - - Registries.Component.freeze(); - - const RegA = Registries.Component.get(A); - let 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); - - let B = x => - class extends x { - constructor() { - super(); - assert.step('B'); - } - }; - Registries.Component.addByExtending(B, A); - - let A1 = x => - class extends x { - constructor() { - super(); - assert.step('A1'); - } - }; - Registries.Component.extend(A, A1); - - let 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); - let 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); - - let B = x => - class extends x { - constructor() { - super(); - assert.step('B'); - } - }; - Registries.Component.addByExtending(B, A); - - let B1 = x => - class extends x { - constructor() { - super(); - assert.step('B1'); - } - }; - Registries.Component.extend(B, B1); - - let B2 = x => - class extends x { - constructor() { - super(); - assert.step('B2'); - } - }; - Registries.Component.extend(B, B2); - - let 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); - - let B = x => - class extends x { - constructor() { - super(); - assert.step('B'); - } - }; - Registries.Component.addByExtending(B, A); - - let A1 = x => - class extends x { - constructor() { - super(); - assert.step('A1'); - } - }; - Registries.Component.extend(A, A1); - - let C = x => - class extends x { - constructor() { - super(); - assert.step('C'); - } - }; - Registries.Component.addByExtending(C, B); - - let 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); - let 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); - - let B = x => - class extends x { - constructor() { - super(); - assert.step('B'); - } - }; - Registries.Component.addByExtending(B, A); - - let A1 = x => - class extends x { - constructor() { - super(); - assert.step('A1'); - } - }; - Registries.Component.extend(A, A1); - - let C = x => - class extends x { - constructor() { - super(); - assert.step('C'); - } - }; - Registries.Component.addByExtending(C, B); - - let B2 = x => - class extends x { - constructor() { - super(); - assert.step('B2'); - } - }; - Registries.Component.extend(B, B2); - - let B3 = x => - class extends x { - constructor() { - super(); - assert.step('B3'); - } - }; - Registries.Component.extend(B, B3); - - let A9 = x => - class extends x { - constructor() { - super(); - assert.step('A9'); - } - }; - Registries.Component.extend(A, A9); - - let 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); - - class A { - constructor() { - assert.step('A'); - } - } - Registries.Component.add(A); - - let 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); - - let 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); - - 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); - - let A1 = x => - class extends x { - bar() { - assert.step('A1 bar'); - // should only be for A. - } - }; - Registries.Component.extend(A, A1); - - let 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']); - }); +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 3b834472ffe..07a2957b64b 100644 --- a/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js +++ b/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js @@ -1,44 +1,43 @@ -odoo.define('point_of_sale.tests.NumberBuffer', function(require) { - 'use strict'; +/** @odoo-module */ - const NumberBuffer = require('point_of_sale.NumberBuffer'); - const makeTestEnvironment = require('web.test_env'); - const testUtils = require('web.test_utils'); - const { mount } = require('@web/../tests/helpers/utils'); - const { LegacyComponent } = require("@web/legacy/legacy_component"); +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"; +import { LegacyComponent } from "@web/legacy/legacy_component"; - const { useState, xml } = owl; +const { useState, xml } = owl; - QUnit.module('unit tests for NumberBuffer', { - before() {}, - }); +QUnit.module("unit tests for NumberBuffer", { + before() {}, +}); - QUnit.test('simple fast inputs with capture in between', async function(assert) { - assert.expect(3); - const target = testUtils.prepareTarget(); - const env = makeTestEnvironment(); +QUnit.test("simple fast inputs with capture in between", async function (assert) { + assert.expect(3); + const target = testUtils.prepareTarget(); + const env = makeTestEnvironment(); - class Root extends LegacyComponent { - setup() { - this.state = useState({ buffer: '' }); - NumberBuffer.activate(); - NumberBuffer.use({ - nonKeyboardInputEvent: 'numpad-click-input', - state: this.state, - }); - } - resetBuffer() { - NumberBuffer.capture(); - NumberBuffer.reset(); - } - onClickOne() { - this.trigger('numpad-click-input', { key: '1' }); - } - onClickTwo() { - this.trigger('numpad-click-input', { key: '2' }); - } + class Root extends LegacyComponent { + setup() { + this.state = useState({ buffer: "" }); + NumberBuffer.activate(); + NumberBuffer.use({ + nonKeyboardInputEvent: "numpad-click-input", + state: this.state, + }); } - Root.template = xml/* html */ ` + resetBuffer() { + NumberBuffer.capture(); + NumberBuffer.reset(); + } + onClickOne() { + this.trigger("numpad-click-input", { key: "1" }); + } + onClickTwo() { + this.trigger("numpad-click-input", { key: "2" }); + } + } + Root.template = xml/* html */ `

@@ -47,23 +46,22 @@ odoo.define('point_of_sale.tests.NumberBuffer', function(require) {
`; - await mount(Root, target, { env }); + await mount(Root, target, { env }); - const oneButton = target.querySelector('button.one'); - const twoButton = target.querySelector('button.two'); - const resetButton = target.querySelector('button.reset'); - const bufferEl = target.querySelector('p'); + const oneButton = target.querySelector("button.one"); + const twoButton = target.querySelector("button.two"); + const resetButton = target.querySelector("button.reset"); + const bufferEl = target.querySelector("p"); - testUtils.dom.click(oneButton); - testUtils.dom.click(twoButton); - await testUtils.nextTick(); - assert.strictEqual(bufferEl.textContent, '12'); - testUtils.dom.click(resetButton); - await testUtils.nextTick(); - assert.strictEqual(bufferEl.textContent, ''); - testUtils.dom.click(twoButton); - testUtils.dom.click(oneButton); - await testUtils.nextTick(); - assert.strictEqual(bufferEl.textContent, '21'); - }); + testUtils.dom.click(oneButton); + testUtils.dom.click(twoButton); + await testUtils.nextTick(); + assert.strictEqual(bufferEl.textContent, "12"); + testUtils.dom.click(resetButton); + await testUtils.nextTick(); + assert.strictEqual(bufferEl.textContent, ""); + testUtils.dom.click(twoButton); + testUtils.dom.click(oneButton); + await testUtils.nextTick(); + assert.strictEqual(bufferEl.textContent, "21"); }); 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 51e9e39914c..9b9a34f069a 100644 --- a/addons/point_of_sale/static/tests/unit/test_PosPopupController.js +++ b/addons/point_of_sale/static/tests/unit/test_PosPopupController.js @@ -1,23 +1,22 @@ -odoo.define('point_of_sale.tests.PosPopupController', function(require) { - 'use strict'; +/** @odoo-module */ - const PosPopupController = require('point_of_sale.PosPopupController'); - const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); - const PosComponent = require('point_of_sale.PosComponent'); - const makeTestEnvironment = require('web.test_env'); - const testUtils = require('web.test_utils'); - const Registries = require('point_of_sale.Registries'); - const { mount } = require('@web/../tests/helpers/utils'); +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; +const { EventBus, useSubEnv, xml } = owl; - QUnit.module('unit tests for PosPopupController', { - before() { - Registries.Component.freeze(); +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 */ ` " ); } else if (receipt.recipient === timapi.constants.Recipient.cardholder) { this.pos.get_order().selected_paymentline.set_receipt_info(value); @@ -161,7 +170,9 @@ var PaymentSix = PaymentInterface.extend({ _sendTransaction: function (transactionType) { var amount = new timapi.Amount( - Math.round(this.pos.get_order().selected_paymentline.amount / this.pos.currency.rounding), + Math.round( + this.pos.get_order().selected_paymentline.amount / this.pos.currency.rounding + ), timapi.constants.Currency[this.pos.currency.name], this.pos.currency.decimal_places ); @@ -173,6 +184,4 @@ var PaymentSix = PaymentInterface.extend({ }, }); -return PaymentSix; - -}); +export default PaymentSix; diff --git a/addons/pos_stripe/static/src/js/models.js b/addons/pos_stripe/static/src/js/models.js index b1a0498c137..476539ba4bb 100644 --- a/addons/pos_stripe/static/src/js/models.js +++ b/addons/pos_stripe/static/src/js/models.js @@ -1,6 +1,5 @@ -odoo.define('pos_stripe.models', function (require) { -var models = require('point_of_sale.models'); -var PaymentStripe = require('pos_stripe.payment'); +/** @odoo-module */ +import { register_payment_method } from "@point_of_sale/js/models"; +import PaymentStripe from "@pos_stripe/js/payment_stripe"; -models.register_payment_method('stripe', PaymentStripe); -}); +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 b5f41646e23..ef3282b7b1b 100644 --- a/addons/pos_stripe/static/src/js/payment_stripe.js +++ b/addons/pos_stripe/static/src/js/payment_stripe.js @@ -1,38 +1,40 @@ +/** @odoo-module */ /* global StripeTerminal */ -odoo.define('pos_stripe.payment', function (require) { -"use strict"; -const core = require('web.core'); -const rpc = require('web.rpc'); -const PaymentInterface = require('point_of_sale.PaymentInterface'); -const { Gui } = require('point_of_sale.Gui'); +import core from "web.core"; +import rpc from "web.rpc"; +import PaymentInterface from "@point_of_sale/js/payment"; +import { Gui } from "@point_of_sale/js/Gui"; const _t = core._t; -let PaymentStripe = PaymentInterface.extend({ +const PaymentStripe = PaymentInterface.extend({ init: function (pos, payment_method) { this._super(...arguments); this.terminal = StripeTerminal.create({ - onFetchConnectionToken: this.stripeFetchConnectionToken.bind(this), - onUnexpectedReaderDisconnect: this.stripeUnexpectedDisconnect.bind(this), + onFetchConnectionToken: this.stripeFetchConnectionToken.bind(this), + onUnexpectedReaderDisconnect: this.stripeUnexpectedDisconnect.bind(this), }); this.discoverReaders(); }, stripeUnexpectedDisconnect: function () { - // Include a way to attempt to reconnect to a reader ? - this._showError(_t('Reader disconnected')); + // Include a way to attempt to reconnect to a reader ? + this._showError(_t("Reader disconnected")); }, stripeFetchConnectionToken: async function () { // Do not cache or hardcode the ConnectionToken. try { - let data = await rpc.query({ - model: 'pos.payment.method', - method: 'stripe_connection_token', - }, { - silent: true, - }); + const data = await rpc.query( + { + model: "pos.payment.method", + method: "stripe_connection_token", + }, + { + silent: true, + } + ); if (data.error) { throw data.error; } @@ -40,15 +42,15 @@ let PaymentStripe = PaymentInterface.extend({ } catch (error) { this._showError(error.message); return false; - }; + } }, discoverReaders: async function () { - let discoverResult = await this.terminal.discoverReaders({}); + const discoverResult = await this.terminal.discoverReaders({}); if (discoverResult.error) { - this._showError(_.str.sprintf(_t('Failed to discover: %s'), discoverResult.error)); + this._showError(_.str.sprintf(_t("Failed to discover: %s"), discoverResult.error)); } else if (discoverResult.discoveredReaders.length === 0) { - this._showError(_t('No available Stripe readers.')); + this._showError(_t("No available Stripe readers.")); } else { // Need to stringify all Readers to avoid to put the array into a proxy Object not interpretable // for the Stripe SDK @@ -57,22 +59,22 @@ let PaymentStripe = PaymentInterface.extend({ }, checkReader: async function () { - let line = this.pos.get_order().selected_paymentline; + const line = this.pos.get_order().selected_paymentline; // Because the reader can only connect to one instance of the SDK at a time. // We need the disconnect this reader if we want to use another one if ( this.pos.connectedReader != this.payment_method.stripe_serial_number && - this.terminal.getConnectionStatus() == 'connected' - ) { - let disconnectResult = await this.terminal.disconnectReader(); + this.terminal.getConnectionStatus() == "connected" + ) { + const disconnectResult = await this.terminal.disconnectReader(); if (disconnectResult.error) { this._showError(disconnectResult.error.message, disconnectResult.error.code); - line.set_payment_status('retry'); + line.set_payment_status("retry"); return false; } else { return await this.connectReader(); } - } else if (this.terminal.getConnectionStatus() == 'not_connected') { + } else if (this.terminal.getConnectionStatus() == "not_connected") { return await this.connectReader(); } else { return true; @@ -80,14 +82,16 @@ let PaymentStripe = PaymentInterface.extend({ }, connectReader: async function () { - let line = this.pos.get_order().selected_paymentline; - let discoveredReaders = JSON.parse(this.pos.discoveredReaders); + const line = this.pos.get_order().selected_paymentline; + const discoveredReaders = JSON.parse(this.pos.discoveredReaders); for (const selectedReader of discoveredReaders) { if (selectedReader.serial_number == this.payment_method.stripe_serial_number) { - let connectResult = await this.terminal.connectReader(selectedReader, {fail_if_in_use: true}); + const connectResult = await this.terminal.connectReader(selectedReader, { + fail_if_in_use: true, + }); if (connectResult.error) { this._showError(connectResult.error.message, connectResult.error.code); - line.set_payment_status('retry'); + line.set_payment_status("retry"); return false; } else { this.pos.connectedReader = this.payment_method.stripe_serial_number; @@ -95,57 +99,64 @@ let PaymentStripe = PaymentInterface.extend({ } } } - this._showError(_.str.sprintf( - this.env._t('Stripe readers %s not listed in your account'), - this.payment_method.stripe_serial_number - )); + this._showError( + _.str.sprintf( + this.env._t("Stripe readers %s not listed in your account"), + this.payment_method.stripe_serial_number + ) + ); }, collectPayment: async function (amount) { - let line = this.pos.get_order().selected_paymentline; - let clientSecret = await this.fetchPaymentIntentClientSecret(line.payment_method, amount); + const line = this.pos.get_order().selected_paymentline; + const clientSecret = await this.fetchPaymentIntentClientSecret(line.payment_method, amount); if (!clientSecret) { - line.set_payment_status('retry'); + line.set_payment_status("retry"); return false; } - line.set_payment_status('waitingCard'); - let collectPaymentMethod = await this.terminal.collectPaymentMethod(clientSecret); + line.set_payment_status("waitingCard"); + const collectPaymentMethod = await this.terminal.collectPaymentMethod(clientSecret); if (collectPaymentMethod.error) { this._showError(collectPaymentMethod.error.message, collectPaymentMethod.error.code); - line.set_payment_status('retry'); + line.set_payment_status("retry"); return false; } else { - line.set_payment_status('waitingCapture'); - let processPayment = await this.terminal.processPayment(collectPaymentMethod.paymentIntent); + line.set_payment_status("waitingCapture"); + const processPayment = await this.terminal.processPayment( + collectPaymentMethod.paymentIntent + ); line.transaction_id = collectPaymentMethod.paymentIntent.id; if (processPayment.error) { this._showError(processPayment.error.message, processPayment.error.code); - line.set_payment_status('retry'); + line.set_payment_status("retry"); return false; } else if (processPayment.paymentIntent) { - line.set_payment_status('waitingCapture'); + line.set_payment_status("waitingCapture"); await this.captureAfterPayment(processPayment, line); - line.set_payment_status('done'); + line.set_payment_status("done"); return true; } } }, captureAfterPayment: async function (processPayment, line) { - let capturePayment = await this.capturePayment(processPayment.paymentIntent.id); + const capturePayment = await this.capturePayment(processPayment.paymentIntent.id); line.card_type = capturePayment.charges.data[0].payment_method_details.card_present.brand; line.transaction_id = capturePayment.id; }, capturePayment: async function (paymentIntentId) { try { - let data = await rpc.query({ - model: 'pos.payment.method', - method: 'stripe_capture_payment', - args: [paymentIntentId], - }, { - silent: true, - }); + const data = await rpc.query( + { + model: "pos.payment.method", + method: "stripe_capture_payment", + args: [paymentIntentId], + }, + { + silent: true, + } + ); if (data.error) { throw data.error; } @@ -153,18 +164,21 @@ let PaymentStripe = PaymentInterface.extend({ } catch (error) { this._showError(error.message); return false; - }; + } }, fetchPaymentIntentClientSecret: async function (payment_method, amount) { try { - let data = await rpc.query({ - model: 'pos.payment.method', - method: 'stripe_payment_intent', - args: [[payment_method.id], amount], - }, { - silent: true, - }); + const data = await rpc.query( + { + model: "pos.payment.method", + method: "stripe_payment_intent", + args: [[payment_method.id], amount], + }, + { + silent: true, + } + ); if (data.error) { throw data.error; } @@ -172,7 +186,7 @@ let PaymentStripe = PaymentInterface.extend({ } catch (error) { this._showError(error.message); return false; - }; + } }, send_payment_request: async function (cid) { @@ -180,8 +194,8 @@ let PaymentStripe = PaymentInterface.extend({ * Override */ await this._super.apply(this, arguments); - let line = this.pos.get_order().selected_paymentline; - line.set_payment_status('waiting'); + const line = this.pos.get_order().selected_paymentline; + line.set_payment_status("waiting"); if (await this.checkReader()) { return await this.collectPayment(line.amount); } else { @@ -194,22 +208,25 @@ let PaymentStripe = PaymentInterface.extend({ * Override */ this._super.apply(this, arguments); - let line = this.pos.get_order().selected_paymentline; - let stripeCancel = await this.stripeCancel(); + const line = this.pos.get_order().selected_paymentline; + const stripeCancel = await this.stripeCancel(); if (stripeCancel) { - line.set_payment_status('retry'); + line.set_payment_status("retry"); return true; } }, stripeCancel: async function () { - if (this.terminal.getConnectionStatus() != 'connected') { - this._showError(_t('Payment canceled because not reader connected')); + if (this.terminal.getConnectionStatus() != "connected") { + this._showError(_t("Payment canceled because not reader connected")); return true; } else { - let cancelCollectPaymentMethod = await this.terminal.cancelCollectPaymentMethod(); + const cancelCollectPaymentMethod = await this.terminal.cancelCollectPaymentMethod(); if (cancelCollectPaymentMethod.error) { - this._showError(cancelCollectPaymentMethod.error.message, cancelCollectPaymentMethod.error.code); + this._showError( + cancelCollectPaymentMethod.error.message, + cancelCollectPaymentMethod.error.code + ); } return true; } @@ -219,14 +236,13 @@ let PaymentStripe = PaymentInterface.extend({ _showError: function (msg, title) { if (!title) { - title = _t('Stripe Error'); + title = _t("Stripe Error"); } - Gui.showPopup('ErrorPopup',{ - 'title': title, - 'body': msg, + Gui.showPopup("ErrorPopup", { + title: title, + body: msg, }); }, }); -return PaymentStripe; -}); +export default PaymentStripe; diff --git a/addons/web/tooling/_eslintignore b/addons/web/tooling/_eslintignore index ba3f1b7b2ed..258f22f8335 100644 --- a/addons/web/tooling/_eslintignore +++ b/addons/web/tooling/_eslintignore @@ -151,3 +151,63 @@ addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js # Whitelist mail !addons/mail/ !addons/mail/**/* + +# Whitelist point_of_sale +!addons/point_of_sale/ +!addons/point_of_sale/**/* + +# Whitelist community pos modules +!addons/l10n_co_pos +!addons/l10n_co_pos/**/* +!addons/l10n_fr_pos_cert +!addons/l10n_fr_pos_cert/**/* +!addons/l10n_gcc_pos +!addons/l10n_gcc_pos/**/* +!addons/l10n_in_pos +!addons/l10n_in_pos/**/* +!addons/l10n_sa_pos +!addons/l10n_sa_pos/**/* +!addons/pos_adyen +!addons/pos_adyen/**/* +!addons/pos_discount +!addons/pos_discount/**/* +!addons/pos_epson_printer +!addons/pos_epson_printer/**/* +!addons/pos_epson_printer_restaurant +!addons/pos_epson_printer_restaurant/**/* +!addons/pos_hr +!addons/pos_hr/**/* +!addons/pos_loyalty +!addons/pos_loyalty/**/* +!addons/pos_mercury +!addons/pos_mercury/**/* +!addons/pos_restaurant +!addons/pos_restaurant/**/* +!addons/pos_restaurant_adyen +!addons/pos_restaurant_adyen/**/* +!addons/pos_restaurant_stripe +!addons/pos_restaurant_stripe/**/* +!addons/pos_sale +!addons/pos_sale/**/* +!addons/pos_sale_product_configurator +!addons/pos_sale_product_configurator/**/* +!addons/pos_six +!addons/pos_six/**/* +!addons/pos_stripe +!addons/pos_stripe/**/* + +# Whitelist enterprise pos modules +!l10n_de_pos_cert +!l10n_de_pos_cert/**/* +!l10n_de_pos_res_cert +!l10n_de_pos_res_cert/**/* +!pos_hr_mobile +!pos_hr_mobile/**/* +!pos_iot +!pos_iot/**/* +!pos_l10n_se +!pos_l10n_se/**/* +!pos_restaurant_iot +!pos_restaurant_iot/**/* +!pos_settle_due +!pos_settle_due/**/* \ No newline at end of file diff --git a/addons/web/tooling/_jsconfig.json b/addons/web/tooling/_jsconfig.json index bbea2ce520d..1ba6742f9de 100644 --- a/addons/web/tooling/_jsconfig.json +++ b/addons/web/tooling/_jsconfig.json @@ -20,7 +20,27 @@ "@web_grid/*": ["web_grid/static/src/*"], "@web_studio/*": ["web_studio/static/src/*"], "@mail/*": ["addons/mail/static/src/*"], - "@pos/*": ["addons/pos/static/src/*"] + "@point_of_sale/*": ["addons/point_of_sale/static/src/*"], + + "@l10n_co_pos/*": ["addons/l10n_co_pos/static/src/*"], + "@l10n_fr_pos_cert/*": ["addons/l10n_fr_pos_cert/static/src/*"], + "@l10n_gcc_pos/*": ["addons/l10n_gcc_pos/static/src/*"], + "@l10n_in_pos/*": ["addons/l10n_in_pos/static/src/*"], + "@l10n_sa_pos/*": ["addons/l10n_sa_pos/static/src/*"], + "@pos_adyen/*": ["addons/pos_adyen/static/src/*"], + "@pos_discount/*": ["addons/pos_discount/static/src/*"], + "@pos_epson_printer/*": ["addons/pos_epson_printer/static/src/*"], + "@pos_epson_printer_restaurant/*": ["addons/pos_epson_printer_restaurant/static/src/*"], + "@pos_hr/*": ["addons/pos_hr/static/src/*"], + "@pos_loyalty/*": ["addons/pos_loyalty/static/src/*"], + "@pos_mercury/*": ["addons/pos_mercury/static/src/*"], + "@pos_restaurant/*": ["addons/pos_restaurant/static/src/*"], + "@pos_restaurant_adyen/*": ["addons/pos_restaurant_adyen/static/src/*"], + "@pos_restaurant_stripe/*": ["addons/pos_restaurant_stripe/static/src/*"], + "@pos_sale/*": ["addons/pos_sale/static/src/*"], + "@pos_sale_product_configurator/*": ["addons/pos_sale_product_configurator/static/src/*"], + "@pos_six/*": ["addons/pos_six/static/src/*"], + "@pos_stripe/*": ["addons/pos_stripe/static/src/*"] } }, "include": ["**/*.js", "**/*.ts"],