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"],