diff --git a/addons/l10n_co_pos/static/src/js/PaymentScreen.js b/addons/l10n_co_pos/static/src/js/PaymentScreen.js index f4053645395..47225819dae 100644 --- a/addons/l10n_co_pos/static/src/js/PaymentScreen.js +++ b/addons/l10n_co_pos/static/src/js/PaymentScreen.js @@ -1,30 +1,26 @@ /** @odoo-module */ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; import session from "web.session"; -const L10nCoPosPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - async _postPushOrderResolve(order, order_server_ids) { - try { - if (this.env.pos.is_colombian_country()) { - const result = await this.rpc({ - model: "pos.order", - method: "search_read", - domain: [["id", "in", order_server_ids]], - fields: ["name"], - context: session.user_context, - }); - order.set_l10n_co_dian(result[0].name || false); - } - } catch { - // FIXME this doesn't seem correct but is equivalent to return in finally which we had before. +patch(PaymentScreen.prototype, "l10n_co_pos.PaymentScreen", { + async _postPushOrderResolve(order, order_server_ids) { + const _super = this._super; + try { + if (this.env.pos.is_colombian_country()) { + const result = await this.rpc({ + model: "pos.order", + method: "search_read", + domain: [["id", "in", order_server_ids]], + fields: ["name"], + context: session.user_context, + }); + order.set_l10n_co_dian(result[0].name || false); } - return super._postPushOrderResolve(...arguments); + } catch { + // FIXME this doesn't seem correct but is equivalent to return in finally which we had before. } - }; - -Registries.Component.extend(PaymentScreen, L10nCoPosPaymentScreen); - -export default PaymentScreen; + return _super(...arguments); + }, +}); diff --git a/addons/l10n_co_pos/static/src/js/pos.js b/addons/l10n_co_pos/static/src/js/pos.js index 386b5aefead..2abfbe8dc2c 100644 --- a/addons/l10n_co_pos/static/src/js/pos.js +++ b/addons/l10n_co_pos/static/src/js/pos.js @@ -1,33 +1,29 @@ /** @odoo-module */ import { PosGlobalState, Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const L10nCoPosGlobalState = (PosGlobalState) => - class L10nCoPosGlobalState extends PosGlobalState { - is_colombian_country() { - return this.company.country.code === "CO"; - } - }; -Registries.Model.extend(PosGlobalState, L10nCoPosGlobalState); +patch(PosGlobalState.prototype, "l10n_co_pos.PosGlobalState", { + is_colombian_country() { + return this.company.country.code === "CO"; + }, +}); -const L10nCoPosOrder = (Order) => - class L10nCoPosOrder extends Order { - export_for_printing() { - var result = super.export_for_printing(...arguments); - result.l10n_co_dian = this.get_l10n_co_dian(); - return result; - } - set_l10n_co_dian(l10n_co_dian) { - this.l10n_co_dian = l10n_co_dian; - } - get_l10n_co_dian() { - return this.l10n_co_dian; - } - wait_for_push_order() { - var result = super.wait_for_push_order(...arguments); - result = Boolean(result || this.pos.is_colombian_country()); - return result; - } - }; -Registries.Model.extend(Order, L10nCoPosOrder); +patch(Order.prototype, "l10n_co_pos.Order", { + export_for_printing() { + const result = this._super(...arguments); + result.l10n_co_dian = this.get_l10n_co_dian(); + return result; + }, + set_l10n_co_dian(l10n_co_dian) { + this.l10n_co_dian = l10n_co_dian; + }, + get_l10n_co_dian() { + return this.l10n_co_dian; + }, + wait_for_push_order() { + var result = this._super(...arguments); + result = Boolean(result || this.pos.is_colombian_country()); + return result; + }, +}); diff --git a/addons/l10n_fr_pos_cert/static/src/js/Chrome.js b/addons/l10n_fr_pos_cert/static/src/js/Chrome.js index 2221fc96b8a..cec7f81edef 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/Chrome.js +++ b/addons/l10n_fr_pos_cert/static/src/js/Chrome.js @@ -1,24 +1,20 @@ /** @odoo-module */ -import Chrome from "@point_of_sale/js/Chrome"; -import Registries from "@point_of_sale/js/Registries"; +import { Chrome } from "@point_of_sale/js/Chrome"; +import { patch } from "@web/core/utils/patch"; +import { ClosePosPopup } from "@point_of_sale/js/Popups/ClosePosPopup"; -const PosFrCertChrome = (Chrome) => - class extends Chrome { - async start() { - await super.start(); - if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) { - const now = Date.now(); - const limitDate = new Date(this.env.pos.pos_session.start_at); - limitDate.setDate(limitDate.getDate() + 1); - if (limitDate.getTime() < now) { - const info = await this.env.pos.getClosePosInfo(); - this.showPopup("ClosePosPopup", { info: info }); - } +patch(Chrome.prototype, "l10n_fr_pos_cert.Chrome", { + async start() { + await this._super(); + if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) { + const now = Date.now(); + const limitDate = new Date(this.env.pos.pos_session.start_at); + limitDate.setDate(limitDate.getDate() + 1); + if (limitDate.getTime() < now) { + const info = await this.env.pos.getClosePosInfo(); + this.showPopup(ClosePosPopup, { info: info }); } } - }; - -Registries.Component.extend(Chrome, PosFrCertChrome); - -export default Chrome; + }, +}); diff --git a/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js b/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js index 5cad1898023..f4a6485c388 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js +++ b/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js @@ -1,30 +1,26 @@ /** @odoo-module */ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; import session from "web.session"; -const PosFrPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - async _postPushOrderResolve(order, order_server_ids) { - try { - if (this.env.pos.is_french_country()) { - const result = await this.rpc({ - model: "pos.order", - method: "search_read", - domain: [["id", "in", order_server_ids]], - fields: ["l10n_fr_hash"], - context: session.user_context, - }); - order.set_l10n_fr_hash(result[0].l10n_fr_hash || false); - } - } catch { - // FIXME this doesn't seem correct but is equivalent to return in finally which we had before. +patch(PaymentScreen.prototype, "l10n_fr_pos_cert.PaymentScreen", { + async _postPushOrderResolve(order, order_server_ids) { + const _super = this._super; + try { + if (this.env.pos.is_french_country()) { + const result = await this.rpc({ + model: "pos.order", + method: "search_read", + domain: [["id", "in", order_server_ids]], + fields: ["l10n_fr_hash"], + context: session.user_context, + }); + order.set_l10n_fr_hash(result[0].l10n_fr_hash || false); } - return super._postPushOrderResolve(...arguments); + } catch { + // FIXME this doesn't seem correct but is equivalent to return in finally which we had before. } - }; - -Registries.Component.extend(PaymentScreen, PosFrPaymentScreen); - -export default PaymentScreen; + return _super(...arguments); + }, +}); diff --git a/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js b/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js index aa24ac24e2a..9559d68c91d 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js +++ b/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js @@ -1,25 +1,20 @@ /** @odoo-module */ -import ClosePosPopup from "@point_of_sale/js/Popups/ClosePosPopup"; -import Registries from "@point_of_sale/js/Registries"; +import { ClosePosPopup } from "@point_of_sale/js/Popups/ClosePosPopup"; +import { patch } from "@web/core/utils/patch"; -const PosFrCertClosePopup = (ClosePosPopup) => - class extends ClosePosPopup { - sessionIsOutdated() { - let isOutdated = false; - if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) { - const now = Date.now(); - const limitDate = new Date(this.env.pos.pos_session.start_at); - limitDate.setDate(limitDate.getDate() + 1); - isOutdated = limitDate < now; - } - return isOutdated; +patch(ClosePosPopup.prototype, "l10n_fr_pos_cert.ClosePosPopup", { + sessionIsOutdated() { + let isOutdated = false; + if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) { + const now = Date.now(); + const limitDate = new Date(this.env.pos.pos_session.start_at); + limitDate.setDate(limitDate.getDate() + 1); + isOutdated = limitDate < now; } - canCancel() { - return super.canCancel() && !this.sessionIsOutdated(); - } - }; - -Registries.Component.extend(ClosePosPopup, PosFrCertClosePopup); - -export default ClosePosPopup; + return isOutdated; + }, + canCancel() { + return this._super() && !this.sessionIsOutdated(); + }, +}); diff --git a/addons/l10n_fr_pos_cert/static/src/js/pos.js b/addons/l10n_fr_pos_cert/static/src/js/pos.js index 2e85a7bacac..3633c585e71 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/pos.js +++ b/addons/l10n_fr_pos_cert/static/src/js/pos.js @@ -3,91 +3,86 @@ import { Gui } from "@point_of_sale/js/Gui"; import { PosGlobalState, Order, Orderline } from "@point_of_sale/js/models"; import core from "web.core"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; var _t = core._t; -const L10nFrPosGlobalState = (PosGlobalState) => - class L10nFrPosGlobalState extends PosGlobalState { - is_french_country() { - var french_countries = ["FR", "MF", "MQ", "NC", "PF", "RE", "GF", "GP", "TF"]; - if (!this.company.country) { - Gui.showPopup("ErrorPopup", { - title: _t("Missing Country"), - body: _.str.sprintf( - _t("The company %s doesn't have a country set."), - this.company.name - ), - }); - return false; - } - return _.contains(french_countries, this.company.country.code); +patch(PosGlobalState.prototype, "l10n_fr_pos_cert.PosGlobalState", { + is_french_country() { + var french_countries = ["FR", "MF", "MQ", "NC", "PF", "RE", "GF", "GP", "TF"]; + if (!this.company.country) { + Gui.showPopup(ErrorPopup, { + title: _t("Missing Country"), + body: _.str.sprintf( + _t("The company %s doesn't have a country set."), + this.company.name + ), + }); + return false; } - disallowLineQuantityChange() { - const result = super.disallowLineQuantityChange(...arguments); - return this.is_french_country() || result; - } - }; -Registries.Model.extend(PosGlobalState, L10nFrPosGlobalState); + return _.contains(french_countries, this.company.country.code); + }, + disallowLineQuantityChange() { + const result = this._super(...arguments); + return this.is_french_country() || result; + }, +}); -const L10nFrOrder = (Order) => - class L10nFrOrder extends Order { - constructor() { - super(...arguments); - this.l10n_fr_hash = this.l10n_fr_hash || false; - this.save_to_db(); +patch(Order.prototype, "l10n_fr_pos_cert.Order", { + setup() { + this._super(...arguments); + this.l10n_fr_hash = this.l10n_fr_hash || false; + this.save_to_db(); + }, + export_for_printing() { + var result = this._super(...arguments); + result.l10n_fr_hash = this.get_l10n_fr_hash(); + return result; + }, + set_l10n_fr_hash(l10n_fr_hash) { + this.l10n_fr_hash = l10n_fr_hash; + }, + get_l10n_fr_hash() { + return this.l10n_fr_hash; + }, + wait_for_push_order() { + var result = this._super(...arguments); + result = Boolean(result || this.pos.is_french_country()); + return result; + }, + destroy(option) { + // SUGGESTION: It's probably more appropriate to apply this restriction + // in the TicketScreen. + if ( + option && + option.reason == "abandon" && + this.pos.is_french_country() && + this.get_orderlines().length + ) { + Gui.showPopup(ErrorPopup, { + title: _t("Fiscal Data Module error"), + body: _t("Deleting of orders is not allowed."), + }); + } else { + this._super(...arguments); } - export_for_printing() { - var result = super.export_for_printing(...arguments); - result.l10n_fr_hash = this.get_l10n_fr_hash(); - return result; - } - set_l10n_fr_hash(l10n_fr_hash) { - this.l10n_fr_hash = l10n_fr_hash; - } - get_l10n_fr_hash() { - return this.l10n_fr_hash; - } - wait_for_push_order() { - var result = super.wait_for_push_order(...arguments); - result = Boolean(result || this.pos.is_french_country()); - return result; - } - destroy(option) { - // SUGGESTION: It's probably more appropriate to apply this restriction - // in the TicketScreen. - if ( - option && - option.reason == "abandon" && - this.pos.is_french_country() && - this.get_orderlines().length - ) { - Gui.showPopup("ErrorPopup", { - title: _t("Fiscal Data Module error"), - body: _t("Deleting of orders is not allowed."), - }); - } else { - super.destroy(...arguments); - } - } - }; -Registries.Model.extend(Order, L10nFrOrder); + }, +}); -const L10nFrOrderline = (Orderline) => - class L10nFrOrderline extends Orderline { - can_be_merged_with(orderline) { - const order = this.pos.get_order(); - const orderlines = order.orderlines; - const lastOrderline = order.orderlines.at(orderlines.length - 1); +patch(Orderline.prototype, "l10n_fr_pos_cert.Orderline", { + can_be_merged_with(orderline) { + const order = this.pos.get_order(); + const orderlines = order.orderlines; + const lastOrderline = order.orderlines.at(orderlines.length - 1); - if ( - this.pos.is_french_country() && - (lastOrderline.product.id !== orderline.product.id || lastOrderline.quantity < 0) - ) { - return false; - } else { - return super.can_be_merged_with(...arguments); - } + if ( + this.pos.is_french_country() && + (lastOrderline.product.id !== orderline.product.id || lastOrderline.quantity < 0) + ) { + return false; + } else { + return this._super(...arguments); } - }; -Registries.Model.extend(Orderline, L10nFrOrderline); + }, +}); diff --git a/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js b/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js index 7d34022c5b1..0fa1073d650 100644 --- a/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js +++ b/addons/l10n_gcc_pos/static/src/js/OrderReceipt.js @@ -1,19 +1,16 @@ /** @odoo-module */ -import OrderReceipt from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt"; -import Registries from "@point_of_sale/js/Registries"; +import { OrderReceipt } from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt"; +import { patch } from "@web/core/utils/patch"; -const OrderReceiptGCC = (OrderReceipt) => - class extends OrderReceipt { - get receiptEnv() { - const receipt_render_env = super.receiptEnv; - const receipt = receipt_render_env.receipt; - const country = receipt_render_env.order.pos.company.country; - receipt.is_gcc_country = ["SA", "AE", "BH", "OM", "QA", "KW"].includes( - country && country.code - ); - return receipt_render_env; - } - }; -Registries.Component.extend(OrderReceipt, OrderReceiptGCC); -export default OrderReceiptGCC; +patch(OrderReceipt.prototype, "l10n_gcc_pos.OrderReceipt", { + get receiptEnv() { + const receipt_render_env = this._super(); + const receipt = receipt_render_env.receipt; + const country = receipt_render_env.order.pos.company.country; + receipt.is_gcc_country = ["SA", "AE", "BH", "OM", "QA", "KW"].includes( + country && country.code + ); + return receipt_render_env; + }, +}); diff --git a/addons/l10n_in_pos/static/src/js/receipt.js b/addons/l10n_in_pos/static/src/js/receipt.js index f3218cb2195..5203eae852f 100644 --- a/addons/l10n_in_pos/static/src/js/receipt.js +++ b/addons/l10n_in_pos/static/src/js/receipt.js @@ -1,14 +1,12 @@ /** @odoo-module */ import { Orderline } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const L10nInOrderline = (Orderline) => - class L10nInOrderline extends Orderline { - export_for_printing() { - var line = super.export_for_printing(...arguments); - line.l10n_in_hsn_code = this.get_product().l10n_in_hsn_code; - return line; - } - }; -Registries.Model.extend(Orderline, L10nInOrderline); +patch(Orderline.prototype, "l10n_in_pos.Orderline", { + export_for_printing() { + const line = this._super(...arguments); + line.l10n_in_hsn_code = this.get_product().l10n_in_hsn_code; + return line; + }, +}); diff --git a/addons/l10n_sa_pos/static/src/js/models.js b/addons/l10n_sa_pos/static/src/js/models.js index c1485b589e1..b16347a193f 100644 --- a/addons/l10n_sa_pos/static/src/js/models.js +++ b/addons/l10n_sa_pos/static/src/js/models.js @@ -1,79 +1,75 @@ /** @odoo-module */ import { Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const PosL10nSAOrder = (Order) => - class PosL10nSAOrder extends Order { - export_for_printing() { - var result = super.export_for_printing(...arguments); - if (this.pos.company.country.code === "SA") { - result.is_settlement = this.is_settlement(); - if (!result.is_settlement) { - const codeWriter = new window.ZXing.BrowserQRCodeSvgWriter(); - const qr_values = this.compute_sa_qr_code( - result.company.name, - result.company.vat, - result.date.isostring, - result.total_with_tax, - result.total_tax - ); - const qr_code_svg = new XMLSerializer().serializeToString( - codeWriter.write(qr_values, 150, 150) - ); - result.qr_code = "data:image/svg+xml;base64," + window.btoa(qr_code_svg); - } +patch(Order.prototype, "l10n_sa_pos.Order", { + export_for_printing() { + const result = this._super(...arguments); + if (this.pos.company.country.code === "SA") { + result.is_settlement = this.is_settlement(); + if (!result.is_settlement) { + const codeWriter = new window.ZXing.BrowserQRCodeSvgWriter(); + const qr_values = this.compute_sa_qr_code( + result.company.name, + result.company.vat, + result.date.isostring, + result.total_with_tax, + result.total_tax + ); + const qr_code_svg = new XMLSerializer().serializeToString( + codeWriter.write(qr_values, 150, 150) + ); + result.qr_code = "data:image/svg+xml;base64," + window.btoa(qr_code_svg); } - return result; } - /** - * If the order is empty (there are no products) - * and all "pay_later" payments are negative, - * we are settling a customer's account. - * If the module pos_settle_due is not installed, - * the function always returns false (since "pay_later" doesn't exist) - * @returns {boolean} true if the current order is a settlement, else false - */ - is_settlement() { - return ( - this.is_empty() && - !!this.paymentlines.filter( - (paymentline) => - paymentline.payment_method.type === "pay_later" && paymentline.amount < 0 - ).length - ); + return result; + }, + /** + * If the order is empty (there are no products) + * and all "pay_later" payments are negative, + * we are settling a customer's account. + * If the module pos_settle_due is not installed, + * the function always returns false (since "pay_later" doesn't exist) + * @returns {boolean} true if the current order is a settlement, else false + */ + is_settlement() { + return ( + this.is_empty() && + !!this.paymentlines.filter( + (paymentline) => + paymentline.payment_method.type === "pay_later" && paymentline.amount < 0 + ).length + ); + }, + compute_sa_qr_code(name, vat, date_isostring, amount_total, amount_tax) { + /* Generate the qr code for Saudi e-invoicing. Specs are available at the following link at page 23 + https://zatca.gov.sa/ar/E-Invoicing/SystemsDevelopers/Documents/20210528_ZATCA_Electronic_Invoice_Security_Features_Implementation_Standards_vShared.pdf + */ + const seller_name_enc = this._compute_qr_code_field(1, name); + const company_vat_enc = this._compute_qr_code_field(2, vat); + const timestamp_enc = this._compute_qr_code_field(3, date_isostring); + const invoice_total_enc = this._compute_qr_code_field(4, amount_total.toString()); + const total_vat_enc = this._compute_qr_code_field(5, amount_tax.toString()); + + const str_to_encode = seller_name_enc.concat( + company_vat_enc, + timestamp_enc, + invoice_total_enc, + total_vat_enc + ); + + let binary = ""; + for (let i = 0; i < str_to_encode.length; i++) { + binary += String.fromCharCode(str_to_encode[i]); } - - compute_sa_qr_code(name, vat, date_isostring, amount_total, amount_tax) { - /* Generate the qr code for Saudi e-invoicing. Specs are available at the following link at page 23 - https://zatca.gov.sa/ar/E-Invoicing/SystemsDevelopers/Documents/20210528_ZATCA_Electronic_Invoice_Security_Features_Implementation_Standards_vShared.pdf - */ - const seller_name_enc = this._compute_qr_code_field(1, name); - const company_vat_enc = this._compute_qr_code_field(2, vat); - const timestamp_enc = this._compute_qr_code_field(3, date_isostring); - const invoice_total_enc = this._compute_qr_code_field(4, amount_total.toString()); - const total_vat_enc = this._compute_qr_code_field(5, amount_tax.toString()); - - const str_to_encode = seller_name_enc.concat( - company_vat_enc, - timestamp_enc, - invoice_total_enc, - total_vat_enc - ); - - let binary = ""; - for (let i = 0; i < str_to_encode.length; i++) { - binary += String.fromCharCode(str_to_encode[i]); - } - return btoa(binary); - } - - _compute_qr_code_field(tag, field) { - const textEncoder = new TextEncoder(); - const name_byte_array = Array.from(textEncoder.encode(field)); - const name_tag_encoding = [tag]; - const name_length_encoding = [name_byte_array.length]; - return name_tag_encoding.concat(name_length_encoding, name_byte_array); - } - }; -Registries.Model.extend(Order, PosL10nSAOrder); + return btoa(binary); + }, + _compute_qr_code_field(tag, field) { + const textEncoder = new TextEncoder(); + const name_byte_array = Array.from(textEncoder.encode(field)); + const name_tag_encoding = [tag]; + const name_length_encoding = [name_byte_array.length]; + return name_tag_encoding.concat(name_length_encoding, name_byte_array); + }, +}); diff --git a/addons/point_of_sale/static/src/app/debug_widget/debug_widget.js b/addons/point_of_sale/static/src/app/debug_widget/debug_widget.js index be2b992a424..378e34bf120 100644 --- a/addons/point_of_sale/static/src/app/debug_widget/debug_widget.js +++ b/addons/point_of_sale/static/src/app/debug_widget/debug_widget.js @@ -1,12 +1,14 @@ /** @odoo-module */ import { parse } from "web.field_utils"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; -import PosComponent from "@point_of_sale/js/PosComponent"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { usePos } from "@point_of_sale/app/pos_store"; import { registry } from "@web/core/registry"; import { Transition } from "@web/core/transition"; -import Draggable from "@point_of_sale/js/Misc/Draggable"; +import { Draggable } from "@point_of_sale/js/Misc/Draggable"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; +import { OrderImportPopup } from "@point_of_sale/js/Popups/OrderImportPopup"; const { onMounted, onWillUnmount, useRef, useState } = owl; @@ -21,7 +23,7 @@ export class DebugWidget extends PosComponent { weightInput: "", isPaidOrdersReady: false, isUnpaidOrdersReady: false, - buffer: NumberBuffer.get(), + buffer: numberBuffer.get(), }); // NOTE: Perhaps this can still be improved. @@ -50,11 +52,11 @@ export class DebugWidget extends PosComponent { } onMounted(() => { - NumberBuffer.on("buffer-update", this, this._onBufferUpdate); + numberBuffer.on("buffer-update", this, this._onBufferUpdate); }); onWillUnmount(() => { - NumberBuffer.off("buffer-update", this, this._onBufferUpdate); + numberBuffer.off("buffer-update", this, this._onBufferUpdate); }); } toggleWidget() { @@ -81,7 +83,7 @@ export class DebugWidget extends PosComponent { await this.env.barcode_reader.scan(ean); } async deleteOrders() { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Delete Paid Orders ?"), body: this.env._t( "This operation will permanently destroy all paid orders from the local storage. You will lose all the data. This operation cannot be undone." @@ -93,7 +95,7 @@ export class DebugWidget extends PosComponent { } } async deleteUnpaidOrders() { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Delete Unpaid Orders ?"), body: this.env._t( "This operation will destroy all unpaid orders in the browser. You will lose all the unsaved data and exit the point of sale. This operation cannot be undone." @@ -147,7 +149,7 @@ export class DebugWidget extends PosComponent { const file = event.target.files[0]; if (file) { const report = this.env.pos.import_orders(await file.text()); - await this.showPopup("OrderImportPopup", { report }); + await this.showPopup(OrderImportPopup, { report }); } } refreshDisplay() { diff --git a/addons/point_of_sale/static/src/app/error_handlers/error_handlers.js b/addons/point_of_sale/static/src/app/error_handlers/error_handlers.js index 7b7ffe081d0..8e9ab9ef39d 100644 --- a/addons/point_of_sale/static/src/app/error_handlers/error_handlers.js +++ b/addons/point_of_sale/static/src/app/error_handlers/error_handlers.js @@ -3,7 +3,10 @@ import { registry } from "@web/core/registry"; import { odooExceptionTitleMap } from "@web/core/errors/error_dialogs"; import { ConnectionLostError, RPCError } from "@web/core/network/rpc_service"; -import { Gui } from "@point_of_sale/js/Gui"; +import { Gui, GuiNotReadyError } from "@point_of_sale/js/Gui"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ErrorTracebackPopup } from "@point_of_sale/js/Popups/ErrorTracebackPopup"; +import { OfflineErrorPopup } from "@point_of_sale/js/Popups/OfflineErrorPopup"; export function identifyError(error) { return error && error.legacy ? error.message : error; @@ -15,9 +18,9 @@ function rpcErrorHandler(env, error, originalError) { const { message, data } = error; if (odooExceptionTitleMap.has(error.exceptionName)) { const title = odooExceptionTitleMap.get(error.exceptionName).toString(); - Gui.showPopup("ErrorPopup", { title, body: data.message }); + Gui.showPopup(ErrorPopup, { title, body: data.message }); } else { - Gui.showPopup("ErrorTracebackPopup", { + Gui.showPopup(ErrorTracebackPopup, { title: message, body: data.message + "\n" + data.debug + "\n", }); @@ -30,7 +33,7 @@ registry.category("error_handlers").add("rpcErrorHandler", rpcErrorHandler); function offlineErrorHandler(env, error, originalError) { error = identifyError(originalError); if (error instanceof ConnectionLostError) { - Gui.showPopup("OfflineErrorPopup", { + Gui.showPopup(OfflineErrorPopup, { title: env._t("Couldn't connect to the server"), body: env._t( "The operation couldn't be completed because you are offline. Check your internet connection and try again." @@ -44,12 +47,12 @@ registry.category("error_handlers").add("offlineErrorHandler", offlineErrorHandl function defaultErrorHandler(env, error, originalError) { error = identifyError(originalError); if (error instanceof Error) { - Gui.showPopup("ErrorTracebackPopup", { + Gui.showPopup(ErrorTracebackPopup, { title: `${error.name}: ${error.message}`, body: error.traceback, }); } else { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: env._t("Unknown Error"), body: env._t("Unable to show information about this error."), }); @@ -59,3 +62,12 @@ function defaultErrorHandler(env, error, originalError) { registry .category("error_handlers") .add("defaultErrorHandler", defaultErrorHandler, { sequence: 99 }); + +function guiNotReadyHandler(env, error, originalError) { + error = identifyError(originalError); + if (error instanceof GuiNotReadyError) { + console.error(originalError); + return true; + } +} +registry.category("error_handlers").add("guiNotReadyHandler", guiNotReadyHandler); diff --git a/addons/point_of_sale/static/src/app/navbar/navbar.js b/addons/point_of_sale/static/src/app/navbar/navbar.js index 3e6fe88a877..a90c56327f6 100644 --- a/addons/point_of_sale/static/src/app/navbar/navbar.js +++ b/addons/point_of_sale/static/src/app/navbar/navbar.js @@ -1,10 +1,30 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { usePos } from "@point_of_sale/app/pos_store"; +import { CashierName } from "@point_of_sale/js/ChromeWidgets/CashierName"; +import { CashMoveButton } from "@point_of_sale/js/ChromeWidgets/CashMoveButton"; +import { CustomerFacingDisplayButton } from "@point_of_sale/js/ChromeWidgets/CustomerFacingDisplayButton"; +import { HeaderButton } from "@point_of_sale/js/ChromeWidgets/HeaderButton"; +import { ProxyStatus } from "@point_of_sale/js/ChromeWidgets/ProxyStatus"; +import { SaleDetailsButton } from "@point_of_sale/js/ChromeWidgets/SaleDetailsButton"; +import { SyncNotification } from "@point_of_sale/js/ChromeWidgets/SyncNotification"; +import { TicketButton } from "@point_of_sale/js/ChromeWidgets/TicketButton"; + export class Navbar extends PosComponent { static template = "point_of_sale.Navbar"; + static components = { + // FIXME POSREF remove some of these components + CashierName, + CashMoveButton, + CustomerFacingDisplayButton, + HeaderButton, + ProxyStatus, + SaleDetailsButton, + SyncNotification, + TicketButton, + }; static props = { showCashMoveButton: Boolean, }; diff --git a/addons/point_of_sale/static/src/entry/main.js b/addons/point_of_sale/static/src/entry/main.js index 72f106e122e..512214219bc 100644 --- a/addons/point_of_sale/static/src/entry/main.js +++ b/addons/point_of_sale/static/src/entry/main.js @@ -1,16 +1,10 @@ /** @odoo-module */ import { startWebClient } from "@web/start"; - import { Chrome } from "@point_of_sale/js/Chrome"; -import Registries from "@point_of_sale/js/Registries"; -function startPosApp() { - Registries.Component.freeze(); - Registries.Model.freeze(); - // For consistency's sake, we should trigger"WEB_CLIENT_READY" on the bus when PosApp is mounted - // But we can't since mail and some other poll react on that cue, and we don't want those services started - startWebClient(Registries.Component.get(Chrome)); -} - -startPosApp(); +// FIXME POSREF no we shouldn't but if we did nothing should react, if it does it shouldn't be loaded +// For consistency's sake, we should trigger"WEB_CLIENT_READY" on the bus when PosApp is mounted +// But we can't since mail and some other poll react on that cue, and we don't want those services started +// FIXME POSREF stop using startWebclient: this is not a web client. +startWebClient(Chrome); diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js index 31cfe4abcff..167839189ed 100644 --- a/addons/point_of_sale/static/src/js/Chrome.js +++ b/addons/point_of_sale/static/src/js/Chrome.js @@ -3,10 +3,9 @@ import { loadCSS } from "@web/core/assets"; import { useListener, useBus, useService } from "@web/core/utils/hooks"; import BarcodeParser from "barcodes.BarcodeParser"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; -import Registries from "@point_of_sale/js/Registries"; -import IndependentToOrderScreen from "@point_of_sale/js/Misc/IndependentToOrderScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { IndependentToOrderScreen } from "@point_of_sale/js/Misc/IndependentToOrderScreen"; import { batched } from "@point_of_sale/js/utils"; import { debounce } from "@web/core/utils/timing"; import { Transition } from "@web/core/transition"; @@ -15,11 +14,17 @@ import { WithEnv } from "@web/core/utils/components"; import { Navbar } from "@point_of_sale/app/navbar/navbar"; // ChromeAdapter imports -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { PosGlobalState } from "@point_of_sale/js/models"; import { configureGui } from "@point_of_sale/js/Gui"; import { registry } from "@web/core/registry"; -import env from "@point_of_sale/js/pos_env"; +import { pos_env as env } from "@point_of_sale/js/pos_env"; + +import { Notification } from "./Notification"; +import { PosPopupController } from "./Popups/PosPopupController"; +import { ErrorTracebackPopup } from "./Popups/ErrorTracebackPopup"; +import { CashOpeningPopup } from "./Popups/CashOpeningPopup"; +import { ConfirmPopup } from "./Popups/ConfirmPopup"; import { onMounted, @@ -36,6 +41,7 @@ import { usePos } from "@point_of_sale/app/pos_store"; * Chrome is the root component of the PoS App. */ export class Chrome extends PosComponent { + static template = "Chrome"; // FIXME POSREF namespace templates setup() { // BEGIN ChromeAdapter ProductScreen.sortControlButtons(); @@ -43,7 +49,7 @@ export class Chrome extends PosComponent { // Instantiate PosGlobalState here to ensure that every extension // (or class overloads) is taken into consideration. - const pos = PosGlobalState.create({ env: markRaw(env) }); + const pos = new PosGlobalState({ env: markRaw(env) }); this.batchedCustomerDisplayRender = batched(() => { reactivePos.send_current_order_to_customer_facing_display(); @@ -95,7 +101,7 @@ export class Chrome extends PosComponent { useListener("close-notification", this._onCloseNotification); useListener("connect-to-proxy", this.connect_to_proxy); useBus(this.env.posbus, "start-cash-control", this.openCashControl); - NumberBuffer.activate(); + numberBuffer.activate(); this.state = usePos(); @@ -207,7 +213,7 @@ export class Chrome extends PosComponent { } } - return this.showPopup("ErrorTracebackPopup", { title, body, exitButtonIsShown: true }); + return this.showPopup(ErrorTracebackPopup, { title, body, exitButtonIsShown: true }); } registry.category("main_components").add("BlockUI", BlockUiFromRegistry); @@ -239,10 +245,13 @@ export class Chrome extends PosComponent { setupBarcodeParser() { if (!this.env.pos.company.nomenclature_id) { - const errorMessage = this.env._t("The barcode nomenclature setting is not configured. " + - "Make sure to configure it on your Point of Sale configuration settings"); - throw new Error(this.env._t("Missing barcode nomenclature"), { cause: { message: errorMessage } }); - + const errorMessage = this.env._t( + "The barcode nomenclature setting is not configured. " + + "Make sure to configure it on your Point of Sale configuration settings" + ); + throw new Error(this.env._t("Missing barcode nomenclature"), { + cause: { message: errorMessage }, + }); } const barcode_parser = new BarcodeParser({ nomenclature_id: this.env.pos.company.nomenclature_id, @@ -292,7 +301,7 @@ export class Chrome extends PosComponent { openCashControl() { if (this.shouldShowCashControl()) { - this.showPopup("CashOpeningPopup", { keepBehind: true }); + this.showPopup(CashOpeningPopup, { keepBehind: true }); } } @@ -315,7 +324,7 @@ export class Chrome extends PosComponent { const { name, props, resolve } = event.detail; this.state.tempScreen = { name, - component: this.constructor.components[name], + component: registry.category("pos_screens").get(name), props: { ...props, resolve }, }; } @@ -323,7 +332,7 @@ export class Chrome extends PosComponent { this.state.tempScreen = null; } __showScreen({ detail: { name, props = {} } }) { - const component = this.constructor.components[name]; + const component = registry.category("pos_screens").get(name); // 1. Set the information of the screen to display. this.mainScreen.name = name; this.mainScreen.component = component; @@ -383,7 +392,7 @@ export class Chrome extends PosComponent { "not close the session before the issue " + "has been resolved." ); - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Offline Orders"), body: reason, }); @@ -486,7 +495,6 @@ export class Chrome extends PosComponent { return this.env.pos && this.env.pos.config && this.env.pos.config.cash_control; } } -Chrome.template = "Chrome"; Object.defineProperty(Chrome, "components", { get() { return Object.assign( @@ -495,12 +503,10 @@ Object.defineProperty(Chrome, "components", { MainComponentsContainer, WithEnv, Navbar, + PosPopupController, + Notification, }, PosComponent.components ); }, }); - -Registries.Component.add(Chrome); - -export default Chrome; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js index 340a443655b..09d643886ac 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js @@ -1,18 +1,21 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; -import { _t } from "web.core"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { _lt } from "web.core"; import { renderToString } from "@web/core/utils/render"; +import { CashMovePopup } from "../Popups/CashMovePopup"; +import { ErrorPopup } from "../Popups/ErrorPopup"; const TRANSLATED_CASH_MOVE_TYPE = { - in: _t("in"), - out: _t("out"), + in: _lt("in"), + out: _lt("out"), }; -class CashMoveButton extends PosComponent { +export class CashMoveButton extends PosComponent { + static template = "point_of_sale.CashMoveButton"; + async onClick() { - const { confirmed, payload } = await this.showPopup("CashMovePopup"); + const { confirmed, payload } = await this.showPopup(CashMovePopup); if (!confirmed) { return; } @@ -37,7 +40,7 @@ class CashMoveButton extends PosComponent { }); const printResult = await this.env.proxy.printer.print_receipt(renderedReceipt); if (!printResult.successful) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: printResult.message.title, body: printResult.message.body, }); @@ -55,8 +58,3 @@ class CashMoveButton extends PosComponent { return result; } } -CashMoveButton.template = "point_of_sale.CashMoveButton"; - -Registries.Component.add(CashMoveButton); - -export default CashMoveButton; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js b/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js index db19c43c047..f67d7545a78 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CashierName.js @@ -1,10 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; // Previously UsernameWidget -class CashierName extends PosComponent { +export class CashierName extends PosComponent { + static template = "CashierName"; + get username() { const { name } = this.env.pos.get_cashier(); return name ? name : ""; @@ -18,8 +19,3 @@ class CashierName extends PosComponent { return { "not-clickable": true }; } } -CashierName.template = "CashierName"; - -Registries.Component.add(CashierName); - -export default CashierName; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js index 4fce19efca4..76c14d5b633 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CustomerFacingDisplayButton.js @@ -1,11 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { useState } = owl; -class CustomerFacingDisplayButton extends PosComponent { +export class CustomerFacingDisplayButton extends PosComponent { + static template = "CustomerFacingDisplayButton"; + setup() { super.setup(); this.local = @@ -101,8 +102,3 @@ class CustomerFacingDisplayButton extends PosComponent { loop(); } } -CustomerFacingDisplayButton.template = "CustomerFacingDisplayButton"; - -Registries.Component.add(CustomerFacingDisplayButton); - -export default CustomerFacingDisplayButton; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js index ff38bf77b1d..24a32a7352a 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js @@ -1,18 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ClosePosPopup } from "../Popups/ClosePosPopup"; // Previously HeaderButtonWidget // This is the close session button -class HeaderButton extends PosComponent { +export class HeaderButton extends PosComponent { + static template = "HeaderButton"; + async onClick() { const info = await this.env.pos.getClosePosInfo(); - this.showPopup("ClosePosPopup", { info: info, keepBehind: true }); + this.showPopup(ClosePosPopup, { info: info, keepBehind: true }); } } -HeaderButton.template = "HeaderButton"; - -Registries.Component.add(HeaderButton); - -export default HeaderButton; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js b/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js index 618749a59a8..1fd73a7b4b8 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js @@ -1,12 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { onMounted, onWillUnmount, useState } = owl; // Previously ProxyStatusWidget -class ProxyStatus extends PosComponent { +export class ProxyStatus extends PosComponent { + static template = "ProxyStatus"; + setup() { super.setup(); const initialProxyStatus = this.env.proxy.get("status"); @@ -69,8 +70,3 @@ class ProxyStatus extends PosComponent { } } } -ProxyStatus.template = "ProxyStatus"; - -Registries.Component.add(ProxyStatus); - -export default ProxyStatus; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js index 7f847461c28..cc02270d018 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js @@ -1,10 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { renderToString } from "@web/core/utils/render"; +import { ErrorPopup } from "../Popups/ErrorPopup"; + +export class SaleDetailsButton extends PosComponent { + static template = "SaleDetailsButton"; -class SaleDetailsButton extends PosComponent { async onClick() { // IMPROVEMENT: Perhaps put this logic in a parent component // so that for unit testing, we can check if this simple @@ -23,15 +25,10 @@ class SaleDetailsButton extends PosComponent { ); const printResult = await this.env.proxy.printer.print_receipt(report); if (!printResult.successful) { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: printResult.message.title, body: printResult.message.body, }); } } } -SaleDetailsButton.template = "SaleDetailsButton"; - -Registries.Component.add(SaleDetailsButton); - -export default SaleDetailsButton; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js b/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js index b22c1dc0557..393e5287295 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js @@ -1,15 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class SyncNotification extends PosComponent { + static template = "SyncNotification"; -class SyncNotification extends PosComponent { onClick() { this.env.pos.push_orders(null, { show_error: true }); } } -SyncNotification.template = "SyncNotification"; - -Registries.Component.add(SyncNotification); - -export default SyncNotification; diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js index 611efa518fa..a11b33771f5 100644 --- a/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js +++ b/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js @@ -1,10 +1,11 @@ /** @odoo-module */ import { usePos } from "@point_of_sale/app/pos_store"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class TicketButton extends PosComponent { + static template = "TicketButton"; -class TicketButton extends PosComponent { setup() { this.pos = usePos(); } @@ -26,8 +27,3 @@ class TicketButton extends PosComponent { } } } -TicketButton.template = "TicketButton"; - -Registries.Component.add(TicketButton); - -export default TicketButton; diff --git a/addons/point_of_sale/static/src/js/ClassRegistry.js b/addons/point_of_sale/static/src/js/ClassRegistry.js deleted file mode 100644 index 24c66e25f54..00000000000 --- a/addons/point_of_sale/static/src/js/ClassRegistry.js +++ /dev/null @@ -1,260 +0,0 @@ -/** @odoo-module */ - -/** - * **Usage:** - * ``` - * const Registry = new ClassRegistry(); - * - * class A {} - * Registry.add(A); - * - * const AExt1 = A => class extends A {} - * Registry.extend(A, AExt1) - * - * const B = A => class extends A {} - * Registry.addByExtending(B, A) - * - * const AExt2 = A => class extends A {} - * Registry.extend(A, AExt2) - * - * Registry.get(A) - * // above returns: AExt2 -> AExt1 -> A - * // Basically, 'A' in the registry points to - * // the inheritance chain above. - * - * Registry.get(B) - * // above returns: B -> AExt2 -> AExt1 -> A - * // Even though B extends A before applying all - * // the extensions of A, when getting it from the - * // registry, the return points to a class with - * // inheritance chain that includes all the extensions - * // of 'A'. - * - * Registry.freeze() - * // Example 'B' above is lazy. Basically, it is only - * // computed when we call `get` from the registry. - * // If we know that no more dynamic inheritances will happen, - * // we can freeze the registry and cache the final form - * // of each class in the registry. - * ``` - * - * IMPROVEMENT: - * * So far, mixin can be accomplished by creating a method - * the takes a class and returns a class expression. This is then - * used before the extends keyword like so: - * - * ```js - * class A {} - * Registry.add(A) - * const Mixin = x => class extends x {} - * // apply mixin - * // | - * // v - * const B = x => class extends Mixin(x) {} - * Registry.addByExtending(B, A) - * ``` - * - * In the example, `|B| => B -> Mixin -> A`, and this is pretty convenient - * already. However, this can still be improved since classes are only - * compiled after `Registry.freeze()`. Perhaps, we can make the - * Mixins extensible as well, such as so: - * - * ``` - * class A {} - * Registry.add(A) - * const Mixin = x => class extends x {} - * Registry.add(Mixin) - * const OtherMixin = x => class extends x {} - * Registry.add(OtherMixin) - * const B = x => class extends x {} - * Registry.addByExtending(B, A, [Mixin, OtherMixin]) - * const ExtendMixin = x => class extends x {} - * Registry.extend(Mixin, ExtendMixin) - * ``` - * - * In the above, after `Registry.freeze()`, - * `|B| => B -> OtherMixin -> ExtendMixin -> Mixin -> A` - */ -class ClassRegistry { - constructor() { - // base name map - this.baseNameMap = {}; - // Object that maps `baseClass` to the class implementation extended in-place. - this.includedMap = new Map(); - // Object that maps `baseClassCB` to the array of callbacks to generate the extended class. - this.extendedCBMap = new Map(); - // Object that maps `baseClassCB` extended class to the `baseClass` of its super in the includedMap. - this.extendedSuperMap = new Map(); - // For faster access, we can `freeze` the registry so that instead of dynamically generating - // the extended classes, it is taken from the cache instead. - this.cache = new Map(); - } - /** - * Add a new class in the Registry. - * @param {Function} baseClass `class` - */ - add(baseClass) { - this.includedMap.set(baseClass, []); - this.baseNameMap[baseClass.name] = baseClass; - } - /** - * Add a new class in the Registry based on other class - * in the registry. - * @param {Function} baseClassCB `class -> class` - * @param {Function} base `class | class -> class` - */ - addByExtending(baseClassCB, base) { - this.extendedCBMap.set(baseClassCB, [baseClassCB]); - this.extendedSuperMap.set(baseClassCB, base); - this.baseNameMap[baseClassCB.name] = baseClassCB; - } - /** - * Extend in-place a class in the registry. E.g. - * ``` - * // Using the following notation: - * // * |A| - compiled class in the registry - * // * A - class or an extension callback - * // * |A| => A2 -> A1 -> A - * // - the above means, compiled class A - * // points to the class inheritance derived from - * // A2(A1(A)) - * - * class A {}; - * Registry.add(A); - * // |A| => A - * - * let A1 = x => class extends x {}; - * Registry.extend(A, A1); - * // |A| => A1 -> A - * - * let B = x => class extends x {}; - * Registry.addByExtending(B, A); - * // |B| => B -> |A| - * // |B| => B -> A1 -> A - * - * let B1 = x => class extends x {}; - * Registry.extend(B, B1); - * // |B| => B1 -> B -> |A| - * - * let C = x => class extends x {}; - * Registry.addByExtending(C, B); - * // |C| => C -> |B| - * - * let B2 = x => class extends x {}; - * Registry.extend(B, B2); - * // |B| => B2 -> B1 -> B -> |A| - * - * // Overall: - * // |A| => A1 -> A - * // |B| => B2 -> B1 -> B -> A1 -> A - * // |C| => C -> B2 -> B1 -> B -> A1 -> A - * ``` - * @param {Function} base `class | class -> class` - * @param {Function} extensionCB `class -> class` - */ - extend(base, extensionCB) { - if (typeof base === "string") { - base = this.baseNameMap[base]; - } - let extensionArray; - if (this.includedMap.get(base)) { - extensionArray = this.includedMap.get(base); - } else if (this.extendedCBMap.get(base)) { - extensionArray = this.extendedCBMap.get(base); - } else { - throw new Error( - `'${base.name}' is not in the Registry. Add it to Registry before extending.` - ); - } - extensionArray.push(extensionCB); - const locOfNewExtension = extensionArray.length - 1; - const self = this; - const oldCompiled = this.isFrozen ? this.cache.get(base) : null; - return { - remove() { - extensionArray.splice(locOfNewExtension, 1); - self._recompute(base, oldCompiled); - }, - compile() { - self._recompute(base); - }, - }; - } - _compile(base) { - let res; - if (this.includedMap.has(base)) { - res = this.includedMap.get(base).reduce((acc, ext) => ext(acc), base); - } else { - const superClass = this.extendedSuperMap.get(base); - const extensionCBs = this.extendedCBMap.get(base); - res = extensionCBs.reduce((acc, ext) => ext(acc), this._compile(superClass)); - } - Object.defineProperty(res, "name", { value: base.name }); - return res; - } - /** - * Return the compiled class (containing all the extensions) of the base class. - * @param {Function} base `class | class -> class` function used in adding the class - */ - get(base) { - if (typeof base === "string") { - base = this.baseNameMap[base]; - } - if (this.isFrozen) { - return this.cache.get(base); - } - return this._compile(base); - } - /** - * Uses the callbacks registered in the registry to compile the classes. - */ - freeze() { - // Step: Compile the `included classes`. - for (const [baseClass, extensionCBs] of this.includedMap.entries()) { - const compiled = extensionCBs.reduce((acc, ext) => ext(acc), baseClass); - this.cache.set(baseClass, compiled); - } - - // Step: Compile the `extended classes` based on `included classes`. - // Also gather those the are based on `extended classes`. - const remaining = []; - for (const [baseClassCB, extensionCBArray] of this.extendedCBMap.entries()) { - const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB)); - if (!compiled) { - remaining.push([baseClassCB, extensionCBArray]); - continue; - } - const extendedClass = extensionCBArray.reduce( - (acc, extensionCB) => extensionCB(acc), - compiled - ); - this.cache.set(baseClassCB, extendedClass); - } - - // Step: Compile the `extended classes` based on `extended classes`. - for (const [baseClassCB, extensionCBArray] of remaining) { - const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB)); - const extendedClass = extensionCBArray.reduce( - (acc, extensionCB) => extensionCB(acc), - compiled - ); - this.cache.set(baseClassCB, extendedClass); - } - - // Step: Set the name of the compiled classess - for (const [base, compiledClass] of this.cache.entries()) { - Object.defineProperty(compiledClass, "name", { value: base.name }); - } - - // Step: Set the flag to true; - this.isFrozen = true; - } - _recompute(base, old) { - if (typeof base === "string") { - base = this.baseNameMap[base]; - } - return old ? old : this._compile(base); - } -} - -export default ClassRegistry; diff --git a/addons/point_of_sale/static/src/js/ComponentRegistry.js b/addons/point_of_sale/static/src/js/ComponentRegistry.js deleted file mode 100644 index 1547aa930e0..00000000000 --- a/addons/point_of_sale/static/src/js/ComponentRegistry.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @odoo-module */ - -import PosComponent from "@point_of_sale/js/PosComponent"; -import ClassRegistry from "@point_of_sale/js/ClassRegistry"; - -class ComponentRegistry extends ClassRegistry { - freeze() { - super.freeze(); - // Make sure PosComponent has the compiled classes. - // This way, we don't need to explicitly declare that - // a set of components is children of another. - PosComponent.components = {}; - for (const [base, compiledClass] of this.cache.entries()) { - PosComponent.components[base.name] = compiledClass; - } - } - _recompute(base, old) { - const res = super._recompute(base, old); - if (typeof base === "string") { - base = this.baseNameMap[base]; - } - PosComponent.components[base.name] = res; - return res; - } -} - -export default ComponentRegistry; diff --git a/addons/point_of_sale/static/src/js/ControlButtonsMixin.js b/addons/point_of_sale/static/src/js/ControlButtonsMixin.js index 1ed763ce552..8d76cac4c01 100644 --- a/addons/point_of_sale/static/src/js/ControlButtonsMixin.js +++ b/addons/point_of_sale/static/src/js/ControlButtonsMixin.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import Registries from "@point_of_sale/js/Registries"; +/** @typedef {import("@odoo/owl").Component} Component */ /** * Component that has this mixin allows the use of `addControlButton`. @@ -9,7 +9,7 @@ import Registries from "@point_of_sale/js/Registries"; * control buttons can then be rendered in the Component. * @param {Function} x superclass */ -const ControlButtonsMixin = (x) => { +export const ControlButtonsMixin = (x) => { const controlButtonsToPosition = []; const sortedControlButtons = []; @@ -19,15 +19,13 @@ const ControlButtonsMixin = (x) => { .filter((cb) => { return cb.condition ? cb.condition.bind(this)() : true; }) - .map((cb) => - Object.assign({}, cb, { component: Registries.Component.get(cb.component) }) - ); + .map((cb) => Object.assign({}, cb, { component: cb.component })); } } /** * @param {Object} controlButton - * @param {Function} controlButton.component - * Base class that is added in the Registries.Component. + * @param {new (...args: any[]) => Component} controlButton.component the + * component class * @param {Function} controlButton.condition zero argument function that is bound * to the instance of ProductScreen, such that `this.env.pos` can be used * inside the function. @@ -103,5 +101,3 @@ const ControlButtonsMixin = (x) => { }; return Extended; }; - -export default ControlButtonsMixin; diff --git a/addons/point_of_sale/static/src/js/Gui.js b/addons/point_of_sale/static/src/js/Gui.js index f6d3c6806a4..ad7040df0e2 100644 --- a/addons/point_of_sale/static/src/js/Gui.js +++ b/addons/point_of_sale/static/src/js/Gui.js @@ -43,6 +43,7 @@ export const configureGui = ({ component }) => { ]); }; +export class GuiNotReadyError extends Error {} /** * Import this and consume like so: `Gui.showPopup(, )`. * Like you would call `showPopup` in a component. @@ -57,5 +58,6 @@ export const Gui = new Proxy(config, { if (availableMethods.has(key) && isMounted) { return component[key].bind(component); } + throw new GuiNotReadyError(`Attempted get ${key} on Gui when Chrome is not yet mounted.`); }, }); diff --git a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js index 003310210b1..3e6cf95e2e5 100644 --- a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js @@ -1,7 +1,8 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ConfirmPopup } from "../Popups/ConfirmPopup"; +import { ErrorPopup } from "../Popups/ErrorPopup"; const { useRef } = owl; @@ -11,7 +12,7 @@ const { useRef } = owl; * `order-receipt` in the template of the Component that extends * this abstract component. */ -class AbstractReceiptScreen extends PosComponent { +export class AbstractReceiptScreen extends PosComponent { setup() { super.setup(); this.orderReceipt = useRef("order-receipt"); @@ -24,7 +25,7 @@ class AbstractReceiptScreen extends PosComponent { if (printResult.successful) { return true; } else { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: printResult.message.title, body: "Do you want to print using the web printer?", }); @@ -45,7 +46,7 @@ class AbstractReceiptScreen extends PosComponent { window.print(); return true; } catch { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Printing is not supported on some browsers"), body: this.env._t( "Printing is not supported on some browsers due to no default printing protocol " + @@ -56,7 +57,3 @@ class AbstractReceiptScreen extends PosComponent { } } } - -Registries.Component.add(AbstractReceiptScreen); - -export default AbstractReceiptScreen; diff --git a/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js b/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js index 82481f61240..0ce8915526d 100644 --- a/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js +++ b/addons/point_of_sale/static/src/js/Misc/CurrencyAmount.js @@ -1,11 +1,7 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; -class CurrencyAmount extends PosComponent {} -CurrencyAmount.template = "CurrencyAmount"; - -Registries.Component.add(CurrencyAmount); - -export default CurrencyAmount; +export class CurrencyAmount extends PosComponent { + static template = "CurrencyAmount"; +} diff --git a/addons/point_of_sale/static/src/js/Misc/Draggable.js b/addons/point_of_sale/static/src/js/Misc/Draggable.js index 4739df8e668..5fd5e916659 100644 --- a/addons/point_of_sale/static/src/js/Misc/Draggable.js +++ b/addons/point_of_sale/static/src/js/Misc/Draggable.js @@ -1,8 +1,7 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { onMounted, useExternalListener } = owl; @@ -28,7 +27,9 @@ const { onMounted, useExternalListener } = owl; * * @trigger 'drag-end' when dragging ended with payload `{ loc: { top, left } }` */ -class Draggable extends PosComponent { +export class Draggable extends PosComponent { + static template = "Draggable"; + setup() { super.setup(); this.isDragging = false; @@ -137,8 +138,3 @@ class Draggable extends PosComponent { return posTop; } } -Draggable.template = "Draggable"; - -Registries.Component.add(Draggable); - -export default Draggable; diff --git a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js index c46d93f7294..c579be99371 100644 --- a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js +++ b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js @@ -1,13 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; -class IndependentToOrderScreen extends PosComponent { +export class IndependentToOrderScreen extends PosComponent { close() { const order = this.env.pos.get_order(); const { name: screenName } = order.get_screen_data(); this.showScreen(screenName); } } - -export default IndependentToOrderScreen; diff --git a/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js b/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js index ddbb2817fb0..813aff7e6b5 100644 --- a/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js +++ b/addons/point_of_sale/static/src/js/Misc/MobileOrderWidget.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class MobileOrderWidget extends PosComponent { + static template = "MobileOrderWidget"; -class MobileOrderWidget extends PosComponent { get order() { return this.env.pos.get_order(); } @@ -17,9 +18,3 @@ class MobileOrderWidget extends PosComponent { : 0; } } - -MobileOrderWidget.template = "MobileOrderWidget"; - -Registries.Component.add(MobileOrderWidget); - -export default MobileOrderWidget; diff --git a/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js b/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js index ff3e5736ae6..f91eb851e5d 100644 --- a/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js +++ b/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js @@ -317,4 +317,4 @@ class NumberBuffer extends EventBus { } } -export default new NumberBuffer(); +export const numberBuffer = new NumberBuffer(); diff --git a/addons/point_of_sale/static/src/js/Misc/SearchBar.js b/addons/point_of_sale/static/src/js/Misc/SearchBar.js index 4f74637dcd0..94db0f7bea3 100644 --- a/addons/point_of_sale/static/src/js/Misc/SearchBar.js +++ b/addons/point_of_sale/static/src/js/Misc/SearchBar.js @@ -1,8 +1,7 @@ /** @odoo-module */ import { useAutofocus, useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { useExternalListener, useState } = owl; @@ -29,7 +28,9 @@ const { useExternalListener, useState } = owl; * NOTE: The payload of the emitted event is accessible via the `detail` * field of the event. */ -class SearchBar extends PosComponent { +export class SearchBar extends PosComponent { + static template = "point_of_sale.SearchBar"; + setup() { super.setup(); useAutofocus(); @@ -108,7 +109,3 @@ class SearchBar extends PosComponent { this.state.showSearchFields = false; } } -SearchBar.template = "point_of_sale.SearchBar"; -Registries.Component.add(SearchBar); - -export default SearchBar; diff --git a/addons/point_of_sale/static/src/js/Notification.js b/addons/point_of_sale/static/src/js/Notification.js index f0d92fca7ee..9f029b2f40c 100644 --- a/addons/point_of_sale/static/src/js/Notification.js +++ b/addons/point_of_sale/static/src/js/Notification.js @@ -1,12 +1,13 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { onMounted } = owl; -class Notification extends PosComponent { +export class Notification extends PosComponent { + static template = "Notification"; + setup() { super.setup(); useListener("click", this.closeNotification); @@ -18,8 +19,3 @@ class Notification extends PosComponent { }); } } -Notification.template = "Notification"; - -Registries.Component.add(Notification); - -export default Notification; diff --git a/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js b/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js index 3041691a7d8..ed252d67a79 100644 --- a/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { useBus } from "@web/core/utils/hooks"; /** @@ -22,13 +22,13 @@ import { useBus } from "@web/core/utils/hooks"; * The concrete popup can now be instantiated and be awaited for * the user's response like so: * ```js - * const { confirmed, payload } = await this.showPopup('ConcretePopup'); + * const { confirmed, payload } = await this.showPopup(ConcretePopup); * // based on the implementation above, * // if confirmed, payload = 'result' * // otherwise, payload = null * ``` */ -class AbstractAwaitablePopup extends PosComponent { +export class AbstractAwaitablePopup extends PosComponent { setup() { super.setup(); if (this.props.confirmKey) { @@ -58,5 +58,3 @@ class AbstractAwaitablePopup extends PosComponent { return null; } } - -export default AbstractAwaitablePopup; diff --git a/addons/point_of_sale/static/src/js/Popups/AlertPopup.js b/addons/point_of_sale/static/src/js/Popups/AlertPopup.js index a6acd64d4f7..df5ac2126e7 100644 --- a/addons/point_of_sale/static/src/js/Popups/AlertPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/AlertPopup.js @@ -1,17 +1,14 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; -export class AlertPopup extends AbstractAwaitablePopup {} - -AlertPopup.template = "AlertPopup"; -AlertPopup.defaultProps = { - confirmText: _lt("Ok"), - title: "", - body: "", - cancelKey: false, -}; - -Registries.Component.add(AlertPopup); +export class AlertPopup extends AbstractAwaitablePopup { + static template = "AlertPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + title: "", + body: "", + cancelKey: false, + }; +} diff --git a/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js b/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js index a1cafb05b6a..91b4e0e6afa 100644 --- a/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js @@ -1,12 +1,13 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { parse } from "web.field_utils"; const { useRef, useState, onMounted } = owl; -class CashMovePopup extends AbstractAwaitablePopup { +export class CashMovePopup extends AbstractAwaitablePopup { + static template = "point_of_sale.CashMovePopup"; + setup() { super.setup(); this.state = useState({ @@ -49,7 +50,3 @@ class CashMovePopup extends AbstractAwaitablePopup { }; } } -CashMovePopup.template = "point_of_sale.CashMovePopup"; -Registries.Component.add(CashMovePopup); - -export default CashMovePopup; diff --git a/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js b/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js index 59c77266ba9..cfdf767b798 100644 --- a/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/CashOpeningPopup.js @@ -1,11 +1,14 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import { MoneyDetailsPopup } from "./MoneyDetailsPopup"; const { useState } = owl; -class CashOpeningPopup extends AbstractAwaitablePopup { +export class CashOpeningPopup extends AbstractAwaitablePopup { + static template = "CashOpeningPopup"; + static defaultProps = { cancelKey: false }; + setup() { super.setup(); this.manualInputCashCount = null; @@ -27,7 +30,7 @@ class CashOpeningPopup extends AbstractAwaitablePopup { super.confirm(); } async openDetailsPopup() { - const { confirmed, payload } = await this.showPopup("MoneyDetailsPopup", { + const { confirmed, payload } = await this.showPopup(MoneyDetailsPopup, { moneyDetails: this.moneyDetails, total: this.manualInputCashCount ? 0 : this.state.openingCash, }); @@ -50,9 +53,3 @@ class CashOpeningPopup extends AbstractAwaitablePopup { } } } - -CashOpeningPopup.template = "CashOpeningPopup"; -CashOpeningPopup.defaultProps = { cancelKey: false }; -Registries.Component.add(CashOpeningPopup); - -export default CashOpeningPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js index dc58a0ff984..68fdc6a2988 100644 --- a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js @@ -1,12 +1,19 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { identifyError } from "@point_of_sale/app/error_handlers/error_handlers"; import { ConnectionLostError } from "@web/core/network/rpc_service"; +import { SaleDetailsButton } from "../ChromeWidgets/SaleDetailsButton"; +import { ConfirmPopup } from "./ConfirmPopup"; +import { ErrorPopup } from "./ErrorPopup"; +import { MoneyDetailsPopup } from "./MoneyDetailsPopup"; + const { useState } = owl; -class ClosePosPopup extends AbstractAwaitablePopup { +export class ClosePosPopup extends AbstractAwaitablePopup { + static components = { SaleDetailsButton }; + static template = "ClosePosPopup"; + setup() { super.setup(); this.manualInputCashCount = false; @@ -24,7 +31,7 @@ class ClosePosPopup extends AbstractAwaitablePopup { if (!this.cashControl || !this.hasDifference()) { this.closeSession(); } else if (this.hasUserAuthority()) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Payments Difference"), body: this.env._t( "Do you want to accept payments difference and post a profit/loss journal entry?" @@ -34,7 +41,7 @@ class ClosePosPopup extends AbstractAwaitablePopup { this.closeSession(); } } else { - await this.showPopup("ConfirmPopup", { + await this.showPopup(ConfirmPopup, { title: this.env._t("Payments Difference"), body: _.str.sprintf( this.env._t( @@ -54,7 +61,7 @@ class ClosePosPopup extends AbstractAwaitablePopup { } } async openDetailsPopup() { - const { confirmed, payload } = await this.showPopup("MoneyDetailsPopup", { + const { confirmed, payload } = await this.showPopup(MoneyDetailsPopup, { moneyDetails: this.moneyDetails, total: this.manualInputCashCount ? 0 @@ -175,7 +182,7 @@ class ClosePosPopup extends AbstractAwaitablePopup { throw error; } else { // FIXME POSREF: why are we catching errors here but not anywhere else in this method? - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Closing session error"), body: this.env._t( "An error has occurred when trying to close the session.\n" + @@ -210,8 +217,3 @@ class ClosePosPopup extends AbstractAwaitablePopup { return pm.type == "bank" && pm.number !== 0; } } - -ClosePosPopup.template = "ClosePosPopup"; -Registries.Component.add(ClosePosPopup); - -export default ClosePosPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js b/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js index 106ba159911..6d138708c65 100644 --- a/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js @@ -1,19 +1,18 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + // formerly ConfirmPopupWidget -class ConfirmPopup extends AbstractAwaitablePopup {} -ConfirmPopup.template = "ConfirmPopup"; -ConfirmPopup.defaultProps = { - confirmText: _lt("Ok"), - cancelText: _lt("Cancel"), - title: _lt("Confirm ?"), - body: "", -}; - -Registries.Component.add(ConfirmPopup); - -export default ConfirmPopup; +export class ConfirmPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "ConfirmPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + cancelText: _lt("Cancel"), + title: _lt("Confirm ?"), + body: "", + }; +} diff --git a/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js b/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js index 2d4eb65b541..d47b9f8d6a4 100644 --- a/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ControlButtonPopup.js @@ -1,10 +1,19 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; -class ControlButtonPopup extends AbstractAwaitablePopup { +import { Draggable } from "../Misc/Draggable"; + +export class ControlButtonPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "ControlButtonPopup"; + static defaultProps = { + cancelText: _lt("Back"), + controlButtons: [], + confirmKey: false, + }; + /** * @param {Object} props * @param {string} props.startingValue @@ -14,13 +23,3 @@ class ControlButtonPopup extends AbstractAwaitablePopup { this.controlButtons = this.props.controlButtons; } } -ControlButtonPopup.template = "ControlButtonPopup"; -ControlButtonPopup.defaultProps = { - cancelText: _lt("Back"), - controlButtons: [], - confirmKey: false, -}; - -Registries.Component.add(ControlButtonPopup); - -export default ControlButtonPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/DatePickerPopup.js b/addons/point_of_sale/static/src/js/Popups/DatePickerPopup.js index e7a7864a277..98714b62269 100644 --- a/addons/point_of_sale/static/src/js/Popups/DatePickerPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/DatePickerPopup.js @@ -1,12 +1,18 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; const { onMounted, useRef, useState } = owl; -class DatePickerPopup extends AbstractAwaitablePopup { +export class DatePickerPopup extends AbstractAwaitablePopup { + static template = 'DatePickerPopup'; + static defaultProps = { + confirmText: _lt('Confirm'), + cancelText: _lt('Discard'), + title: _lt('DatePicker'), + }; + setup() { super.setup(); this.state = useState({shippingDate: this._today()}); @@ -21,13 +27,3 @@ class DatePickerPopup extends AbstractAwaitablePopup { } } -DatePickerPopup.template = 'DatePickerPopup'; -DatePickerPopup.defaultProps = { - confirmText: _lt('Confirm'), - cancelText: _lt('Discard'), - title: _lt('DatePicker'), -}; - -Registries.Component.add(DatePickerPopup); - -export default DatePickerPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/EditListInput.js b/addons/point_of_sale/static/src/js/Popups/EditListInput.js index 3b48b008fae..588b2a50f67 100644 --- a/addons/point_of_sale/static/src/js/Popups/EditListInput.js +++ b/addons/point_of_sale/static/src/js/Popups/EditListInput.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; /** * props { @@ -10,7 +9,9 @@ import Registries from "@point_of_sale/js/Registries"; * item: object, * } */ -class EditListInput extends PosComponent { +export class EditListInput extends PosComponent { + static template = "EditListInput"; + onKeyup(event) { if (event.key === "Enter" && event.target.value.trim() !== "") { this.props.createNewItem(); @@ -20,8 +21,3 @@ class EditListInput extends PosComponent { this.props.onInputChange(this.props.item._id, event.target.value); } } -EditListInput.template = "EditListInput"; - -Registries.Component.add(EditListInput); - -export default EditListInput; diff --git a/addons/point_of_sale/static/src/js/Popups/EditListPopup.js b/addons/point_of_sale/static/src/js/Popups/EditListPopup.js index c1aa8220674..7f0f042e70c 100644 --- a/addons/point_of_sale/static/src/js/Popups/EditListPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/EditListPopup.js @@ -1,10 +1,11 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { useAutoFocusToLast } from "@point_of_sale/js/custom_hooks"; import { _lt } from "@web/core/l10n/translation"; +import { EditListInput } from "./EditListInput"; + const { useState } = owl; /** @@ -25,7 +26,7 @@ const { useState } = owl; * * // supply the items to the popup and wait for user's response * // when user pressed `confirm` in the popup, the changes he made will be returned by the showPopup function. - * const { confirmed, payload: newNames } = await this.showPopup('EditListPopup', { + * const { confirmed, payload: newNames } = await this.showPopup(EditListPopup, { * title: "Can you confirm this item?", * array: names }) * @@ -39,7 +40,16 @@ const { useState } = owl; * } * ``` */ -class EditListPopup extends AbstractAwaitablePopup { +export class EditListPopup extends AbstractAwaitablePopup { + static components = { EditListInput }; + static template = "EditListPopup"; + static defaultProps = { + confirmText: _lt("Add"), + cancelText: _lt("Discard"), + array: [], + isSingleItem: false, + }; + /** * @param {String} title required title of popup * @param {Array} [props.array=[]] the array of { id, text } to be edited or an array of strings @@ -104,14 +114,3 @@ class EditListPopup extends AbstractAwaitablePopup { }; } } -EditListPopup.template = "EditListPopup"; -EditListPopup.defaultProps = { - confirmText: _lt("Add"), - cancelText: _lt("Discard"), - array: [], - isSingleItem: false, -}; - -Registries.Component.add(EditListPopup); - -export default EditListPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js b/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js index 9a43be839cc..7d715bbb68e 100644 --- a/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js @@ -1,26 +1,25 @@ /** @odoo-module */ -import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup"; -import Registries from "@point_of_sale/js/Registries"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + // formerly ErrorBarcodePopupWidget -class ErrorBarcodePopup extends ErrorPopup { +export class ErrorBarcodePopup extends ErrorPopup { + static components = { Draggable }; + static template = "ErrorBarcodePopup"; + static defaultProps = { + confirmText: _lt("Ok"), + cancelText: _lt("Cancel"), + title: _lt("Error"), + body: "", + message: _lt( + "The Point of Sale could not find any product, customer, employee or action associated with the scanned barcode." + ), + }; + get translatedMessage() { return this.env._t(this.props.message); } } -ErrorBarcodePopup.template = "ErrorBarcodePopup"; -ErrorBarcodePopup.defaultProps = { - confirmText: _lt("Ok"), - cancelText: _lt("Cancel"), - title: _lt("Error"), - body: "", - message: _lt( - "The Point of Sale could not find any product, customer, employee or action associated with the scanned barcode." - ), -}; - -Registries.Component.add(ErrorBarcodePopup); - -export default ErrorBarcodePopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js b/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js index d685efa91c8..00c3cca7d16 100644 --- a/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js @@ -1,11 +1,18 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; // formerly ErrorPopupWidget -class ErrorPopup extends AbstractAwaitablePopup { +export class ErrorPopup extends AbstractAwaitablePopup { + static template = "ErrorPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + title: _lt("Error"), + body: "", + cancelKey: false, + }; + setup() { super.setup(); owl.onMounted(this.onMounted); @@ -14,14 +21,3 @@ class ErrorPopup extends AbstractAwaitablePopup { this.playSound("error"); } } -ErrorPopup.template = "ErrorPopup"; -ErrorPopup.defaultProps = { - confirmText: _lt("Ok"), - title: _lt("Error"), - body: "", - cancelKey: false, -}; - -Registries.Component.add(ErrorPopup); - -export default ErrorPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js b/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js index 42dbb93a590..61212835e59 100644 --- a/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js @@ -1,11 +1,22 @@ /** @odoo-module */ -import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup"; -import Registries from "@point_of_sale/js/Registries"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { _lt } from "@web/core/l10n/translation"; // formerly ErrorTracebackPopupWidget -class ErrorTracebackPopup extends ErrorPopup { +export class ErrorTracebackPopup extends ErrorPopup { + static template = "ErrorTracebackPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + cancelText: _lt("Cancel"), + confirmKey: false, + title: _lt("Error with Traceback"), + body: "", + exitButtonIsShown: false, + exitButtonText: _lt("Exit Pos"), + exitButtonTrigger: "close-pos", + }; + get tracebackUrl() { const blob = new Blob([this.props.body]); const URL = window.URL || window.webkitURL; @@ -27,18 +38,3 @@ class ErrorTracebackPopup extends ErrorPopup { ); } } -ErrorTracebackPopup.template = "ErrorTracebackPopup"; -ErrorTracebackPopup.defaultProps = { - confirmText: _lt("Ok"), - cancelText: _lt("Cancel"), - confirmKey: false, - title: _lt("Error with Traceback"), - body: "", - exitButtonIsShown: false, - exitButtonText: _lt("Exit Pos"), - exitButtonTrigger: "close-pos", -}; - -Registries.Component.add(ErrorTracebackPopup); - -export default ErrorTracebackPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js b/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js index 5068a23317a..3657d0d5136 100644 --- a/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/MoneyDetailsPopup.js @@ -1,11 +1,15 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; + +import { CurrencyAmount } from "../Misc/CurrencyAmount"; const { useState } = owl; -class MoneyDetailsPopup extends AbstractAwaitablePopup { +export class MoneyDetailsPopup extends AbstractAwaitablePopup { + static components = { CurrencyAmount }; + static template = "MoneyDetailsPopup"; + setup() { super.setup(); this.currency = this.env.pos.currency; @@ -51,8 +55,3 @@ class MoneyDetailsPopup extends AbstractAwaitablePopup { }; } } - -MoneyDetailsPopup.template = "MoneyDetailsPopup"; -Registries.Component.add(MoneyDetailsPopup); - -export default MoneyDetailsPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/NumberPopup.js b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js index c20515d8839..5bd0ee9824c 100644 --- a/addons/point_of_sale/static/src/js/Popups/NumberPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js @@ -2,15 +2,27 @@ import core from "web.core"; var _t = core._t; -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; + +import { Draggable } from "../Misc/Draggable"; const { useState } = owl; -// formerly NumberPopupWidget -class NumberPopup extends AbstractAwaitablePopup { +export class NumberPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "NumberPopup"; + static defaultProps = { + confirmText: _t("Confirm"), + cancelText: _t("Discard"), + title: _t("Confirm ?"), + body: "", + cheap: false, + startingValue: null, + isPassword: false, + }; + /** * @param {Object} props * @param {Boolean} props.isPassword Show password popup. @@ -32,7 +44,7 @@ class NumberPopup extends AbstractAwaitablePopup { .replace(".", this.decimalSeparator); } this.state = useState({ buffer: startingBuffer, toStartOver: this.props.isInputSelected }); - NumberBuffer.use({ + numberBuffer.use({ nonKeyboardInputEvent: "numpad-click-input", triggerAtEnter: "accept-input", triggerAtEscape: "close-this-popup", @@ -53,7 +65,7 @@ class NumberPopup extends AbstractAwaitablePopup { } } confirm(event) { - if (NumberBuffer.get()) { + if (numberBuffer.get()) { super.confirm(); } } @@ -61,20 +73,6 @@ class NumberPopup extends AbstractAwaitablePopup { this.trigger("numpad-click-input", { key }); } getPayload() { - return NumberBuffer.get(); + return numberBuffer.get(); } } -NumberPopup.template = "NumberPopup"; -NumberPopup.defaultProps = { - confirmText: _t("Confirm"), - cancelText: _t("Discard"), - title: _t("Confirm ?"), - body: "", - cheap: false, - startingValue: null, - isPassword: false, -}; - -Registries.Component.add(NumberPopup); - -export default NumberPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js b/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js index e8d878a720e..0f3d9bcb169 100644 --- a/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js @@ -1,28 +1,27 @@ /** @odoo-module */ -import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup"; -import Registries from "@point_of_sale/js/Registries"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + /** * This is a special kind of error popup as it introduces * an option to not show it again. */ -class OfflineErrorPopup extends ErrorPopup { +export class OfflineErrorPopup extends ErrorPopup { + static components = { Draggable }; + static template = "OfflineErrorPopup"; + static dontShow = false; + static defaultProps = { + confirmText: _lt("Ok"), + cancelText: _lt("Cancel"), + title: _lt("Offline Error"), + body: _lt("Either the server is inaccessible or browser is not connected online."), + }; + dontShowAgain() { this.constructor.dontShow = true; this.cancel(); } } -OfflineErrorPopup.template = "OfflineErrorPopup"; -OfflineErrorPopup.dontShow = false; -OfflineErrorPopup.defaultProps = { - confirmText: _lt("Ok"), - cancelText: _lt("Cancel"), - title: _lt("Offline Error"), - body: _lt("Either the server is inaccessible or browser is not connected online."), -}; - -Registries.Component.add(OfflineErrorPopup); - -export default OfflineErrorPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js b/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js index aa16638428b..014671cd0a8 100644 --- a/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js @@ -1,11 +1,20 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + // formerly OrderImportPopupWidget -class OrderImportPopup extends AbstractAwaitablePopup { +export class OrderImportPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "OrderImportPopup"; + static defaultProps = { + confirmText: _lt("Ok"), + cancelKey: false, + body: "", + }; + get unpaidSkipped() { return ( (this.props.report.unpaid_skipped_existing || 0) + @@ -14,13 +23,3 @@ class OrderImportPopup extends AbstractAwaitablePopup { } getPayload() {} } -OrderImportPopup.template = "OrderImportPopup"; -OrderImportPopup.defaultProps = { - confirmText: _lt("Ok"), - cancelKey: false, - body: "", -}; - -Registries.Component.add(OrderImportPopup); - -export default OrderImportPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/PosPopupController.js b/addons/point_of_sale/static/src/js/Popups/PosPopupController.js index 9e01e95f02e..6dc72e4edde 100644 --- a/addons/point_of_sale/static/src/js/Popups/PosPopupController.js +++ b/addons/point_of_sale/static/src/js/Popups/PosPopupController.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import Registries from "@point_of_sale/js/Registries"; -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { useBus } from "@web/core/utils/hooks"; /** @@ -25,7 +24,10 @@ import { useBus } from "@web/core/utils/hooks"; * @typedef {{ id: number, resolve: Function, keepBehind?: boolean, cancelKey?: string, confirmKey?: string }} BasePopupProps * @typedef {{ name: string, component: AbstractAwaitablePopup, props: BasePopupProps, key: string }} Popup */ -class PosPopupController extends PosComponent { +export class PosPopupController extends PosComponent { + static template = "point_of_sale.PosPopupController"; + static components = {}; + setup() { super.setup(); useBus(this.env.posbus, "show-popup", this._showPopup); @@ -33,24 +35,33 @@ class PosPopupController extends PosComponent { owl.useExternalListener(window, "keyup", this._onWindowKeyup); this.popups = owl.useState([]); } + /** + * A popup can be cancelled/confirmed with 'Escape'/'Enter' key by default. + * Also, if it's not the top popup, it is hidden from the view. + * This can be overridden by the default props of the popop component + * and the props used in requesting to show the popup. + * + * @param {AbstractAwaitablePopup} popupComponent + * @param {Object} props + * @returns {BasePopupProps} + */ _showPopup(event) { - let { id, name, props, resolve } = event.detail; + let { id, component, props, resolve } = event.detail; props = Object.assign(props || {}, { id, resolve }); - const component = this.constructor.components[name]; - if (!component) { - throw new Error( - `'${name}' is not found. Make sure the file is loaded and the component is properly registered using 'Registries.Component.add'.` - ); - } if (component.dontShow) { resolve(); return; } this.popups.push({ - name, component, - props: this._constructPopupProps(component, props), - key: `${name}-${id}`, + props: { + keepBehind: false, + cancelKey: "Escape", + confirmKey: "Enter", + // FIXME POSREF assigning the default props by hand defeats the point of default props + ...component.defaultProps, + ...props, + }, }); } _closePopup(event) { @@ -76,28 +87,6 @@ class PosPopupController extends PosComponent { this.env.posbus.trigger(`confirm-popup-${this.topPopup.props.id}`); } } - /** - * A popup can be cancelled/confirmed with 'Escape'/'Enter' key by default. - * Also, if it's not the top popup, it is hidden from the view. - * This can be overridden by the default props of the popop component - * and the props used in requesting to show the popup. - * - * @param {AbstractAwaitablePopup} popupComponent - * @param {Object} props - * @returns {BasePopupProps} - */ - _constructPopupProps(popupComponent, props) { - const defaultProps = popupComponent.defaultProps || {}; - return Object.assign( - { - keepBehind: false, - cancelKey: "Escape", - confirmKey: "Enter", - }, - defaultProps, - props - ); - } /** * @returns {boolean} Hide the element of this component when this returns false. */ @@ -120,7 +109,3 @@ class PosPopupController extends PosComponent { return this.topPopup === popup || popup.props.keepBehind; } } -PosPopupController.template = "point_of_sale.PosPopupController"; -Registries.Component.add(PosPopupController); - -export default PosPopupController; diff --git a/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js b/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js index cb00c72f03f..ea213adf559 100644 --- a/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js @@ -1,35 +1,9 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; const { useState, useSubEnv } = owl; -export class ProductConfiguratorPopup extends AbstractAwaitablePopup { - setup() { - super.setup(); - useSubEnv({ attribute_components: [] }); - } - - getPayload() { - var selected_attributes = []; - var price_extra = 0.0; - - this.env.attribute_components.forEach((attribute_component) => { - const { value, extra } = attribute_component.getValue(); - selected_attributes.push(value); - price_extra += extra; - }); - - return { - selected_attributes, - price_extra, - }; - } -} -ProductConfiguratorPopup.template = "ProductConfiguratorPopup"; -Registries.Component.add(ProductConfiguratorPopup); - export class BaseProductAttribute extends PosComponent { setup() { super.setup(); @@ -60,6 +34,8 @@ export class BaseProductAttribute extends PosComponent { } export class RadioProductAttribute extends BaseProductAttribute { + static template = "RadioProductAttribute"; + setup() { super.setup(); owl.onMounted(this.onMounted); @@ -72,13 +48,41 @@ export class RadioProductAttribute extends BaseProductAttribute { $(this.el).find('input[type="radio"]:first').prop("checked", true); } } -RadioProductAttribute.template = "RadioProductAttribute"; -Registries.Component.add(RadioProductAttribute); -export class SelectProductAttribute extends BaseProductAttribute {} -SelectProductAttribute.template = "SelectProductAttribute"; -Registries.Component.add(SelectProductAttribute); +export class SelectProductAttribute extends BaseProductAttribute { + static template = "SelectProductAttribute"; +} -export class ColorProductAttribute extends BaseProductAttribute {} -ColorProductAttribute.template = "ColorProductAttribute"; -Registries.Component.add(ColorProductAttribute); +export class ColorProductAttribute extends BaseProductAttribute { + static template = "ColorProductAttribute"; +} + +export class ProductConfiguratorPopup extends AbstractAwaitablePopup { + static template = "ProductConfiguratorPopup"; + static components = { + RadioProductAttribute, + SelectProductAttribute, + ColorProductAttribute + }; + + setup() { + super.setup(); + useSubEnv({ attribute_components: [] }); + } + + getPayload() { + var selected_attributes = []; + var price_extra = 0.0; + + this.env.attribute_components.forEach((attribute_component) => { + const { value, extra } = attribute_component.getValue(); + selected_attributes.push(value); + price_extra += extra; + }); + + return { + selected_attributes, + price_extra, + }; + } +} diff --git a/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js b/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js index 9e7b9ce05c3..2216a680ffe 100644 --- a/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ProductInfoPopup.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; /** * Props: @@ -9,7 +8,10 @@ import Registries from "@point_of_sale/js/Registries"; * info: {object of data} * } */ -class ProductInfoPopup extends AbstractAwaitablePopup { +export class ProductInfoPopup extends AbstractAwaitablePopup { + static template = "ProductInfoPopup"; + static defaultProps = { confirmKey: false }; + setup() { super.setup(); Object.assign(this, this.props.info); @@ -25,7 +27,3 @@ class ProductInfoPopup extends AbstractAwaitablePopup { return isAccessibleToEveryUser || isCashierManager; } } - -ProductInfoPopup.template = "ProductInfoPopup"; -ProductInfoPopup.defaultProps = { confirmKey: false }; -Registries.Component.add(ProductInfoPopup); diff --git a/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js b/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js index 124b53ec395..2877561953b 100644 --- a/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js @@ -1,13 +1,24 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + const { useState } = owl; // formerly SelectionPopupWidget -class SelectionPopup extends AbstractAwaitablePopup { +export class SelectionPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "SelectionPopup"; + static defaultProps = { + cancelText: _lt("Cancel"), + title: _lt("Select"), + body: "", + list: [], + confirmKey: false, + }; + /** * Value of the `item` key of the selected element in the Selection * Array is the payload of this popup. @@ -43,15 +54,3 @@ class SelectionPopup extends AbstractAwaitablePopup { return selected && selected.item; } } -SelectionPopup.template = "SelectionPopup"; -SelectionPopup.defaultProps = { - cancelText: _lt("Cancel"), - title: _lt("Select"), - body: "", - list: [], - confirmKey: false, -}; - -Registries.Component.add(SelectionPopup); - -export default SelectionPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js b/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js index 42123d93933..b331c81af61 100644 --- a/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js @@ -1,15 +1,25 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; +import { Draggable } from "../Misc/Draggable"; + const { onMounted, useRef, useState } = owl; // formerly TextAreaPopupWidget // IMPROVEMENT: This code is very similar to TextInputPopup. // Combining them would reduce the code. -class TextAreaPopup extends AbstractAwaitablePopup { +export class TextAreaPopup extends AbstractAwaitablePopup { + static components = { Draggable }; + static template = "TextAreaPopup"; + static defaultProps = { + confirmText: _lt("Add"), + cancelText: _lt("Discard"), + title: "", + body: "", + }; + /** * @param {Object} props * @param {string} props.startingValue @@ -27,14 +37,3 @@ class TextAreaPopup extends AbstractAwaitablePopup { return this.state.inputValue; } } -TextAreaPopup.template = "TextAreaPopup"; -TextAreaPopup.defaultProps = { - confirmText: _lt("Add"), - cancelText: _lt("Discard"), - title: "", - body: "", -}; - -Registries.Component.add(TextAreaPopup); - -export default TextAreaPopup; diff --git a/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js b/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js index 26aa028f40d..2ab6a397b05 100644 --- a/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js @@ -1,13 +1,22 @@ /** @odoo-module */ -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { _lt } from "@web/core/l10n/translation"; const { onMounted, useRef, useState } = owl; // formerly TextInputPopupWidget -class TextInputPopup extends AbstractAwaitablePopup { +export class TextInputPopup extends AbstractAwaitablePopup { + static template = "TextInputPopup"; + static defaultProps = { + confirmText: _lt("Confirm"), + cancelText: _lt("Discard"), + title: "", + body: "", + startingValue: "", + placeholder: "", + }; + setup() { super.setup(); this.state = useState({ inputValue: this.props.startingValue }); @@ -21,16 +30,3 @@ class TextInputPopup extends AbstractAwaitablePopup { return this.state.inputValue; } } -TextInputPopup.template = "TextInputPopup"; -TextInputPopup.defaultProps = { - confirmText: _lt("Confirm"), - cancelText: _lt("Discard"), - title: "", - body: "", - startingValue: "", - placeholder: "", -}; - -Registries.Component.add(TextInputPopup); - -export default TextInputPopup; diff --git a/addons/point_of_sale/static/src/js/PosComponent.js b/addons/point_of_sale/static/src/js/PosComponent.js index 1e5d9a425ba..1eba2538564 100644 --- a/addons/point_of_sale/static/src/js/PosComponent.js +++ b/addons/point_of_sale/static/src/js/PosComponent.js @@ -4,7 +4,8 @@ import { LegacyComponent } from "@web/legacy/legacy_component"; let nextId = 0; -class PosComponent extends LegacyComponent { +export class PosComponent extends LegacyComponent { + static components = {}; /** * This function is available to all Components that inherit this class. * The goal of this function is to show an awaitable dialog (popup) that @@ -13,10 +14,7 @@ class PosComponent extends LegacyComponent { * * ``` * async getUserName() { - * const userResponse = await this.showPopup( - * 'TextInputPopup', - * { title: 'What is your name?' } - * ); + * const userResponse = await this.showPopup(TextInputPopup, { title: 'What is your name?' }); * // at this point, the TextInputPopup is displayed. Depending on how the popup is defined, * // say the input contains the name, the result of the interaction with the user is * // saved in `userResponse`. @@ -27,9 +25,9 @@ class PosComponent extends LegacyComponent { * @param {String} name Name of the popup component * @param {Object} props Object that will be used to render to popup */ - showPopup(name, props) { + showPopup(component, props) { return new Promise((resolve) => { - this.env.posbus.trigger("show-popup", { name, props, resolve, id: nextId++ }); + this.env.posbus.trigger("show-popup", { component, props, resolve, id: nextId++ }); }); } showTempScreen(name, props) { @@ -61,5 +59,3 @@ class PosComponent extends LegacyComponent { this.trigger("close-notification"); } } - -export default PosComponent; diff --git a/addons/point_of_sale/static/src/js/Registries.js b/addons/point_of_sale/static/src/js/Registries.js deleted file mode 100644 index b55a3424f9a..00000000000 --- a/addons/point_of_sale/static/src/js/Registries.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @odoo-module */ -/** - * This definition contains all the instances of ClassRegistry. - */ - -import ComponentRegistry from "@point_of_sale/js/ComponentRegistry"; -import ClassRegistry from "@point_of_sale/js/ClassRegistry"; - -class ModelRegistry extends ClassRegistry { - add(baseClass) { - super.add(baseClass); - /** - * Introduce a static method (`create`) to each base class that can be - * conveniently use to create an instance of the extended version - * of the class. - */ - baseClass.create = (...args) => { - const ExtendedClass = this.get(baseClass); - return new ExtendedClass(...args); - }; - } -} - -export const Component = new ComponentRegistry(); -export const Model = new ModelRegistry(); - -export default { Component, Model }; diff --git a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js index 24e969f0604..8b1bfcc0df7 100644 --- a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js +++ b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerDetailsEdit.js @@ -2,12 +2,14 @@ import { _t } from "web.core"; import { getDataURLFromFile } from "web.utils"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; const { onMounted, onWillUnmount } = owl; -class PartnerDetailsEdit extends PosComponent { +export class PartnerDetailsEdit extends PosComponent { + static template = "PartnerDetailsEdit"; + setup() { super.setup(); this.intFields = ["country_id", "state_id", "property_product_pricelist"]; @@ -56,7 +58,7 @@ class PartnerDetailsEdit extends PosComponent { } } if ((!this.props.partner.name && !processedChanges.name) || processedChanges.name === "") { - return this.showPopup("ErrorPopup", { + return this.showPopup(ErrorPopup, { title: _t("A Customer Name Is Required"), }); } @@ -66,7 +68,7 @@ class PartnerDetailsEdit extends PosComponent { async uploadImage(event) { const file = event.target.files[0]; if (!file.type.match(/image.*/)) { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Unsupported File Format"), body: this.env._t( "Only web-compatible Image formats such as .png or .jpeg are supported." @@ -114,7 +116,7 @@ class PartnerDetailsEdit extends PosComponent { const img = new Image(); img.addEventListener("load", () => resolve(img)); img.addEventListener("error", () => { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Loading Image Error"), body: this.env._t("Encountered error when loading image. Please try again."), }); @@ -124,8 +126,3 @@ class PartnerDetailsEdit extends PosComponent { }); } } -PartnerDetailsEdit.template = "PartnerDetailsEdit"; - -Registries.Component.add(PartnerDetailsEdit); - -export default PartnerDetailsEdit; diff --git a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerLine.js b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerLine.js index b3f6d75ac82..99e5733c289 100644 --- a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerLine.js +++ b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerLine.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class PartnerLine extends PosComponent { + static template = "PartnerLine"; -class PartnerLine extends PosComponent { get highlight() { return this._isPartnerSelected ? "highlight" : ""; } @@ -11,8 +12,3 @@ class PartnerLine extends PosComponent { return this.props.partner === this.props.selectedPartner; } } -PartnerLine.template = "PartnerLine"; - -Registries.Component.add(PartnerLine); - -export default PartnerLine; diff --git a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerListScreen.js b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerListScreen.js index 447e6699ece..9e282d64504 100644 --- a/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerListScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PartnerListScreen/PartnerListScreen.js @@ -1,11 +1,14 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { registry } from "@web/core/registry"; import { debounce } from "@web/core/utils/timing"; import { useListener } from "@web/core/utils/hooks"; +import { PartnerLine } from "./PartnerLine"; +import { PartnerDetailsEdit } from "./PartnerDetailsEdit"; + const { onWillUnmount, useRef } = owl; /** @@ -23,7 +26,10 @@ const { onWillUnmount, useRef } = owl; * * @props partner - originally selected partner */ -class PartnerListScreen extends PosComponent { +export class PartnerListScreen extends PosComponent { + static components = { PartnerDetailsEdit, PartnerLine }; + static template = "PartnerListScreen"; + setup() { super.setup(); useListener("click-save", () => this.env.bus.trigger("save-partner")); @@ -196,8 +202,5 @@ class PartnerListScreen extends PosComponent { return result; } } -PartnerListScreen.template = "PartnerListScreen"; -Registries.Component.add(PartnerListScreen); - -export default PartnerListScreen; +registry.category("pos_screens").add("PartnerListScreen", PartnerListScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js index c5146c7146e..6228ce74d1a 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js @@ -1,15 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class PSNumpadInputButton extends PosComponent { + static template = "PSNumpadInputButton"; -class PSNumpadInputButton extends PosComponent { get _class() { return this.props.changeClassTo || "input-button number-char"; } } -PSNumpadInputButton.template = "PSNumpadInputButton"; - -Registries.Component.add(PSNumpadInputButton); - -export default PSNumpadInputButton; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index 9eac2d4006c..0632c00c0c9 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -1,14 +1,30 @@ /** @odoo-module */ import { parse } from "web.field_utils"; -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { useErrorHandlers } from "@point_of_sale/js/custom_hooks"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { registry } from "@web/core/registry"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; import utils from "web.utils"; -class PaymentScreen extends PosComponent { +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { DatePickerPopup } from "@point_of_sale/js/Popups/DatePickerPopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; + +import { PaymentScreenNumpad } from "./PaymentScreenNumpad"; +import { PaymentScreenPaymentLines } from "./PaymentScreenPaymentLines"; +import { PaymentScreenStatus } from "./PaymentScreenStatus"; + +export class PaymentScreen extends PosComponent { + static template = "PaymentScreen"; + static components = { + PaymentScreenNumpad, + PaymentScreenPaymentLines, + PaymentScreenStatus, + }; + setup() { super.setup(); useListener("delete-payment-line", this.deletePaymentLine); @@ -23,14 +39,14 @@ class PaymentScreen extends PosComponent { this.payment_methods_from_config = this.env.pos.payment_methods.filter((method) => this.env.pos.config.payment_method_ids.includes(method.id) ); - NumberBuffer.use(this._getNumberBufferConfig); + numberBuffer.use(this._getNumberBufferConfig); useErrorHandlers(); this.payment_interface = null; this.error = false; } showMaxValueError() { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Maximum value reached"), body: this.env._t( "The amount cannot be higher than the due amount if you don't have a cash payment method configured." @@ -83,10 +99,10 @@ class PaymentScreen extends PosComponent { // original function: click_paymentmethods const result = this.currentOrder.add_paymentline(paymentMethod); if (result) { - NumberBuffer.reset(); + numberBuffer.reset(); return true; } else { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Error"), body: this.env._t("There is already an electronic payment in progress."), }); @@ -108,10 +124,10 @@ class PaymentScreen extends PosComponent { ) { return; } - if (NumberBuffer.get() === null) { + if (numberBuffer.get() === null) { this.deletePaymentLine({ detail: { cid: this.selectedPaymentLine.cid } }); } else { - this.selectedPaymentLine.set_amount(NumberBuffer.getFloat()); + this.selectedPaymentLine.set_amount(numberBuffer.getFloat()); } } toggleIsToInvoice() { @@ -128,7 +144,7 @@ class PaymentScreen extends PosComponent { const change = this.currentOrder.get_change(); const value = tip === 0 && change > 0 ? change : tip; - const { confirmed, payload } = await this.showPopup("NumberPopup", { + const { confirmed, payload } = await this.showPopup(NumberPopup, { title: tip ? this.env._t("Change Tip") : this.env._t("Add Tip"), startingValue: value, isInputSelected: true, @@ -140,7 +156,7 @@ class PaymentScreen extends PosComponent { } async toggleShippingDatePicker() { if (!this.currentOrder.getShippingDate()) { - const { confirmed, payload: shippingDate } = await this.showPopup("DatePickerPopup", { + const { confirmed, payload: shippingDate } = await this.showPopup(DatePickerPopup, { title: this.env._t("Select the shipping date"), }); if (confirmed) { @@ -164,12 +180,12 @@ class PaymentScreen extends PosComponent { .send_payment_cancel(this.currentOrder, cid) .then(function () { self.currentOrder.remove_paymentline(line); - NumberBuffer.reset(); + numberBuffer.reset(); self.render(true); }); } else if (line.get_payment_status() !== "waitingCancel") { this.currentOrder.remove_paymentline(line); - NumberBuffer.reset(); + numberBuffer.reset(); this.render(true); } } @@ -177,13 +193,13 @@ class PaymentScreen extends PosComponent { const { cid } = event.detail; const line = this.paymentLines.find((line) => line.cid === cid); this.currentOrder.select_paymentline(line); - NumberBuffer.reset(); + numberBuffer.reset(); this.render(true); } async validateOrder(isForceValidate) { if (this.env.pos.config.cash_rounding) { if (!this.env.pos.get_order().check_paymentlines_rounding()) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Rounding error in payment lines"), body: this.env._t( "The amount of your payment lines must be rounded to validate the transaction." @@ -243,7 +259,7 @@ class PaymentScreen extends PosComponent { syncOrderResult.map((res) => res.id) ); if (!postPushResult) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Error: no internet connection."), body: this.env._t( "Some, if not all, post-processing after syncing order failed." @@ -275,7 +291,7 @@ class PaymentScreen extends PosComponent { // Ask the user to sync the remaining unsynced orders. if (!hasError && syncOrderResult && this.env.pos.db.get_orders().length) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Remaining unsynced orders"), body: this.env._t( "There are unsynced orders. Do you want to sync these orders?" @@ -294,18 +310,19 @@ class PaymentScreen extends PosComponent { return !this.error ? "ReceiptScreen" : "ProductScreen"; } paymentMethodImage(id) { - if (this.paymentMethod.image) + if (this.paymentMethod.image) { return `/web/image/pos.payment.method/${id}/image`; - else if (this.paymentMethod.type === "cash") + } else if (this.paymentMethod.type === "cash") { return "/point_of_sale/static/src/img/money.png"; - else if(this.paymentMethod.type === "pay_later") + } else if (this.paymentMethod.type === "pay_later") { return "/point_of_sale/static/src/img/pay-later.png"; - else + } else { return "/point_of_sale/static/src/img/card-bank.png"; + } } async _isOrderValid(isForceValidate) { if (this.currentOrder.get_orderlines().length === 0 && this.currentOrder.is_to_invoice()) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Empty Order"), body: this.env._t( "There must be at least one product in your order before it can be validated and invoiced." @@ -319,7 +336,7 @@ class PaymentScreen extends PosComponent { ); if (splitPayments.length && !this.currentOrder.get_partner()) { const paymentMethod = splitPayments[0].payment_method; - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Customer Required"), body: _.str.sprintf( this.env._t("Customer is required for %s payment method."), @@ -336,7 +353,7 @@ class PaymentScreen extends PosComponent { (this.currentOrder.is_to_invoice() || this.currentOrder.getShippingDate()) && !this.currentOrder.get_partner() ) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Please select the Customer"), body: this.env._t( "You need to select the customer before you can invoice or ship an order." @@ -353,7 +370,7 @@ class PaymentScreen extends PosComponent { this.currentOrder.getShippingDate() && !(partner.name && partner.street && partner.city && partner.country_id) ) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Incorrect address for shipping"), body: this.env._t("The selected customer needs an address."), }); @@ -374,7 +391,7 @@ class PaymentScreen extends PosComponent { if (this.currentOrder.has_not_valid_rounding()) { var line = this.currentOrder.has_not_valid_rounding(); - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Incorrect rounding"), body: this.env._t( "You have to round your payments lines." + line.amount + " is not rounded." @@ -396,7 +413,7 @@ class PaymentScreen extends PosComponent { cash = cash || this.env.pos.payment_methods[i].is_cash_count; } if (!cash) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Cannot return change without a cash payment method"), body: this.env._t( "There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration" @@ -412,7 +429,7 @@ class PaymentScreen extends PosComponent { this.currentOrder.get_total_with_tax() > 0 && this.currentOrder.get_total_with_tax() * 1000 < this.currentOrder.get_total_paid() ) { - this.showPopup("ConfirmPopup", { + this.showPopup(ConfirmPopup, { title: this.env._t("Please Confirm Large Amount"), body: this.env._t("Are you sure that the customer wants to pay") + @@ -500,8 +517,5 @@ class PaymentScreen extends PosComponent { line.set_payment_status("done"); } } -PaymentScreen.template = "PaymentScreen"; -Registries.Component.add(PaymentScreen); - -export default PaymentScreen; +registry.category("pos_screens").add("PaymentScreen", PaymentScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js index 8db09d3f16f..e971c77cdb2 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js @@ -1,16 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +import { PSNumpadInputButton } from "./PSNumpadInputButton"; + +export class PaymentScreenNumpad extends PosComponent { + static components = { PSNumpadInputButton }; + static template = "PaymentScreenNumpad"; -class PaymentScreenNumpad extends PosComponent { setup() { super.setup(); this.decimalPoint = this.env._t.database.parameters.decimal_point; } } -PaymentScreenNumpad.template = "PaymentScreenNumpad"; - -Registries.Component.add(PaymentScreenNumpad); - -export default PaymentScreenNumpad; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js index 7a0b4185284..13ed6001f21 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class PaymentScreenPaymentLines extends PosComponent { + static template = "PaymentScreenPaymentLines"; -class PaymentScreenPaymentLines extends PosComponent { formatLineAmount(paymentline) { return this.env.pos.format_currency_no_symbol(paymentline.get_amount()); } @@ -14,8 +15,3 @@ class PaymentScreenPaymentLines extends PosComponent { return {}; } } -PaymentScreenPaymentLines.template = "PaymentScreenPaymentLines"; - -Registries.Component.add(PaymentScreenPaymentLines); - -export default PaymentScreenPaymentLines; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js index 2cdc39e7081..54e9673af7c 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class PaymentScreenStatus extends PosComponent { + static template = "PaymentScreenStatus"; -class PaymentScreenStatus extends PosComponent { get changeText() { return this.env.pos.format_currency(this.props.order.get_change()); } @@ -18,8 +19,3 @@ class PaymentScreenStatus extends PosComponent { ); } } -PaymentScreenStatus.template = "PaymentScreenStatus"; - -Registries.Component.add(PaymentScreenStatus); - -export default PaymentScreenStatus; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js index b7b8ece117e..4c3b5ff5a26 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js @@ -1,23 +1,19 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; /** * @props partner * @emits click-partner * @emits click-pay */ -class ActionpadWidget extends PosComponent { +export class ActionpadWidget extends PosComponent { + static template = "ActionpadWidget"; + static defaultProps = { + isActionButtonHighlighted: false, + }; + get isLongName() { return this.props.partner && this.props.partner.name.length > 10; } } -ActionpadWidget.template = "ActionpadWidget"; -ActionpadWidget.defaultProps = { - isActionButtonHighlighted: false, -}; - -Registries.Component.add(ActionpadWidget); - -export default ActionpadWidget; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/CategoryButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/CategoryButton.js index af034bf1d1a..ad7a7e02f12 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/CategoryButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/CategoryButton.js @@ -1,16 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class CategoryButton extends PosComponent { + static template = "CategoryButton"; -class CategoryButton extends PosComponent { get imageUrl() { const category = this.props.category; return `/web/image?model=pos.category&field=image_128&id=${category.id}&unique=${category.write_date}`; } } -CategoryButton.template = "CategoryButton"; - -Registries.Component.add(CategoryButton); - -export default CategoryButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineCustomerNoteButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineCustomerNoteButton.js index 2445f3e3d67..c1345e4a7d9 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineCustomerNoteButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineCustomerNoteButton.js @@ -1,11 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { TextAreaPopup } from "@point_of_sale/js/Popups/TextAreaPopup"; + +export class OrderlineCustomerNoteButton extends PosComponent { + static template = "OrderlineCustomerNoteButton"; -class OrderlineCustomerNoteButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -16,7 +18,7 @@ class OrderlineCustomerNoteButton extends PosComponent { return; } - const { confirmed, payload: inputNote } = await this.showPopup("TextAreaPopup", { + const { confirmed, payload: inputNote } = await this.showPopup(TextAreaPopup, { startingValue: selectedOrderline.get_customer_note(), title: this.env._t("Add Customer Note"), }); @@ -26,12 +28,7 @@ class OrderlineCustomerNoteButton extends PosComponent { } } } -OrderlineCustomerNoteButton.template = "OrderlineCustomerNoteButton"; ProductScreen.addControlButton({ component: OrderlineCustomerNoteButton, }); - -Registries.Component.add(OrderlineCustomerNoteButton); - -export default OrderlineCustomerNoteButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/ProductInfoButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/ProductInfoButton.js index 4c8440ca6fc..091ed2fd88f 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/ProductInfoButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/ProductInfoButton.js @@ -1,11 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { ProductInfoPopup } from "@point_of_sale/js/Popups/ProductInfoPopup"; + +export class ProductInfoButton extends PosComponent { + static template = "ProductInfoButton"; -class ProductInfoButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -16,17 +18,12 @@ class ProductInfoButton extends PosComponent { const product = orderline.get_product(); const quantity = orderline.get_quantity(); const info = await this.env.pos.getProductInfo(product, quantity); - this.showPopup("ProductInfoPopup", { info: info, product: product }); + this.showPopup(ProductInfoPopup, { info: info, product: product }); } } } -ProductInfoButton.template = "ProductInfoButton"; ProductScreen.addControlButton({ component: ProductInfoButton, position: ["before", "SetFiscalPositionButton"], }); - -Registries.Component.add(ProductInfoButton); - -export default ProductInfoButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/RefundButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/RefundButton.js index b21861ff640..56019ebc476 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/RefundButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/RefundButton.js @@ -1,11 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -class RefundButton extends PosComponent { +export class RefundButton extends PosComponent { + static template = "point_of_sale.RefundButton"; + setup() { super.setup(); useListener("click", this._onClick); @@ -19,7 +20,6 @@ class RefundButton extends PosComponent { }); } } -RefundButton.template = "point_of_sale.RefundButton"; ProductScreen.addControlButton({ component: RefundButton, @@ -27,7 +27,3 @@ ProductScreen.addControlButton({ return true; }, }); - -Registries.Component.add(RefundButton); - -export default RefundButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js index 0a0a55f2aec..0ee0b354ee4 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js @@ -1,11 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; + +export class SetFiscalPositionButton extends PosComponent { + static template = "SetFiscalPositionButton"; -class SetFiscalPositionButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -38,7 +40,7 @@ class SetFiscalPositionButton extends PosComponent { }); } const { confirmed, payload: selectedFiscalPosition } = await this.showPopup( - "SelectionPopup", + SelectionPopup, { title: this.env._t("Select Fiscal Position"), list: fiscalPosList, @@ -54,7 +56,6 @@ class SetFiscalPositionButton extends PosComponent { } } } -SetFiscalPositionButton.template = "SetFiscalPositionButton"; ProductScreen.addControlButton({ component: SetFiscalPositionButton, @@ -63,7 +64,3 @@ ProductScreen.addControlButton({ }, position: ["before", "SetPricelistButton"], }); - -Registries.Component.add(SetFiscalPositionButton); - -export default SetFiscalPositionButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js index 2da6a0bcb2b..7473d55a0e8 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js @@ -1,11 +1,13 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; + +export class SetPricelistButton extends PosComponent { + static template = "SetPricelistButton"; -class SetPricelistButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); @@ -24,21 +26,21 @@ class SetPricelistButton extends PosComponent { const selectionList = this.env.pos.pricelists.map((pricelist) => ({ id: pricelist.id, label: pricelist.name, - isSelected: this.currentOrder.pricelist - && pricelist.id === this.currentOrder.pricelist.id, + isSelected: + this.currentOrder.pricelist && pricelist.id === this.currentOrder.pricelist.id, item: pricelist, })); if (!this.env.pos.default_pricelist) { selectionList.push({ id: null, - label: this.env._t('Default Price'), + label: this.env._t("Default Price"), isSelected: !this.currentOrder.pricelist, item: null, - }) + }); } - const { confirmed, payload: selectedPricelist } = await this.showPopup("SelectionPopup", { + const { confirmed, payload: selectedPricelist } = await this.showPopup(SelectionPopup, { title: this.env._t("Select the pricelist"), list: selectionList, }); @@ -48,7 +50,6 @@ class SetPricelistButton extends PosComponent { } } } -SetPricelistButton.template = "SetPricelistButton"; ProductScreen.addControlButton({ component: SetPricelistButton, @@ -56,7 +57,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.use_pricelist && this.env.pos.pricelists.length > 0; }, }); - -Registries.Component.add(SetPricelistButton); - -export default SetPricelistButton; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/NumpadWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/NumpadWidget.js index 933fccd1983..d9a690a0583 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/NumpadWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/NumpadWidget.js @@ -1,7 +1,6 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; /** * @prop {'quantity' | 'price' | 'discount'} activeMode @@ -10,7 +9,13 @@ import Registries from "@point_of_sale/js/Registries"; * @event set-numpad-mode - triggered when mode button is clicked * @event numpad-click-input - triggered when numpad button is clicked */ -class NumpadWidget extends PosComponent { +export class NumpadWidget extends PosComponent { + static template = "NumpadWidget"; + static defaultProps = { + disabledModes: [], + disableSign: false, + }; + get hasPriceControlRights() { return ( this.env.pos.cashierHasPriceControlRights() && @@ -38,12 +43,3 @@ class NumpadWidget extends PosComponent { return this.env._t.database.parameters.decimal_point; } } -NumpadWidget.template = "NumpadWidget"; -NumpadWidget.defaultProps = { - disabledModes: [], - disableSign: false, -}; - -Registries.Component.add(NumpadWidget); - -export default NumpadWidget; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderSummary.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderSummary.js index b77f9060b10..4fc28439648 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderSummary.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderSummary.js @@ -1,10 +1,11 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { float_is_zero } from "web.utils"; -class OrderSummary extends PosComponent { +export class OrderSummary extends PosComponent { + static template = "OrderSummary"; + getTotal() { return this.env.pos.format_currency(this.props.order.get_total_with_tax()); } @@ -18,8 +19,3 @@ class OrderSummary extends PosComponent { }; } } -OrderSummary.template = "OrderSummary"; - -Registries.Component.add(OrderSummary); - -export default OrderSummary; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderWidget.js index b03704f2a87..46feb8667f5 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/OrderWidget.js @@ -1,12 +1,18 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { EditListPopup } from "@point_of_sale/js/Popups/EditListPopup"; + +import { Orderline } from "./Orderline"; +import { OrderSummary } from "./OrderSummary"; const { useEffect, useRef } = owl; -class OrderWidget extends PosComponent { +export class OrderWidget extends PosComponent { + static components = { Orderline, OrderSummary }; + static template = "OrderWidget"; + setup() { super.setup(); useListener("select-line", this._selectLine); @@ -42,7 +48,7 @@ class OrderWidget extends PosComponent { const orderline = event.detail.orderline; const isAllowOnlyOneLot = orderline.product.isAllowOnlyOneLot(); const packLotLinesToEdit = orderline.getPackLotLinesToEdit(isAllowOnlyOneLot); - const { confirmed, payload } = await this.showPopup("EditListPopup", { + const { confirmed, payload } = await this.showPopup(EditListPopup, { title: this.env._t("Lot/Serial Number(s) Required"), name: orderline.product.display_name, isSingleItem: isAllowOnlyOneLot, @@ -62,8 +68,3 @@ class OrderWidget extends PosComponent { this.order.select_orderline(event.detail.orderline); } } -OrderWidget.template = "OrderWidget"; - -Registries.Component.add(OrderWidget); - -export default OrderWidget; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js index 29aa8ee0126..7c4173a5e1d 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class Orderline extends PosComponent { + static template = "Orderline"; -class Orderline extends PosComponent { selectLine() { this.trigger("select-line", { orderline: this.props.line }); } @@ -19,8 +20,3 @@ class Orderline extends PosComponent { return this.props.line.get_customer_note(); } } -Orderline.template = "Orderline"; - -Registries.Component.add(Orderline); - -export default Orderline; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductItem.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductItem.js index 87a42cf4bc4..2eae514a991 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductItem.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductItem.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class ProductItem extends PosComponent { + static template = "ProductItem"; -class ProductItem extends PosComponent { /** * For accessibility, pressing should be like clicking the product. * is not considered because it conflicts with the barcode. @@ -40,8 +41,3 @@ class ProductItem extends PosComponent { } } } -ProductItem.template = "ProductItem"; - -Registries.Component.add(ProductItem); - -export default ProductItem; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js index d9f1ebe879b..20f1a85f680 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js @@ -1,16 +1,39 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ControlButtonsMixin from "@point_of_sale/js/ControlButtonsMixin"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ControlButtonsMixin } from "@point_of_sale/js/ControlButtonsMixin"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { registry } from "@web/core/registry"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; import { parse } from "web.field_utils"; +import { ProductConfiguratorPopup } from "@point_of_sale/js/Popups/ProductConfiguratorPopup"; +import { EditListPopup } from "@point_of_sale/js/Popups/EditListPopup"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ErrorBarcodePopup } from "@point_of_sale/js/Popups/ErrorBarcodePopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; +import { ControlButtonPopup } from "@point_of_sale/js/Popups/ControlButtonPopup"; + +import { ActionpadWidget } from "./ActionpadWidget"; +import { MobileOrderWidget } from "../../Misc/MobileOrderWidget"; +import { NumpadWidget } from "./NumpadWidget"; +import { OrderWidget } from "./OrderWidget"; +import { ProductsWidget } from "./ProductsWidget"; + const { onMounted, useState } = owl; -class ProductScreen extends ControlButtonsMixin(PosComponent) { +export class ProductScreen extends ControlButtonsMixin(PosComponent) { + static template = "ProductScreen"; + static components = { + ActionpadWidget, + MobileOrderWidget, + NumpadWidget, + OrderWidget, + ProductsWidget, + }; + setup() { super.setup(); useListener("update-selected-orderline", this._updateSelectedOrderline); @@ -27,16 +50,16 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { discount: this._barcodeDiscountAction, error: this._barcodeErrorAction, }); - NumberBuffer.use({ + numberBuffer.use({ nonKeyboardInputEvent: "numpad-click-input", triggerAtInput: "update-selected-orderline", useWithBarcode: true, }); onMounted(this.onMounted); - // Call `reset` when the `onMounted` callback in `NumberBuffer.use` is done. + // Call `reset` when the `onMounted` callback in `numberBuffer.use` is done. // We don't do this in the `mounted` lifecycle method because it is called before // the callbacks in `onMounted` hook. - onMounted(() => NumberBuffer.reset()); + onMounted(() => numberBuffer.reset()); this.state = useState({ mobile_pane: this.props.mobile_pane || "right", }); @@ -67,7 +90,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { product.attribute_line_ids, (id) => this.env.pos.attributes_by_ptal_id[id] ).filter((attr) => attr !== undefined); - const { confirmed, payload } = await this.showPopup("ProductConfiguratorPopup", { + const { confirmed, payload } = await this.showPopup(ProductConfiguratorPopup, { product: product, attributes: attributes, }); @@ -100,7 +123,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { packLotLinesToEdit = []; } } - const { confirmed, payload } = await this.showPopup("EditListPopup", { + const { confirmed, payload } = await this.showPopup(EditListPopup, { title: this.env._t("Lot/Serial Number(s) Required"), name: product.display_name, isSingleItem: isAllowOnlyOneLot, @@ -161,16 +184,16 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { } // Add the product after having the extra information. await this._addProduct(product, options); - NumberBuffer.reset(); + numberBuffer.reset(); } _setNumpadMode(event) { const { mode } = event.detail; - NumberBuffer.capture(); - NumberBuffer.reset(); + numberBuffer.capture(); + numberBuffer.reset(); this.env.pos.numpadMode = mode; } _selectLine() { - NumberBuffer.reset(); + numberBuffer.reset(); } async _updateSelectedOrderline(event) { if (this.env.pos.numpadMode === "quantity" && this.env.pos.disallowLineQuantityChange()) { @@ -187,7 +210,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { .get_quantity(); if (selectedLine.noDecrease) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Invalid action"), body: this.env._t("You are not allowed to change this quantity"), }); @@ -206,7 +229,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { const val = buffer === null ? "remove" : buffer; this._setValue(val); if (val == "remove") { - NumberBuffer.reset(); + numberBuffer.reset(); this.env.pos.numpadMode = "quantity"; } } @@ -216,7 +239,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { if (this.env.pos.numpadMode === "quantity") { const result = this.currentOrder.get_selected_orderline().set_quantity(val); if (!result) { - NumberBuffer.reset(); + numberBuffer.reset(); } } else if (this.env.pos.numpadMode === "discount") { this.currentOrder.get_selected_orderline().set_discount(val); @@ -296,7 +319,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { // Why? Because once we start declaring barcode actions in different // screens, these methods will also be declared over and over. _barcodeErrorAction(code) { - this.showPopup("ErrorBarcodePopup", { code: this._codeRepr(code) }); + this.showPopup(ErrorBarcodePopup, { code: this._codeRepr(code) }); } _codeRepr(code) { if (code.code.length > 32) { @@ -306,7 +329,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { } } async _displayAllControlPopup() { - await this.showPopup("ControlButtonPopup", { + await this.showPopup(ControlButtonPopup, { controlButtons: this.controlButtons, }); } @@ -316,7 +339,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { */ async _onScaleNotAvailable() {} async _showDecreaseQuantityPopup() { - const { confirmed, payload: inputNumber } = await this.showPopup("NumberPopup", { + const { confirmed, payload: inputNumber } = await this.showPopup(NumberPopup, { startingValue: 0, title: this.env._t("Set the new quantity"), }); @@ -347,7 +370,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { // IMPROVEMENT: This code snippet is very similar to selectPartner of PaymentScreen. const currentPartner = this.currentOrder.get_partner(); if (currentPartner && this.currentOrder.getHasRefundLines()) { - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Can't change customer"), body: _.str.sprintf( this.env._t( @@ -377,7 +400,7 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { (this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots) ) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Some Serial/Lot Numbers are missing"), body: this.env._t( "You are trying to sell products with serial/lot numbers, but some of them are not set.\nWould you like to proceed anyway?" @@ -396,8 +419,5 @@ class ProductScreen extends ControlButtonsMixin(PosComponent) { this.state.mobile_pane = this.state.mobile_pane === "left" ? "right" : "left"; } } -ProductScreen.template = "ProductScreen"; -Registries.Component.add(ProductScreen); - -export default ProductScreen; +registry.category("pos_screens").add("ProductScreen", ProductScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js index 5d1a0d6e55c..e59cd92d2e7 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js @@ -1,13 +1,18 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; import { usePos } from "@point_of_sale/app/pos_store"; +import { ProductItem } from "./ProductItem"; +import { ProductsWidgetControlPanel } from "./ProductsWidgetControlPanel"; + const { useState } = owl; -class ProductsWidget extends PosComponent { +export class ProductsWidget extends PosComponent { + static components = { ProductItem, ProductsWidgetControlPanel }; + static template = "ProductsWidget"; + /** * @param {Object} props * @param {number?} props.startCategoryId @@ -78,8 +83,3 @@ class ProductsWidget extends PosComponent { this.trigger("switch-category", 0); } } -ProductsWidget.template = "ProductsWidget"; - -Registries.Component.add(ProductsWidget); - -export default ProductsWidget; diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidgetControlPanel.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidgetControlPanel.js index 7d1df6c49df..d1717a65775 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidgetControlPanel.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidgetControlPanel.js @@ -1,13 +1,17 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { debounce } from "@web/core/utils/timing"; import { usePos } from "@point_of_sale/app/pos_store"; +import { CategoryButton } from "./CategoryButton"; + import { onMounted, onWillUnmount, useState } from "@odoo/owl"; -class ProductsWidgetControlPanel extends PosComponent { +export class ProductsWidgetControlPanel extends PosComponent { + static components = { CategoryButton }; + static template = "ProductsWidgetControlPanel"; + setup() { super.setup(); this.pos = usePos(); @@ -88,8 +92,3 @@ class ProductsWidgetControlPanel extends PosComponent { return ProductIds; } } -ProductsWidgetControlPanel.template = "ProductsWidgetControlPanel"; - -Registries.Component.add(ProductsWidgetControlPanel); - -export default ProductsWidgetControlPanel; diff --git a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/OrderReceipt.js b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/OrderReceipt.js index 8de7f185c5f..437af9b4af0 100644 --- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/OrderReceipt.js +++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/OrderReceipt.js @@ -1,11 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +import { WrappedProductNameLines } from "./WrappedProductNameLines"; const { onWillUpdateProps } = owl; -class OrderReceipt extends PosComponent { +export class OrderReceipt extends PosComponent { + static components = { WrappedProductNameLines }; + static template = "OrderReceipt"; + setup() { super.setup(); this._receiptEnv = this.props.order.getOrderReceiptEnv(); @@ -41,8 +45,3 @@ class OrderReceipt extends PosComponent { ); } } -OrderReceipt.template = "OrderReceipt"; - -Registries.Component.add(OrderReceipt); - -export default OrderReceipt; diff --git a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js index 1018af53cdd..ea20f33efff 100644 --- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -3,150 +3,149 @@ import { Printer } from "@point_of_sale/js/printers"; import { is_email } from "web.utils"; import { useErrorHandlers } from "@point_of_sale/js/custom_hooks"; -import Registries from "@point_of_sale/js/Registries"; -import AbstractReceiptScreen from "@point_of_sale/js/Misc/AbstractReceiptScreen"; +import { AbstractReceiptScreen } from "@point_of_sale/js/Misc/AbstractReceiptScreen"; +import { registry } from "@web/core/registry"; + +import { OrderReceipt } from "./OrderReceipt"; import { onMounted, useRef, status } from "@odoo/owl"; -const ReceiptScreen = (AbstractReceiptScreen) => { - class ReceiptScreen extends AbstractReceiptScreen { - setup() { - super.setup(); - useErrorHandlers(); - this.orderReceipt = useRef("order-receipt"); - const order = this.currentOrder; - const partner = order.get_partner(); - this.orderUiState = order.uiState.ReceiptScreen; - this.orderUiState.inputEmail = - this.orderUiState.inputEmail || (partner && partner.email) || ""; - this.is_email = is_email; +export class ReceiptScreen extends AbstractReceiptScreen { + static template = "ReceiptScreen"; + static components = { OrderReceipt }; - onMounted(() => { - // Here, we send a task to the event loop that handles - // the printing of the receipt when the component is mounted. - // We are doing this because we want the receipt screen to be - // displayed regardless of what happen to the handleAutoPrint - // call. - setTimeout(async () => { - if (status(this) === "mounted") { - const images = this.orderReceipt.el.getElementsByTagName("img"); - for (const image of images) { - await image.decode(); - } - await this.handleAutoPrint(); + setup() { + super.setup(); + useErrorHandlers(); + this.orderReceipt = useRef("order-receipt"); + const order = this.currentOrder; + const partner = order.get_partner(); + this.orderUiState = order.uiState.ReceiptScreen; + this.orderUiState.inputEmail = + this.orderUiState.inputEmail || (partner && partner.email) || ""; + this.is_email = is_email; + + onMounted(() => { + // Here, we send a task to the event loop that handles + // the printing of the receipt when the component is mounted. + // We are doing this because we want the receipt screen to be + // displayed regardless of what happen to the handleAutoPrint + // call. + setTimeout(async () => { + if (status(this) === "mounted") { + const images = this.orderReceipt.el.getElementsByTagName("img"); + for (const image of images) { + await image.decode(); } - }, 0); - }); - } - _addNewOrder() { - this.env.pos.add_new_order(); - } - async onSendEmail() { - if (!is_email(this.orderUiState.inputEmail)) { - this.orderUiState.emailSuccessful = false; - this.orderUiState.emailNotice = this.env._t("Invalid email."); - return; - } - try { - await this._sendReceiptToCustomer(); - this.orderUiState.emailSuccessful = true; - this.orderUiState.emailNotice = this.env._t("Email sent."); - } catch { - this.orderUiState.emailSuccessful = false; - this.orderUiState.emailNotice = this.env._t( - "Sending email failed. Please try again." - ); - } - } - get orderAmountPlusTip() { - const order = this.currentOrder; - const orderTotalAmount = order.get_total_with_tax(); - const tip_product_id = - this.env.pos.config.tip_product_id && this.env.pos.config.tip_product_id[0]; - const tipLine = order - .get_orderlines() - .find((line) => tip_product_id && line.product.id === tip_product_id); - const tipAmount = tipLine ? tipLine.get_all_prices().priceWithTax : 0; - const orderAmountStr = this.env.pos.format_currency(orderTotalAmount - tipAmount); - if (!tipAmount) { - return orderAmountStr; - } - const tipAmountStr = this.env.pos.format_currency(tipAmount); - return `${orderAmountStr} + ${tipAmountStr} tip`; - } - get currentOrder() { - return this.env.pos.get_order(); - } - get nextScreen() { - return { name: "ProductScreen" }; - } - whenClosing() { - this.orderDone(); - } - /** - * This function is called outside the rendering call stack. This way, - * we don't block the displaying of ReceiptScreen when it is mounted; additionally, - * any error that can happen during the printing does not affect the rendering. - */ - async handleAutoPrint() { - if (this._shouldAutoPrint()) { - await this.printReceipt(); - if (this.currentOrder._printed && this._shouldCloseImmediately()) { - this.whenClosing(); + await this.handleAutoPrint(); } - } + }, 0); + }); + } + _addNewOrder() { + this.env.pos.add_new_order(); + } + async onSendEmail() { + if (!is_email(this.orderUiState.inputEmail)) { + this.orderUiState.emailSuccessful = false; + this.orderUiState.emailNotice = this.env._t("Invalid email."); + return; } - orderDone() { - this.env.pos.removeOrder(this.currentOrder); - this._addNewOrder(); - const { name, props } = this.nextScreen; - this.showScreen(name, props); - if (this.env.pos.config.iface_customer_facing_display) { - this.env.pos.send_current_order_to_customer_facing_display(); - } - } - async printReceipt() { - const isPrinted = await this._printReceipt(); - if (isPrinted) { - this.currentOrder._printed = true; - } - } - _shouldAutoPrint() { - return this.env.pos.config.iface_print_auto && !this.currentOrder._printed; - } - _shouldCloseImmediately() { - var invoiced_finalized = this.currentOrder.is_to_invoice() - ? this.currentOrder.finalized - : true; - return ( - this.env.proxy.printer && - this.env.pos.config.iface_print_skip_screen && - invoiced_finalized + try { + await this._sendReceiptToCustomer(); + this.orderUiState.emailSuccessful = true; + this.orderUiState.emailNotice = this.env._t("Email sent."); + } catch { + this.orderUiState.emailSuccessful = false; + this.orderUiState.emailNotice = this.env._t( + "Sending email failed. Please try again." ); } - async _sendReceiptToCustomer() { - const printer = new Printer(null, this.env.pos); - const receiptString = this.orderReceipt.el.innerHTML; - const ticketImage = await printer.htmlToImg(receiptString); - const order = this.currentOrder; - const partner = order.get_partner(); - const orderName = order.get_name(); - const orderPartner = { - email: this.orderUiState.inputEmail, - name: partner ? partner.name : this.orderUiState.inputEmail, - }; - const order_server_id = this.env.pos.validated_orders_name_server_id_map[orderName]; - await this.rpc({ - model: "pos.order", - method: "action_receipt_to_customer", - args: [[order_server_id], orderName, orderPartner, ticketImage], - }); + } + get orderAmountPlusTip() { + const order = this.currentOrder; + const orderTotalAmount = order.get_total_with_tax(); + const tip_product_id = + this.env.pos.config.tip_product_id && this.env.pos.config.tip_product_id[0]; + const tipLine = order + .get_orderlines() + .find((line) => tip_product_id && line.product.id === tip_product_id); + const tipAmount = tipLine ? tipLine.get_all_prices().priceWithTax : 0; + const orderAmountStr = this.env.pos.format_currency(orderTotalAmount - tipAmount); + if (!tipAmount) { + return orderAmountStr; + } + const tipAmountStr = this.env.pos.format_currency(tipAmount); + return `${orderAmountStr} + ${tipAmountStr} tip`; + } + get currentOrder() { + return this.env.pos.get_order(); + } + get nextScreen() { + return { name: "ProductScreen" }; + } + whenClosing() { + this.orderDone(); + } + /** + * This function is called outside the rendering call stack. This way, + * we don't block the displaying of ReceiptScreen when it is mounted; additionally, + * any error that can happen during the printing does not affect the rendering. + */ + async handleAutoPrint() { + if (this._shouldAutoPrint()) { + await this.printReceipt(); + if (this.currentOrder._printed && this._shouldCloseImmediately()) { + this.whenClosing(); + } } } - ReceiptScreen.template = "ReceiptScreen"; - return ReceiptScreen; -}; + orderDone() { + this.env.pos.removeOrder(this.currentOrder); + this._addNewOrder(); + const { name, props } = this.nextScreen; + this.showScreen(name, props); + if (this.env.pos.config.iface_customer_facing_display) { + this.env.pos.send_current_order_to_customer_facing_display(); + } + } + async printReceipt() { + const isPrinted = await this._printReceipt(); + if (isPrinted) { + this.currentOrder._printed = true; + } + } + _shouldAutoPrint() { + return this.env.pos.config.iface_print_auto && !this.currentOrder._printed; + } + _shouldCloseImmediately() { + var invoiced_finalized = this.currentOrder.is_to_invoice() + ? this.currentOrder.finalized + : true; + return ( + this.env.proxy.printer && + this.env.pos.config.iface_print_skip_screen && + invoiced_finalized + ); + } + async _sendReceiptToCustomer() { + const printer = new Printer(null, this.env.pos); + const receiptString = this.orderReceipt.el.innerHTML; + const ticketImage = await printer.htmlToImg(receiptString); + const order = this.currentOrder; + const partner = order.get_partner(); + const orderName = order.get_name(); + const orderPartner = { + email: this.orderUiState.inputEmail, + name: partner ? partner.name : this.orderUiState.inputEmail, + }; + const order_server_id = this.env.pos.validated_orders_name_server_id_map[orderName]; + await this.rpc({ + model: "pos.order", + method: "action_receipt_to_customer", + args: [[order_server_id], orderName, orderPartner, ticketImage], + }); + } +} -Registries.Component.addByExtending(ReceiptScreen, AbstractReceiptScreen); - -export default ReceiptScreen; +registry.category("pos_screens").add("ReceiptScreen", ReceiptScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/WrappedProductNameLines.js b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/WrappedProductNameLines.js index 282954aefd0..f41d09c9d9e 100644 --- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/WrappedProductNameLines.js +++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/WrappedProductNameLines.js @@ -1,11 +1,7 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; -class WrappedProductNameLines extends PosComponent {} -WrappedProductNameLines.template = "WrappedProductNameLines"; - -Registries.Component.add(WrappedProductNameLines); - -export default WrappedProductNameLines; +export class WrappedProductNameLines extends PosComponent { + static template = "WrappedProductNameLines"; +} diff --git a/addons/point_of_sale/static/src/js/Screens/ScaleScreen/ScaleScreen.js b/addons/point_of_sale/static/src/js/Screens/ScaleScreen/ScaleScreen.js index 78b57cb11f4..4404522ecb0 100644 --- a/addons/point_of_sale/static/src/js/Screens/ScaleScreen/ScaleScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ScaleScreen/ScaleScreen.js @@ -1,12 +1,14 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { round_precision as round_pr } from "web.utils"; -import Registries from "@point_of_sale/js/Registries"; +import { registry } from "@web/core/registry"; const { onMounted, onWillUnmount, useExternalListener, useState } = owl; -class ScaleScreen extends PosComponent { +export class ScaleScreen extends PosComponent { + static template = "ScaleScreen"; + /** * @param {Object} props * @param {Object} props.product The product to weight. @@ -95,8 +97,5 @@ class ScaleScreen extends PosComponent { : ""; } } -ScaleScreen.template = "ScaleScreen"; -Registries.Component.add(ScaleScreen); - -export default ScaleScreen; +registry.category("pos_screens").add("ScaleScreen", ScaleScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js index 0fa138f85a0..611cdb78e5d 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js @@ -1,10 +1,13 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; + +export class InvoiceButton extends PosComponent { + static template = "InvoiceButton"; -class InvoiceButton extends PosComponent { setup() { super.setup(); useListener("click", this._onClick); @@ -42,7 +45,7 @@ class InvoiceButton extends PosComponent { throw error; } else { // NOTE: error here is most probably undefined - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Network Error"), body: this.env._t("Unable to download invoice."), }); @@ -66,7 +69,7 @@ class InvoiceButton extends PosComponent { // Part 1: Handle missing partner. // Write to pos.order the selected partner. if (!order.get_partner()) { - const { confirmed: confirmedPopup } = await this.showPopup("ConfirmPopup", { + const { confirmed: confirmedPopup } = await this.showPopup(ConfirmPopup, { title: this.env._t("Need customer to invoice"), body: this.env._t("Do you want to open the customer list to select customer?"), }); @@ -115,7 +118,3 @@ class InvoiceButton extends PosComponent { } } } -InvoiceButton.template = "InvoiceButton"; -Registries.Component.add(InvoiceButton); - -export default InvoiceButton; diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/ReprintReceiptButton.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/ReprintReceiptButton.js index 6eb5d8bc97c..b5f07fa523c 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/ReprintReceiptButton.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/ReprintReceiptButton.js @@ -1,10 +1,11 @@ /** @odoo-module */ import { useListener } from "@web/core/utils/hooks"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; + +export class ReprintReceiptButton extends PosComponent { + static template = "ReprintReceiptButton"; -class ReprintReceiptButton extends PosComponent { setup() { super.setup(); useListener("click", this._onClick); @@ -16,7 +17,3 @@ class ReprintReceiptButton extends PosComponent { this.showScreen("ReprintReceiptScreen", { order: this.props.order }); } } -ReprintReceiptButton.template = "ReprintReceiptButton"; -Registries.Component.add(ReprintReceiptButton); - -export default ReprintReceiptButton; diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderDetails.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderDetails.js index ffabdb098c8..be6e3b219d7 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderDetails.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderDetails.js @@ -1,12 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { OrderlineDetails } from "./OrderlineDetails"; /** * @props {models.Order} order */ -class OrderDetails extends PosComponent { +export class OrderDetails extends PosComponent { + static components = { OrderlineDetails }; + static template = "OrderDetails"; + get order() { return this.props.order; } @@ -20,8 +23,3 @@ class OrderDetails extends PosComponent { return this.env.pos.format_currency(this.order ? this.order.get_total_tax() : 0); } } -OrderDetails.template = "OrderDetails"; - -Registries.Component.add(OrderDetails); - -export default OrderDetails; diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderlineDetails.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderlineDetails.js index b4de4db9c77..9db44a405ff 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderlineDetails.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/OrderlineDetails.js @@ -1,14 +1,15 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import { format } from "web.field_utils"; import { round_precision as round_pr } from "web.utils"; /** * @props {pos.order.line} line */ -class OrderlineDetails extends PosComponent { +export class OrderlineDetails extends PosComponent { + static template = "OrderlineDetails"; + get line() { const line = this.props.line; const formatQty = (line) => { @@ -72,8 +73,3 @@ class OrderlineDetails extends PosComponent { return _.str.sprintf(this.env._t("To Refund: %s"), this.getFormattedToRefundQty()); } } -OrderlineDetails.template = "OrderlineDetails"; - -Registries.Component.add(OrderlineDetails); - -export default OrderlineDetails; diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js index 22b5050b08d..98639465775 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js @@ -1,35 +1,33 @@ /** @odoo-module */ -import AbstractReceiptScreen from "@point_of_sale/js/Misc/AbstractReceiptScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { AbstractReceiptScreen } from "@point_of_sale/js/Misc/AbstractReceiptScreen"; +import { registry } from "@web/core/registry"; +import { OrderReceipt } from "../ReceiptScreen/OrderReceipt"; -const ReprintReceiptScreen = (AbstractReceiptScreen) => { - class ReprintReceiptScreen extends AbstractReceiptScreen { - setup() { - super.setup(); - owl.onMounted(this.onMounted); - } - onMounted() { - this.printReceipt(); - } - confirm() { - this.showScreen("TicketScreen", { reuseSavedUIState: true }); - } - async printReceipt() { - if (this.env.proxy.printer && this.env.pos.config.iface_print_skip_screen) { - const result = await this._printReceipt(); - if (result) { - this.showScreen("TicketScreen", { reuseSavedUIState: true }); - } +export class ReprintReceiptScreen extends AbstractReceiptScreen { + static template = "ReprintReceiptScreen"; + static components = { OrderReceipt }; + setup() { + super.setup(); + owl.onMounted(this.onMounted); + } + onMounted() { + this.printReceipt(); + } + confirm() { + this.showScreen("TicketScreen", { reuseSavedUIState: true }); + } + async printReceipt() { + if (this.env.proxy.printer && this.env.pos.config.iface_print_skip_screen) { + const result = await this._printReceipt(); + if (result) { + this.showScreen("TicketScreen", { reuseSavedUIState: true }); } } - async tryReprint() { - await this._printReceipt(); - } } - ReprintReceiptScreen.template = "ReprintReceiptScreen"; - return ReprintReceiptScreen; -}; -Registries.Component.addByExtending(ReprintReceiptScreen, AbstractReceiptScreen); + async tryReprint() { + await this._printReceipt(); + } +} -export default ReprintReceiptScreen; +registry.category("pos_screens").add("ReprintReceiptScreen", ReprintReceiptScreen); diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js index cbec4b7221d..3374a488dda 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js @@ -1,15 +1,44 @@ /** @odoo-module */ import { Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; -import IndependentToOrderScreen from "@point_of_sale/js/Misc/IndependentToOrderScreen"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { IndependentToOrderScreen } from "@point_of_sale/js/Misc/IndependentToOrderScreen"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { registry } from "@web/core/registry"; import { useListener } from "@web/core/utils/hooks"; import { parse } from "web.field_utils"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; + +import { ActionpadWidget } from "../ProductScreen/ActionpadWidget"; +import { InvoiceButton } from "../TicketScreen/ControlButtons/InvoiceButton"; +import { NumpadWidget } from "../ProductScreen/NumpadWidget"; +import { OrderDetails } from "../TicketScreen/OrderDetails"; +import { ReprintReceiptButton } from "./ControlButtons/ReprintReceiptButton"; +import { SearchBar } from "../../Misc/SearchBar"; + const { onMounted, onWillUnmount, useState } = owl; -class TicketScreen extends IndependentToOrderScreen { +export class TicketScreen extends IndependentToOrderScreen { + static template = "TicketScreen"; + static components = { + ActionpadWidget, + InvoiceButton, + NumpadWidget, + OrderDetails, + ReprintReceiptButton, + SearchBar, + }; + static defaultProps = { + destinationOrder: null, + // When passed as true, it will use the saved _state.ui as default + // value when this component is reinstantiated. + // After setting the default value, the _state.ui will be overridden + // by the passed props.ui if there is any. + reuseSavedUIState: false, + ui: {}, + }; + setup() { super.setup(); useListener("close-screen", this._onCloseScreen); @@ -25,7 +54,7 @@ class TicketScreen extends IndependentToOrderScreen { useListener("click-refund-order-uid", this._onClickRefundOrderUid); useListener("update-selected-orderline", this._onUpdateSelectedOrderline); useListener("do-refund", this._onDoRefund); - NumberBuffer.use({ + numberBuffer.use({ nonKeyboardInputEvent: "numpad-click-input", triggerAtInput: "update-selected-orderline", }); @@ -89,7 +118,7 @@ class TicketScreen extends IndependentToOrderScreen { this._state.ui.selectedOrderlineIds[clickedOrder.backendId] = firstLine.id; } } - NumberBuffer.reset(); + numberBuffer.reset(); } else { this._setOrder(clickedOrder); } @@ -111,7 +140,7 @@ class TicketScreen extends IndependentToOrderScreen { ["ProductScreen", "PaymentScreen"].includes(screen.name) && order.get_orderlines().length > 0 ) { - const { confirmed } = await this.showPopup("ConfirmPopup", { + const { confirmed } = await this.showPopup(ConfirmPopup, { title: this.env._t("Existing orderlines"), body: _.str.sprintf( this.env._t( @@ -151,7 +180,7 @@ class TicketScreen extends IndependentToOrderScreen { _onClickOrderline({ detail: orderline }) { const order = this.getSelectedSyncedOrder(); this._state.ui.selectedOrderlineIds[order.backendId] = orderline.id; - NumberBuffer.reset(); + numberBuffer.reset(); } _onClickRefundOrderUid({ detail: orderUid }) { // Open the refund order. @@ -164,24 +193,24 @@ class TicketScreen extends IndependentToOrderScreen { const buffer = detail.buffer; const order = this.getSelectedSyncedOrder(); if (!order) { - return NumberBuffer.reset(); + return numberBuffer.reset(); } const selectedOrderlineId = this.getSelectedOrderlineId(); const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); if (!orderline) { - return NumberBuffer.reset(); + return numberBuffer.reset(); } const toRefundDetail = this._getToRefundDetail(orderline); // When already linked to an order, do not modify the to refund quantity. if (toRefundDetail.destinationOrderUid) { - return NumberBuffer.reset(); + return numberBuffer.reset(); } const refundableQty = toRefundDetail.orderline.qty - toRefundDetail.orderline.refundedQty; if (refundableQty <= 0) { - return NumberBuffer.reset(); + return numberBuffer.reset(); } if (buffer == null || buffer == "") { @@ -189,8 +218,8 @@ class TicketScreen extends IndependentToOrderScreen { } else { const quantity = Math.abs(parse.float(buffer)); if (quantity > refundableQty) { - NumberBuffer.reset(); - this.showPopup("ErrorPopup", { + numberBuffer.reset(); + this.showPopup(ErrorPopup, { title: this.env._t("Maximum Exceeded"), body: _.str.sprintf( this.env._t( @@ -665,7 +694,7 @@ class TicketScreen extends IndependentToOrderScreen { // Cache these fetched orders so that next time, no need to fetch // them again, unless invalidated. See `_onInvoiceOrder`. fetchedOrders.forEach((order) => { - this._state.syncedOrders.cache[order.id] = Order.create( + this._state.syncedOrders.cache[order.id] = new Order( {}, { pos: this.env.pos, json: order } ); @@ -687,17 +716,5 @@ class TicketScreen extends IndependentToOrderScreen { //#endregion //#endregion } -TicketScreen.template = "TicketScreen"; -TicketScreen.defaultProps = { - destinationOrder: null, - // When passed as true, it will use the saved _state.ui as default - // value when this component is reinstantiated. - // After setting the default value, the _state.ui will be overridden - // by the passed props.ui if there is any. - reuseSavedUIState: false, - ui: {}, -}; -Registries.Component.add(TicketScreen); - -export default TicketScreen; +registry.category("pos_screens").add("TicketScreen", TicketScreen); diff --git a/addons/point_of_sale/static/src/js/barcode_reader.js b/addons/point_of_sale/static/src/js/barcode_reader.js index 702473b3e05..46d6f4fa231 100644 --- a/addons/point_of_sale/static/src/js/barcode_reader.js +++ b/addons/point_of_sale/static/src/js/barcode_reader.js @@ -8,7 +8,7 @@ var Mutex = concurrency.Mutex; // is set-up to act like a keyboard. Use connect() and disconnect() to activate // and deactivate the barcode reader. Use set_action_callbacks to tell it // what to do when it reads a barcode. -var BarcodeReader = core.Class.extend({ +export const BarcodeReader = core.Class.extend({ actions: ["product", "cashier", "client"], init: function (attributes) { @@ -163,5 +163,3 @@ var BarcodeReader = core.Class.extend({ this.remote_scanning = false; }, }); - -export default BarcodeReader; diff --git a/addons/point_of_sale/static/src/js/custom_hooks.js b/addons/point_of_sale/static/src/js/custom_hooks.js index a62686e291a..f054e8ee61f 100644 --- a/addons/point_of_sale/static/src/js/custom_hooks.js +++ b/addons/point_of_sale/static/src/js/custom_hooks.js @@ -1,4 +1,9 @@ /** @odoo-module */ +import { OfflineErrorPopup } from "./Popups/OfflineErrorPopup"; +import { ConfirmPopup } from "./Popups/ConfirmPopup"; +import { ErrorTracebackPopup } from "./Popups/ErrorTracebackPopup"; +import { ErrorPopup } from "./Popups/ErrorPopup"; + const { onMounted, onPatched, onWillUnmount, useComponent } = owl; /** @@ -13,7 +18,7 @@ export function useErrorHandlers() { component._handlePushOrderError = async function (error) { // This error handler receives `error` equivalent to `error.message` of the rpc error. if (error.message === "Backend Invoice") { - await this.showPopup("ConfirmPopup", { + await this.showPopup(ConfirmPopup, { title: this.env._t("Please print the invoice from the backend"), body: this.env._t( @@ -26,10 +31,10 @@ export function useErrorHandlers() { const body = this.env._t( "Check the internet connection then try to sync again by clicking on the red wifi button (upper right of the screen)." ); - await this.showPopup("OfflineErrorPopup", { title, body }); + await this.showPopup(OfflineErrorPopup, { title, body }); } else if (error.code === 200) { // OpenERP Server Errors - await this.showPopup("ErrorTracebackPopup", { + await this.showPopup(ErrorTracebackPopup, { title: error.data.message || this.env._t("Server Error"), body: error.data.debug || @@ -37,7 +42,7 @@ export function useErrorHandlers() { }); } else if (error.code === 700) { // Sweden Fiscal module errors - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Fiscal data module error"), body: error.data.error.status || @@ -53,13 +58,13 @@ export function useErrorHandlers() { } else { bodyMessage = "Fiscal data module is not on."; } - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Fiscal data module error"), body: bodyMessage, }); } else { // ??? - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("Unknown Error"), body: this.env._t( "The order could not be sent to the server due to an unknown error" diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index e685715283f..16b78c46ef1 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -18,7 +18,7 @@ import utils from "web.utils"; */ const CACHE = {}; -var PosDB = core.Class.extend({ +export const PosDB = core.Class.extend({ name: "openerp_pos_db", //the prefix of the localstorage data limit: 100, // the maximum number of results returned by a search init: function (options) { @@ -583,5 +583,3 @@ var PosDB = core.Class.extend({ this.save("unpaid_orders_to_remove", to_remove); }, }); - -export default PosDB; diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 857db75e6a7..377df34c6b4 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1,7 +1,7 @@ /** @odoo-module */ /* global waitForWebfonts */ -import PosDB from "@point_of_sale/js/db"; +import { PosDB } from "@point_of_sale/js/db"; import config from "web.config"; import core from "web.core"; import field_utils from "web.field_utils"; @@ -9,13 +9,13 @@ import time from "web.time"; import utils from "web.utils"; import { Gui } from "@point_of_sale/js/Gui"; import { batched, uuidv4 } from "@point_of_sale/js/utils"; +import { ErrorPopup } from "./Popups/ErrorPopup"; var QWeb = core.qweb; var _t = core._t; var round_di = utils.round_decimals; var round_pr = utils.round_precision; -import Registries from "@point_of_sale/js/Registries"; const { markRaw, reactive } = owl; // Container of the product images fetched during rendering @@ -60,7 +60,11 @@ class PosModel { * don't want this default calculation of cid. * @param {Object?} defaultObj its props copied to this instance. */ - constructor(defaultObj) { + constructor() { + this.setup(...arguments); + } + // To be used by Model patches to patch constructor + setup(defaultObj) { defaultObj = defaultObj || {}; if (!defaultObj.cid) { defaultObj.cid = this._getCID(defaultObj); @@ -84,8 +88,8 @@ class PosModel { } export class PosGlobalState extends PosModel { - constructor(obj) { - super(obj); + setup() { + super.setup(...arguments); this.db = new PosDB(); // a local database used to search trough products and categories & store pending orders this.debug = config.isDebug(); //debug mode @@ -240,7 +244,7 @@ export class PosGlobalState extends PosModel { productTemplateMap[product.product_tmpl_id[0]] = ( productTemplateMap[product.product_tmpl_id[0]] || [] ).concat(product); - return Product.create(product); + return new Product(product); }); for (const pricelist of this.pricelists) { @@ -405,7 +409,7 @@ export class PosGlobalState extends PosModel { if (json) { options.json = json; } - return this.makeOrderReactive(Order.create({}, options)); + return this.makeOrderReactive(new Order({}, options)); } makeOrderReactive(order) { const batchedCallback = batched(() => { @@ -1425,7 +1429,6 @@ export class PosGlobalState extends PosModel { } } PosGlobalState.prototype.electronic_payment_interfaces = {}; -Registries.Model.add(PosGlobalState); /** * Call this function to map your PaymentInterface implementation to @@ -1497,7 +1500,7 @@ export class Product extends PosModel { var pricelist_items = []; if (pricelist) { - var pricelist_items = _.filter( + pricelist_items = _.filter( self.applicablePricelistItems[pricelist.id], function (item) { return ( @@ -1578,7 +1581,6 @@ export class Product extends PosModel { } } } -Registries.Model.add(Product); var orderline_id = 1; @@ -1586,8 +1588,8 @@ var orderline_id = 1; // An orderline contains a product, its quantity, its price, discount. etc. // An Order contains zero or more Orderlines. export class Orderline extends PosModel { - constructor(obj, options) { - super(obj); + setup(_defaultObj, options) { + super.setup(...arguments); this.pos = options.pos; this.order = options.order; this.price_manually_set = options.price_manually_set || false; @@ -1637,7 +1639,7 @@ export class Orderline extends PosModel { var pack_lot_lines = json.pack_lot_ids; for (var i = 0; i < pack_lot_lines.length; i++) { var packlotline = pack_lot_lines[i][2]; - var pack_lot_line = Packlotline.create( + var pack_lot_line = new Packlotline( {}, { json: _.extend({...packlotline}, { order_line: this }) } ); @@ -1649,7 +1651,7 @@ export class Orderline extends PosModel { this.refunded_orderline_id = json.refunded_orderline_id; } clone() { - var orderline = Orderline.create( + var orderline = new Orderline( {}, { pos: this.pos, @@ -1712,7 +1714,7 @@ export class Orderline extends PosModel { // Create new pack lot lines. let newPackLotLine; for (const newLotLine of newPackLotLines) { - newPackLotLine = Packlotline.create({}, { order_line: this }); + newPackLotLine = new Packlotline({}, { order_line: this }); newPackLotLine.lot_name = newLotLine.lot_name; this.pack_lot_lines.add(newPackLotLine); } @@ -1779,7 +1781,7 @@ export class Orderline extends PosModel { const maxQtyToRefund = toRefundDetail.orderline.qty - toRefundDetail.orderline.refundedQty; if (quant > 0) { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Positive quantity not allowed"), body: _t( "Only a negative quantity is allowed for this refund line. Click on +/- to modify the quantity to be refunded." @@ -1791,7 +1793,7 @@ export class Orderline extends PosModel { } else if (-quant <= maxQtyToRefund) { toRefundDetail.qty = -quant; } else { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Greater than allowed"), body: _.str.sprintf( _t( @@ -2308,11 +2310,10 @@ export class Orderline extends PosModel { return this.product.standard_price * this.quantity; } } -Registries.Model.add(Orderline); export class Packlotline extends PosModel { - constructor(obj, options) { - super(obj); + setup(_defaultObj, options) { + super.setup(...arguments); this.lot_name = null; this.order_line = options.order_line; if (options.json) { @@ -2340,12 +2341,11 @@ export class Packlotline extends PosModel { }; } } -Registries.Model.add(Packlotline); // Every Paymentline contains a cashregister and an amount of money. export class Payment extends PosModel { - constructor(obj, options) { - super(obj); + setup(obj, options) { + super.setup(...arguments); this.pos = options.pos; this.order = options.order; this.amount = 0; @@ -2475,15 +2475,14 @@ export class Payment extends PosModel { return Boolean(this.get_payment_status()); } } -Registries.Model.add(Payment); // An order more or less represents the content of a customer's shopping cart (the OrderLines) // plus the associated payment information (the Paymentlines) // there is always an active ('selected') order in the Pos, a new one is created // automaticaly once an order is completed and sent to the server. export class Order extends PosModel { - constructor(obj, options) { - super(obj); + setup(_defaultObj, options) { + super.setup(...arguments); var self = this; options = options || {}; @@ -2606,7 +2605,7 @@ export class Order extends PosModel { var orderline = orderlines[i][2]; if (this.pos.db.get_product_by_id(orderline.product_id)) { this.add_orderline( - Orderline.create({}, { pos: this.pos, order: this, json: orderline }) + new Orderline({}, { pos: this.pos, order: this, json: orderline }) ); } } @@ -2614,7 +2613,7 @@ export class Order extends PosModel { var paymentlines = json.statement_ids; for (i = 0; i < paymentlines.length; i++) { var paymentline = paymentlines[i][2]; - var newpaymentline = Payment.create( + var newpaymentline = new Payment( {}, { pos: this.pos, order: this, json: paymentline } ); @@ -2990,7 +2989,7 @@ export class Order extends PosModel { } this.assert_editable(); options = options || {}; - var line = Orderline.create({}, { pos: this.pos, order: this, product: product }); + var line = new Orderline({}, { pos: this.pos, order: this, product: product }); this.fix_tax_included_price(line); this.set_orderline_options(line, options); @@ -3097,7 +3096,7 @@ export class Order extends PosModel { if (this.electronic_payment_in_progress()) { return false; } else { - var newPaymentline = Payment.create( + var newPaymentline = new Payment( {}, { order: this, payment_method: payment_method, pos: this.pos } ); @@ -3617,4 +3616,3 @@ export class Order extends PosModel { } } } -Registries.Model.add(Order); diff --git a/addons/point_of_sale/static/src/js/payment.js b/addons/point_of_sale/static/src/js/payment.js index 8e5724184f7..7c4bee6da3d 100644 --- a/addons/point_of_sale/static/src/js/payment.js +++ b/addons/point_of_sale/static/src/js/payment.js @@ -5,7 +5,7 @@ import core from "web.core"; /** * Implement this interface to support a new payment method in the POS: * - import var PaymentInterface from "@point_of_sale/js/payment"; + import const PaymentInterface from "@point_of_sale/js/payment"; * var MyPayment = PaymentInterface.extend({ * ... * }) @@ -22,7 +22,7 @@ import core from "web.core"; * by overriding the loader_params of the models in the back end * in the `pos.session` model */ -var PaymentInterface = core.Class.extend({ +export const PaymentInterface = core.Class.extend({ init: function (pos, payment_method) { this.pos = pos; this.payment_method = payment_method; @@ -89,5 +89,3 @@ var PaymentInterface = core.Class.extend({ */ close: function () {}, }); - -export default PaymentInterface; diff --git a/addons/point_of_sale/static/src/js/pos_env.js b/addons/point_of_sale/static/src/js/pos_env.js index da033984cf4..87a3e34ea11 100644 --- a/addons/point_of_sale/static/src/js/pos_env.js +++ b/addons/point_of_sale/static/src/js/pos_env.js @@ -6,16 +6,14 @@ import env from "web.env"; import concurrency from "web.concurrency"; import { JobQueue, ProxyDevice } from "@point_of_sale/js/devices"; -import BarcodeReader from "@point_of_sale/js/barcode_reader"; +import { BarcodeReader } from "@point_of_sale/js/barcode_reader"; // Create new env object base on web.env. // Mutating this new object won't affect the original object. -const pos_env = Object.create(env); +export const pos_env = Object.create(env); pos_env.proxy_queue = new JobQueue(); // used to prevent parallels communications to the proxy pos_env.proxy = new ProxyDevice({ env: pos_env }); // used to communicate to the hardware devices via a local proxy pos_env.barcode_reader = new BarcodeReader({ env: pos_env, proxy: pos_env.proxy }); pos_env.posbus = new owl.EventBus(); pos_env.posMutex = new concurrency.Mutex(); - -export default pos_env; diff --git a/addons/point_of_sale/static/src/js/printers.js b/addons/point_of_sale/static/src/js/printers.js index 7eb0eb6ff69..cc1ae86fddf 100644 --- a/addons/point_of_sale/static/src/js/printers.js +++ b/addons/point_of_sale/static/src/js/printers.js @@ -4,6 +4,7 @@ import Session from "web.Session"; import core from "web.core"; import { Gui } from "@point_of_sale/js/Gui"; +import { ErrorPopup } from "./Popups/ErrorPopup"; var _t = core._t; // IMPROVEMENT: This is too much. We can get away from this class. @@ -114,7 +115,7 @@ export const PrinterMixin = { _onIoTActionResult: function (data) { if (this.pos && (data === false || data.result === false)) { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Connection to the printer failed"), body: _t("Please check if the printer is still connected."), }); @@ -123,7 +124,7 @@ export const PrinterMixin = { _onIoTActionFail: function () { if (this.pos) { - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: _t("Connection to IoT Box failed"), body: _t("Please check if the IoT Box is still connected."), }); diff --git a/addons/point_of_sale/static/src/xml/Popups/PosPopupController.xml b/addons/point_of_sale/static/src/xml/Popups/PosPopupController.xml index 78392b7ef8f..acd752853ff 100644 --- a/addons/point_of_sale/static/src/xml/Popups/PosPopupController.xml +++ b/addons/point_of_sale/static/src/xml/Popups/PosPopupController.xml @@ -3,9 +3,9 @@
- +
diff --git a/addons/point_of_sale/static/tests/unit/helpers/test_env.js b/addons/point_of_sale/static/tests/unit/helpers/test_env.js index 58520aa13f2..da172119eaa 100644 --- a/addons/point_of_sale/static/tests/unit/helpers/test_env.js +++ b/addons/point_of_sale/static/tests/unit/helpers/test_env.js @@ -8,7 +8,7 @@ */ import makeTestEnvironment from "web.test_env"; -import env from "@point_of_sale/js/pos_env"; +import { pos_env as env} from "@point_of_sale/js/pos_env"; import { PosGlobalState } from "@point_of_sale/js/models"; // eslint-disable-next-line no-undef @@ -18,9 +18,9 @@ const cleanup = require("@web/../tests/helpers/cleanup"); // sensitive to load order. STOP DOING THIS ASAP. cleanup.registerCleanup = () => {}; -const makePosTestEnv = env.session.is_bound +export const makePosTestEnv = env.session.is_bound .then(() => { - const pos = PosGlobalState.create({ env }); + const pos = new PosGlobalState({ env }); return pos.load_server_data().then(() => pos); }) .then((pos) => { @@ -34,5 +34,3 @@ const makePosTestEnv = env.session.is_bound return makeTestEnvironment(env, providedRPC); }; }); - -export default makePosTestEnv; diff --git a/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js b/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js deleted file mode 100644 index 33029faceee..00000000000 --- a/addons/point_of_sale/static/tests/unit/test_ComponentRegistry.js +++ /dev/null @@ -1,412 +0,0 @@ -/** @odoo-module */ - -import Registries from "@point_of_sale/js/Registries"; - -QUnit.module("unit tests for ComponentRegistry", { - before() {}, -}); - -QUnit.test("basic extend", async function (assert) { - assert.expect(5); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - Registries.Component.freeze(); - - const RegA = Registries.Component.get(A); - const a = new RegA(); - assert.verifySteps(["A", "A1"]); - assert.ok(a instanceof RegA); - assert.ok(RegA.name === "A"); -}); - -QUnit.test("addByExtending", async function (assert) { - assert.expect(8); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const B = (x) => - class extends x { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - const A2 = (x) => - class extends x { - constructor() { - super(); - assert.step("A2"); - } - }; - Registries.Component.extend(A, A2); - - Registries.Component.freeze(); - - const RegA = Registries.Component.get(A); - const RegB = Registries.Component.get(B); - const b = new RegB(); - assert.verifySteps(["A", "A1", "A2", "B"]); - assert.ok(b instanceof RegA); - assert.ok(b instanceof RegB); - assert.ok(RegB.name === "B"); -}); - -QUnit.test("extend the one that is added by extending", async function (assert) { - assert.expect(6); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const B = (x) => - class extends x { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const B1 = (x) => - class extends x { - constructor() { - super(); - assert.step("B1"); - } - }; - Registries.Component.extend(B, B1); - - const B2 = (x) => - class extends x { - constructor() { - super(); - assert.step("B2"); - } - }; - Registries.Component.extend(B, B2); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - Registries.Component.freeze(); - - const RegB = Registries.Component.get(B); - new RegB(); - assert.verifySteps(["A", "A1", "B", "B1", "B2"]); -}); - -QUnit.test("addByExtending based on added by extending", async function (assert) { - assert.expect(10); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const B = (x) => - class extends x { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - const C = (x) => - class extends x { - constructor() { - super(); - assert.step("C"); - } - }; - Registries.Component.addByExtending(C, B); - - const B7 = (x) => - class extends x { - constructor() { - super(); - assert.step("B7"); - } - }; - Registries.Component.extend(B, B7); - - Registries.Component.freeze(); - - const RegA = Registries.Component.get(A); - const RegB = Registries.Component.get(B); - const RegC = Registries.Component.get(C); - const c = new RegC(); - assert.verifySteps(["A", "A1", "B", "B7", "C"]); - assert.ok(c instanceof RegA); - assert.ok(c instanceof RegB); - assert.ok(c instanceof RegC); - assert.ok(RegC.name === "C"); -}); - -QUnit.test("deeper inheritance", async function (assert) { - assert.expect(9); - - class A { - constructor() { - assert.step("A"); - } - } - Registries.Component.add(A); - - const B = (x) => - class extends x { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - const C = (x) => - class extends x { - constructor() { - super(); - assert.step("C"); - } - }; - Registries.Component.addByExtending(C, B); - - const B2 = (x) => - class extends x { - constructor() { - super(); - assert.step("B2"); - } - }; - Registries.Component.extend(B, B2); - - const B3 = (x) => - class extends x { - constructor() { - super(); - assert.step("B3"); - } - }; - Registries.Component.extend(B, B3); - - const A9 = (x) => - class extends x { - constructor() { - super(); - assert.step("A9"); - } - }; - Registries.Component.extend(A, A9); - - const E = (x) => - class extends x { - constructor() { - super(); - assert.step("E"); - } - }; - Registries.Component.addByExtending(E, C); - - Registries.Component.freeze(); - - // |A| => A9 -> A1 -> A - // |B| => B3 -> B2 -> B -> |A| - // |C| => C -> |B| - // |E| => E -> |C| - - new (Registries.Component.get(E))(); - assert.verifySteps(["A", "A1", "A9", "B", "B2", "B3", "C", "E"]); -}); - -QUnit.test("mixins?", async function (assert) { - assert.expect(12); - - let A = class A { - constructor() { - assert.step("A"); - } - }; - Registries.Component.add(A); - - const Mixin = (x) => - class extends x { - constructor() { - super(); - assert.step("Mixin"); - } - mixinMethod() { - return "mixinMethod"; - } - get mixinGetter() { - return "mixinGetter"; - } - }; - - // use the mixin when declaring B. - let B = (x) => - class extends Mixin(x) { - constructor() { - super(); - assert.step("B"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - constructor() { - super(); - assert.step("A1"); - } - }; - Registries.Component.extend(A, A1); - - Registries.Component.freeze(); - - B = Registries.Component.get(B); - const b = new B(); - assert.verifySteps(["A", "A1", "Mixin", "B"]); - // instance of B should have the mixin properties - assert.strictEqual(b.mixinMethod(), "mixinMethod"); - assert.strictEqual(b.mixinGetter, "mixinGetter"); - - // instance of A should not have the mixin properties - A = Registries.Component.get(A); - const a = new A(); - assert.verifySteps(["A", "A1"]); - assert.notOk(a.mixinMethod); - assert.notOk(a.mixinGetter); -}); - -QUnit.test("extending methods", async function (assert) { - assert.expect(16); - - let A = class A { - foo() { - assert.step("A foo"); - } - }; - Registries.Component.add(A); - - let B = (x) => - class extends x { - bar() { - assert.step("B bar"); - } - }; - Registries.Component.addByExtending(B, A); - - const A1 = (x) => - class extends x { - bar() { - assert.step("A1 bar"); - // should only be for A. - } - }; - Registries.Component.extend(A, A1); - - const B1 = (x) => - class extends x { - foo() { - super.foo(); - assert.step("B1 foo"); - } - }; - Registries.Component.extend(B, B1); - - let C = (x) => - class extends x { - foo() { - super.foo(); - assert.step("C foo"); - } - bar() { - super.bar(); - assert.step("C bar"); - } - }; - Registries.Component.addByExtending(C, B); - - Registries.Component.freeze(); - - A = Registries.Component.get(A); - B = Registries.Component.get(B); - C = Registries.Component.get(C); - const a = new A(); - const b = new B(); - const c = new C(); - - a.foo(); - assert.verifySteps(["A foo"]); - b.foo(); - assert.verifySteps(["A foo", "B1 foo"]); - c.foo(); - assert.verifySteps(["A foo", "B1 foo", "C foo"]); - - a.bar(); - assert.verifySteps(["A1 bar"]); - b.bar(); - assert.verifySteps(["B bar"]); - c.bar(); - assert.verifySteps(["B bar", "C bar"]); -}); diff --git a/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js b/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js index 07a2957b64b..7b137f2c019 100644 --- a/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js +++ b/addons/point_of_sale/static/tests/unit/test_NumberBuffer.js @@ -1,6 +1,6 @@ /** @odoo-module */ -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; import makeTestEnvironment from "web.test_env"; import testUtils from "web.test_utils"; import { mount } from "@web/../tests/helpers/utils"; @@ -20,15 +20,15 @@ QUnit.test("simple fast inputs with capture in between", async function (assert) class Root extends LegacyComponent { setup() { this.state = useState({ buffer: "" }); - NumberBuffer.activate(); - NumberBuffer.use({ + numberBuffer.activate(); + numberBuffer.use({ nonKeyboardInputEvent: "numpad-click-input", state: this.state, }); } resetBuffer() { - NumberBuffer.capture(); - NumberBuffer.reset(); + numberBuffer.capture(); + numberBuffer.reset(); } onClickOne() { this.trigger("numpad-click-input", { key: "1" }); diff --git a/addons/point_of_sale/static/tests/unit/test_PosPopupController.js b/addons/point_of_sale/static/tests/unit/test_PosPopupController.js index 9b9a34f069a..18ac4601d23 100644 --- a/addons/point_of_sale/static/tests/unit/test_PosPopupController.js +++ b/addons/point_of_sale/static/tests/unit/test_PosPopupController.js @@ -1,53 +1,47 @@ /** @odoo-module */ -import PosPopupController from "@point_of_sale/js/Popups/PosPopupController"; -import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; -import PosComponent from "@point_of_sale/js/PosComponent"; +import { PosPopupController } from "@point_of_sale/js/Popups/PosPopupController"; +import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; import makeTestEnvironment from "web.test_env"; import testUtils from "web.test_utils"; -import Registries from "@point_of_sale/js/Registries"; import { mount } from "@web/../tests/helpers/utils"; const { EventBus, useSubEnv, xml } = owl; -QUnit.module("unit tests for PosPopupController", { - before() { - Registries.Component.freeze(); +// Note that we are creating new popups here to decouple this test from the pos app. +class CustomPopup1 extends AbstractAwaitablePopup {} +CustomPopup1.template = xml/* html */ ` + +`; - // Note that we are creating new popups here to decouple this test from the pos app. - class CustomPopup1 extends AbstractAwaitablePopup {} - CustomPopup1.template = xml/* html */ ` - - `; +class CustomPopup2 extends AbstractAwaitablePopup {} +CustomPopup2.template = xml/* html */ ` + +`; - class CustomPopup2 extends AbstractAwaitablePopup {} - CustomPopup2.template = xml/* html */ ` - - `; - - PosPopupController.components = { CustomPopup1, CustomPopup2 }; - }, -}); +QUnit.module("unit tests for PosPopupController"); QUnit.test("allow multiple popups at the same time", async function (assert) { assert.expect(12); class Root extends PosComponent { + static components = { PosPopupController }; setup() { super.setup(); useSubEnv({ @@ -66,7 +60,7 @@ QUnit.test("allow multiple popups at the same time", async function (assert) { const root = await mount(Root, testUtils.prepareTarget()); // Check 1 popup - let popup1Promise = root.showPopup("CustomPopup1", {}); + let popup1Promise = root.showPopup(CustomPopup1, {}); await testUtils.nextTick(); assert.strictEqual(root.el.querySelectorAll(".popup").length, 1); testUtils.dom.click(root.el.querySelector(".modal-dialog .custom-popup-1 .confirm")); @@ -76,13 +70,13 @@ QUnit.test("allow multiple popups at the same time", async function (assert) { assert.strictEqual(root.el.querySelectorAll(".popup").length, 0); // Check multiple popups - popup1Promise = root.showPopup("CustomPopup1", {}); + popup1Promise = root.showPopup(CustomPopup1, {}); await testUtils.nextTick(); // Check if the first popup is shown. assert.strictEqual(root.el.querySelectorAll(".popup").length, 1); - const popup2Promise = root.showPopup("CustomPopup2", {}); + const popup2Promise = root.showPopup(CustomPopup2, {}); await testUtils.nextTick(); // Check for the second popup. @@ -100,8 +94,8 @@ QUnit.test("allow multiple popups at the same time", async function (assert) { assert.strictEqual(root.el.querySelectorAll(".modal-dialog .custom-popup-2").length, 0); // popup 1 should not be hidden - const CustomPopup1 = root.el.querySelector(".modal-dialog"); - assert.strictEqual(![...CustomPopup1.classList].includes("oe_hidden"), true); + const customPopup1 = root.el.querySelector(".modal-dialog"); + assert.strictEqual(![...customPopup1.classList].includes("oe_hidden"), true); testUtils.dom.click(root.el.querySelector(".modal-dialog .custom-popup-1 .cancel")); await testUtils.nextTick(); @@ -118,6 +112,7 @@ QUnit.test("pressing cancel/confirm key should only close the top popup", async assert.expect(6); class Root extends PosComponent { + static components = { PosPopupController }; setup() { super.setup(); useSubEnv({ @@ -135,14 +130,14 @@ QUnit.test("pressing cancel/confirm key should only close the top popup", async const root = await mount(Root, testUtils.prepareTarget()); - const popup1Promise = root.showPopup("CustomPopup1", { + const popup1Promise = root.showPopup(CustomPopup1, { confirmKey: "Enter", cancelKey: "Escape", }); await testUtils.nextTick(); assert.strictEqual(root.el.querySelectorAll(".popup").length, 1); - const popup2Promise = root.showPopup("CustomPopup2", { + const popup2Promise = root.showPopup(CustomPopup2, { confirmKey: "Enter", cancelKey: "Escape", }); diff --git a/addons/pos_adyen/static/src/js/PaymentScreen.js b/addons/pos_adyen/static/src/js/PaymentScreen.js index b9bd2beed20..77bdd0ddb93 100644 --- a/addons/pos_adyen/static/src/js/PaymentScreen.js +++ b/addons/pos_adyen/static/src/js/PaymentScreen.js @@ -1,39 +1,34 @@ /** @odoo-module */ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; const { onMounted } = owl; -const PosAdyenPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - setup() { - super.setup(); - onMounted(() => { - const pendingPaymentLine = this.currentOrder.paymentlines.find( - (paymentLine) => - paymentLine.payment_method.use_payment_terminal === "adyen" && - !paymentLine.is_done() && - paymentLine.get_payment_status() !== "pending" +patch(PaymentScreen.prototype, "pos_adyen.PaymentScreen", { + setup() { + this._super(...arguments); + onMounted(() => { + const pendingPaymentLine = this.currentOrder.paymentlines.find( + (paymentLine) => + paymentLine.payment_method.use_payment_terminal === "adyen" && + !paymentLine.is_done() && + paymentLine.get_payment_status() !== "pending" + ); + if (pendingPaymentLine) { + const paymentTerminal = pendingPaymentLine.payment_method.payment_terminal; + paymentTerminal.set_most_recent_service_id( + pendingPaymentLine.terminalServiceId ); - if (pendingPaymentLine) { - const paymentTerminal = pendingPaymentLine.payment_method.payment_terminal; - paymentTerminal.set_most_recent_service_id( - pendingPaymentLine.terminalServiceId - ); - pendingPaymentLine.set_payment_status("waiting"); - paymentTerminal.start_get_status_polling().then((isPaymentSuccessful) => { - if (isPaymentSuccessful) { - pendingPaymentLine.set_payment_status("done"); - pendingPaymentLine.can_be_reversed = paymentTerminal.supports_reversals; - } else { - pendingPaymentLine.set_payment_status("retry"); - } - }); - } - }); - } - }; - -Registries.Component.extend(PaymentScreen, PosAdyenPaymentScreen); - -export default PaymentScreen; + pendingPaymentLine.set_payment_status("waiting"); + paymentTerminal.start_get_status_polling().then((isPaymentSuccessful) => { + if (isPaymentSuccessful) { + pendingPaymentLine.set_payment_status("done"); + pendingPaymentLine.can_be_reversed = paymentTerminal.supports_reversals; + } else { + pendingPaymentLine.set_payment_status("retry"); + } + }); + } + }); + }, +}); diff --git a/addons/pos_adyen/static/src/js/models.js b/addons/pos_adyen/static/src/js/models.js index 002a556af9c..f625742d560 100644 --- a/addons/pos_adyen/static/src/js/models.js +++ b/addons/pos_adyen/static/src/js/models.js @@ -1,29 +1,27 @@ /** @odoo-module */ import { register_payment_method, Payment } from "@point_of_sale/js/models"; -import PaymentAdyen from "@pos_adyen/js/payment_adyen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentAdyen } from "@pos_adyen/js/payment_adyen"; +import { patch } from "@web/core/utils/patch"; register_payment_method("adyen", PaymentAdyen); -const PosAdyenPayment = (Payment) => - class PosAdyenPayment extends Payment { - constructor(obj, options) { - super(...arguments); - this.terminalServiceId = this.terminalServiceId || null; - } - //@override - export_as_JSON() { - const json = super.export_as_JSON(...arguments); - json.terminal_service_id = this.terminalServiceId; - return json; - } - //@override - init_from_JSON(json) { - super.init_from_JSON(...arguments); - this.terminalServiceId = json.terminal_service_id; - } - setTerminalServiceId(id) { - this.terminalServiceId = id; - } - }; -Registries.Model.extend(Payment, PosAdyenPayment); +patch(Payment.prototype, "pos_adyen.Payment", { + setup() { + this._super(...arguments); + this.terminalServiceId = this.terminalServiceId || null; + }, + //@override + export_as_JSON() { + const json = this._super(...arguments); + json.terminal_service_id = this.terminalServiceId; + return json; + }, + //@override + init_from_JSON(json) { + this._super(...arguments); + this.terminalServiceId = json.terminal_service_id; + }, + setTerminalServiceId(id) { + this.terminalServiceId = id; + }, +}); diff --git a/addons/pos_adyen/static/src/js/payment_adyen.js b/addons/pos_adyen/static/src/js/payment_adyen.js index 2ff8fe53cbc..60f01d40691 100644 --- a/addons/pos_adyen/static/src/js/payment_adyen.js +++ b/addons/pos_adyen/static/src/js/payment_adyen.js @@ -2,12 +2,13 @@ import core from "web.core"; import rpc from "web.rpc"; -import PaymentInterface from "@point_of_sale/js/payment"; +import { PaymentInterface } from "@point_of_sale/js/payment"; import { Gui } from "@point_of_sale/js/Gui"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; var _t = core._t; -var PaymentAdyen = PaymentInterface.extend({ +export const PaymentAdyen = PaymentInterface.extend({ send_payment_request: function (cid) { this._super.apply(this, arguments); this._reset_state(); @@ -365,11 +366,9 @@ var PaymentAdyen = PaymentInterface.extend({ if (!title) { title = _t("Adyen Error"); } - Gui.showPopup("ErrorPopup", { + Gui.showPopup(ErrorPopup, { title: title, body: msg, }); }, }); - -export default PaymentAdyen; diff --git a/addons/pos_discount/static/src/js/DiscountButton.js b/addons/pos_discount/static/src/js/DiscountButton.js index 6ed6aac3abd..ae8859bc6dd 100644 --- a/addons/pos_discount/static/src/js/DiscountButton.js +++ b/addons/pos_discount/static/src/js/DiscountButton.js @@ -1,18 +1,21 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; -import Registries from "@point_of_sale/js/Registries"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; + +export class DiscountButton extends PosComponent { + static template = "DiscountButton"; -class DiscountButton extends PosComponent { setup() { super.setup(); useListener("click", this.onClick); } async onClick() { var self = this; - const { confirmed, payload } = await this.showPopup("NumberPopup", { + const { confirmed, payload } = await this.showPopup(NumberPopup, { title: this.env._t("Discount Percentage"), startingValue: this.env.pos.config.discount_pc, isInputSelected: true, @@ -28,7 +31,7 @@ class DiscountButton extends PosComponent { var lines = order.get_orderlines(); var product = this.env.pos.db.get_product_by_id(this.env.pos.config.discount_product_id[0]); if (product === undefined) { - await this.showPopup("ErrorPopup", { + await this.showPopup(ErrorPopup, { title: this.env._t("No discount product found"), body: this.env._t( "The discount product seems misconfigured. Make sure it is flagged as 'Can be Sold' and 'Available in Point of Sale'." @@ -80,7 +83,6 @@ class DiscountButton extends PosComponent { } } } -DiscountButton.template = "DiscountButton"; ProductScreen.addControlButton({ component: DiscountButton, @@ -88,7 +90,3 @@ ProductScreen.addControlButton({ return this.env.pos.config.module_pos_discount && this.env.pos.config.discount_product_id; }, }); - -Registries.Component.add(DiscountButton); - -export default DiscountButton; diff --git a/addons/pos_epson_printer/static/src/js/pos_epson_printer.js b/addons/pos_epson_printer/static/src/js/pos_epson_printer.js index 49a3ff2ad9f..aa916a9e3d4 100644 --- a/addons/pos_epson_printer/static/src/js/pos_epson_printer.js +++ b/addons/pos_epson_printer/static/src/js/pos_epson_printer.js @@ -1,18 +1,16 @@ /** @odoo-module */ import { PosGlobalState } from "@point_of_sale/js/models"; -import EpsonPrinter from "@pos_epson_printer/js/printers"; -import Registries from "@point_of_sale/js/Registries"; +import { EpsonPrinter } from "@pos_epson_printer/js/printers"; +import { patch } from "@web/core/utils/patch"; -const PosEpsonPosGlobalState = (PosGlobalState) => - class PosEpsonPosGlobalState extends PosGlobalState { - after_load_server_data() { - var self = this; - return super.after_load_server_data(...arguments).then(function () { - if (self.config.other_devices && self.config.epson_printer_ip) { - self.env.proxy.printer = new EpsonPrinter(self.config.epson_printer_ip, self); - } - }); - } - }; -Registries.Model.extend(PosGlobalState, PosEpsonPosGlobalState); +patch(PosGlobalState.prototype, "pos_epson_printer.PosGlobalState", { + after_load_server_data() { + var self = this; + return this._super(...arguments).then(function () { + if (self.config.other_devices && self.config.epson_printer_ip) { + self.env.proxy.printer = new EpsonPrinter(self.config.epson_printer_ip, self); + } + }); + }, +}); diff --git a/addons/pos_epson_printer/static/src/js/printers.js b/addons/pos_epson_printer/static/src/js/printers.js index b6da88b5982..43c86186874 100644 --- a/addons/pos_epson_printer/static/src/js/printers.js +++ b/addons/pos_epson_printer/static/src/js/printers.js @@ -49,7 +49,7 @@ class EpsonPrintResultGenerator extends PrintResultGenerator { } } -var EpsonPrinter = core.Class.extend(PrinterMixin, { +export const EpsonPrinter = core.Class.extend(PrinterMixin, { init(ip, pos) { PrinterMixin.init.call(this, pos); var url = window.location.protocol + "//" + ip; @@ -167,5 +167,3 @@ var EpsonPrinter = core.Class.extend(PrinterMixin, { return $(res).find("response").attr("success") === "true"; }, }); - -export default EpsonPrinter; diff --git a/addons/pos_epson_printer_restaurant/static/src/js/models.js b/addons/pos_epson_printer_restaurant/static/src/js/models.js index 911e03ddfdd..8e52d4ebdf2 100644 --- a/addons/pos_epson_printer_restaurant/static/src/js/models.js +++ b/addons/pos_epson_printer_restaurant/static/src/js/models.js @@ -1,22 +1,20 @@ /** @odoo-module */ import { PosGlobalState } from "@point_of_sale/js/models"; -import EpsonPrinter from "@pos_epson_printer/js/printers"; -import Registries from "@point_of_sale/js/Registries"; +import { EpsonPrinter } from "@pos_epson_printer/js/printers"; +import { patch } from "@web/core/utils/patch"; // The override of create_printer needs to happen after its declaration in // pos_restaurant. We need to make sure that this code is executed after the // models file in pos_restaurant. import "@pos_restaurant/js/models"; -const PosEpsonResPosGlobalState = (PosGlobalState) => - class PosEpsonResPosGlobalState extends PosGlobalState { - create_printer(config) { - if (config.printer_type === "epson_epos") { - return new EpsonPrinter(config.epson_printer_ip, this); - } else { - return super.create_printer(...arguments); - } +patch(PosGlobalState.prototype, "pos_epson_printer_restaurant.PosGlobalState", { + create_printer(config) { + if (config.printer_type === "epson_epos") { + return new EpsonPrinter(config.epson_printer_ip, this); + } else { + return this._super(...arguments); } - }; -Registries.Model.extend(PosGlobalState, PosEpsonResPosGlobalState); + }, +}); diff --git a/addons/pos_hr/static/src/app/navbar.js b/addons/pos_hr/static/src/app/navbar.js new file mode 100644 index 00000000000..045c5ce7535 --- /dev/null +++ b/addons/pos_hr/static/src/app/navbar.js @@ -0,0 +1,9 @@ +/** @odoo-module */ + +import { Navbar } from "@point_of_sale/app/navbar/navbar"; +import { HeaderLockButton } from "../js/HeaderLockButton"; +import { patch } from "@web/core/utils/patch"; + +patch(Navbar, "pos_hr.Navbar", { + components: { ...Navbar.components, HeaderLockButton }, +}); diff --git a/addons/pos_hr/static/src/js/CashierName.js b/addons/pos_hr/static/src/js/CashierName.js index 4b81d0b30a2..aa3a3f335b9 100644 --- a/addons/pos_hr/static/src/js/CashierName.js +++ b/addons/pos_hr/static/src/js/CashierName.js @@ -1,33 +1,29 @@ /** @odoo-module */ -import CashierName from "@point_of_sale/js/ChromeWidgets/CashierName"; -import Registries from "@point_of_sale/js/Registries"; -import SelectCashierMixin from "@pos_hr/js/SelectCashierMixin"; +import { CashierName } from "@point_of_sale/js/ChromeWidgets/CashierName"; +import { patch } from "@web/core/utils/patch"; +import { SelectCashierMixin } from "@pos_hr/js/SelectCashierMixin"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; -const PosHrCashierName = (CashierName) => - class extends SelectCashierMixin(CashierName) { - setup() { - super.setup(); - useBarcodeReader({ cashier: this.barcodeCashierAction }); +patch(CashierName.prototype, "pos_hr.CashierName", { + ...SelectCashierMixin, + setup() { + this._super(...arguments); + useBarcodeReader({ cashier: this.barcodeCashierAction }); + }, + //@Override + get avatar() { + if (this.env.pos.config.module_pos_hr) { + const cashier = this.env.pos.get_cashier(); + return `/web/image/hr.employee/${cashier.id}/avatar_128`; } - //@Override - get avatar() { - if (this.env.pos.config.module_pos_hr) { - const cashier = this.env.pos.get_cashier(); - return `/web/image/hr.employee/${cashier.id}/avatar_128`; - } - return super.avatar; + return this._super(); + }, + //@Override + get cssClass() { + if (this.env.pos.config.module_pos_hr) { + return { oe_status: true }; } - //@Override - get cssClass() { - if (this.env.pos.config.module_pos_hr) { - return { oe_status: true }; - } - return super.cssClass; - } - }; - -Registries.Component.extend(CashierName, PosHrCashierName); - -export default CashierName; + return this._super(); + }, +}); diff --git a/addons/pos_hr/static/src/js/Chrome.js b/addons/pos_hr/static/src/js/Chrome.js index 2fc07716279..05cc98c9d4d 100644 --- a/addons/pos_hr/static/src/js/Chrome.js +++ b/addons/pos_hr/static/src/js/Chrome.js @@ -1,30 +1,25 @@ /** @odoo-module */ -import Chrome from "@point_of_sale/js/Chrome"; -import Registries from "@point_of_sale/js/Registries"; +import { Chrome } from "@point_of_sale/js/Chrome"; +import { patch } from "@web/core/utils/patch"; -const PosHrChrome = (Chrome) => - class extends Chrome { - async start() { - await super.start(); - if (this.env.pos.config.module_pos_hr) { - this.showTempScreen("LoginScreen"); - } +patch(Chrome.prototype, "pos_hr.Chrome", { + async start() { + await this._super(...arguments); + if (this.env.pos.config.module_pos_hr) { + this.showTempScreen("LoginScreen"); } - get showCashMoveButton() { - return ( - super.showCashMoveButton && - (!this.env.pos.cashier || this.env.pos.cashier.role == "manager") - ); + }, + get showCashMoveButton() { + return ( + this._super() && + (!this.env.pos.cashier || this.env.pos.cashier.role == "manager") + ); + }, + shouldShowCashControl() { + if (this.env.pos.config.module_pos_hr) { + return this._super() && this.env.pos.hasLoggedIn; } - shouldShowCashControl() { - if (this.env.pos.config.module_pos_hr) { - return super.shouldShowCashControl() && this.env.pos.hasLoggedIn; - } - return super.shouldShowCashControl(); - } - }; - -Registries.Component.extend(Chrome, PosHrChrome); - -export default Chrome; + return this._super(); + }, +}); diff --git a/addons/pos_hr/static/src/js/HeaderLockButton.js b/addons/pos_hr/static/src/js/HeaderLockButton.js index 608dc155a72..3c50d19a5de 100644 --- a/addons/pos_hr/static/src/js/HeaderLockButton.js +++ b/addons/pos_hr/static/src/js/HeaderLockButton.js @@ -1,11 +1,12 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; const { useState } = owl; -class HeaderLockButton extends PosComponent { +export class HeaderLockButton extends PosComponent { + static template = "HeaderLockButton"; + setup() { super.setup(); this.state = useState({ isUnlockIcon: true, title: "Unlocked" }); @@ -19,8 +20,3 @@ class HeaderLockButton extends PosComponent { this.state.title = isMouseOver ? "Lock" : "Unlocked"; } } -HeaderLockButton.template = "HeaderLockButton"; - -Registries.Component.add(HeaderLockButton); - -export default HeaderLockButton; diff --git a/addons/pos_hr/static/src/js/LoginScreen.js b/addons/pos_hr/static/src/js/LoginScreen.js index 4dd9dda43b5..08f6e694a31 100644 --- a/addons/pos_hr/static/src/js/LoginScreen.js +++ b/addons/pos_hr/static/src/js/LoginScreen.js @@ -1,41 +1,44 @@ /** @odoo-module */ -import PosComponent from "@point_of_sale/js/PosComponent"; -import Registries from "@point_of_sale/js/Registries"; -import SelectCashierMixin from "@pos_hr/js/SelectCashierMixin"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { SelectCashierMixin } from "@pos_hr/js/SelectCashierMixin"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; +import { registry } from "@web/core/registry"; +import { patch } from "@web/core/utils/patch"; -class LoginScreen extends SelectCashierMixin(PosComponent) { +export class LoginScreen extends PosComponent { + static template = "LoginScreen"; +} +// FIXME stop this double patch once the mixin is converted to a hook. +patch(LoginScreen.prototype, "pos_hr.LoginScreen SelectCashierMixin", SelectCashierMixin); +patch(LoginScreen.prototype, "pos_hr.LoginScreen methods", { setup() { - super.setup(); + this._super(...arguments); useBarcodeReader({ cashier: this.barcodeCashierAction }, true); - } + }, async selectCashier() { - if (await super.selectCashier()) { + if (await this._super()) { this.back(); } - } + }, async barcodeCashierAction(code) { - if (await super.barcodeCashierAction(code)) { + if (await this._super(code)) { this.back(); } - } + }, back() { this.props.resolve({ confirmed: false, payload: false }); this.trigger("close-temp-screen"); this.env.pos.hasLoggedIn = true; this.env.posbus.trigger("start-cash-control"); - } + }, confirm() { this.props.resolve({ confirmed: true, payload: true }); this.trigger("close-temp-screen"); - } + }, get shopName() { return this.env.pos.config.name; - } -} -LoginScreen.template = "LoginScreen"; + }, +}); -Registries.Component.add(LoginScreen); - -export default LoginScreen; +registry.category("pos_screens").add("LoginScreen", LoginScreen); diff --git a/addons/pos_hr/static/src/js/PaymentScreen.js b/addons/pos_hr/static/src/js/PaymentScreen.js index ae458e96a88..c2da288386e 100644 --- a/addons/pos_hr/static/src/js/PaymentScreen.js +++ b/addons/pos_hr/static/src/js/PaymentScreen.js @@ -1,16 +1,11 @@ /** @odoo-module */ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; -const PosHrPaymentScreen = (PaymentScreen_) => - class extends PaymentScreen_ { - async _finalizeValidation() { - this.currentOrder.employee = this.env.pos.get_cashier(); - await super._finalizeValidation(); - } - }; - -Registries.Component.extend(PaymentScreen, PosHrPaymentScreen); - -export default PaymentScreen; +patch(PaymentScreen.prototype, "pos_hr.PaymentScreen", { + async _finalizeValidation() { + this.currentOrder.employee = this.env.pos.get_cashier(); + await this._super(); + }, +}); diff --git a/addons/pos_hr/static/src/js/SelectCashierMixin.js b/addons/pos_hr/static/src/js/SelectCashierMixin.js index 7deb6cef0ab..087b0012617 100644 --- a/addons/pos_hr/static/src/js/SelectCashierMixin.js +++ b/addons/pos_hr/static/src/js/SelectCashierMixin.js @@ -1,76 +1,76 @@ /** @odoo-module */ /* global Sha1 */ -const SelectCashierMixin = (PosComponent) => - class ComponentWithSelectCashierMixin extends PosComponent { - async askPin(employee) { - const { confirmed, payload: inputPin } = await this.showPopup("NumberPopup", { - isPassword: true, - title: this.env._t("Password ?"), - startingValue: null, +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; + +// FIXME POSREF make this into a hook or something +export const SelectCashierMixin = { + async askPin(employee) { + const { confirmed, payload: inputPin } = await this.showPopup(NumberPopup, { + isPassword: true, + title: this.env._t("Password ?"), + startingValue: null, + }); + + if (!confirmed) { + return; + } + + if (employee.pin === Sha1.hash(inputPin)) { + return employee; + } else { + await this.showPopup(ErrorPopup, { + title: this.env._t("Incorrect Password"), + }); + return; + } + }, + + /** + * Select a cashier, the returning value will either be an object or nothing (undefined) + */ + async selectCashier() { + if (this.env.pos.config.module_pos_hr) { + const employeesList = this.env.pos.employees + .filter((employee) => employee.id !== this.env.pos.get_cashier().id) + .map((employee) => { + return { + id: employee.id, + item: employee, + label: employee.name, + isSelected: false, + }; + }); + let { confirmed, payload: employee } = await this.showPopup(SelectionPopup, { + title: this.env._t("Change Cashier"), + list: employeesList, }); if (!confirmed) { return; } - if (employee.pin === Sha1.hash(inputPin)) { - return employee; - } else { - await this.showPopup("ErrorPopup", { - title: this.env._t("Incorrect Password"), - }); - return; + if (employee && employee.pin) { + employee = await this.askPin(employee); } - } - - /** - * Select a cashier, the returning value will either be an object or nothing (undefined) - */ - async selectCashier() { - if (this.env.pos.config.module_pos_hr) { - const employeesList = this.env.pos.employees - .filter((employee) => employee.id !== this.env.pos.get_cashier().id) - .map((employee) => { - return { - id: employee.id, - item: employee, - label: employee.name, - isSelected: false, - }; - }); - let { confirmed, payload: employee } = await this.showPopup("SelectionPopup", { - title: this.env._t("Change Cashier"), - list: employeesList, - }); - - if (!confirmed) { - return; - } - - if (employee && employee.pin) { - employee = await this.askPin(employee); - } - if (employee) { - this.env.pos.set_cashier(employee); - } - return employee; - } - } - - async barcodeCashierAction(code) { - const employee = this.env.pos.employees.find( - (emp) => emp.barcode === Sha1.hash(code.code) - ); - if ( - employee && - employee !== this.env.pos.get_cashier() && - (!employee.pin || (await this.askPin(employee))) - ) { + if (employee) { this.env.pos.set_cashier(employee); } return employee; } - }; + }, -export default SelectCashierMixin; + async barcodeCashierAction(code) { + const employee = this.env.pos.employees.find((emp) => emp.barcode === Sha1.hash(code.code)); + if ( + employee && + employee !== this.env.pos.get_cashier() && + (!employee.pin || (await this.askPin(employee))) + ) { + this.env.pos.set_cashier(employee); + } + return employee; + }, +}; diff --git a/addons/pos_hr/static/src/js/models.js b/addons/pos_hr/static/src/js/models.js index 9680bf4d976..7d5990fbd53 100644 --- a/addons/pos_hr/static/src/js/models.js +++ b/addons/pos_hr/static/src/js/models.js @@ -1,86 +1,81 @@ /** @odoo-module */ import { PosGlobalState, Order } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; +import { patch } from "@web/core/utils/patch"; -const PosHrPosGlobalState = (PosGlobalState) => - class PosHrPosGlobalState extends PosGlobalState { - async _processData(loadedData) { - await super._processData(...arguments); - if (this.config.module_pos_hr) { - this.employees = loadedData["hr.employee"]; - this.employee_by_id = loadedData["employee_by_id"]; - this.reset_cashier(); - } +patch(PosGlobalState.prototype, "pos_hr.PosGlobalState", { + async _processData(loadedData) { + await this._super(...arguments); + if (this.config.module_pos_hr) { + this.employees = loadedData["hr.employee"]; + this.employee_by_id = loadedData["employee_by_id"]; + this.reset_cashier(); } - async after_load_server_data() { - await super.after_load_server_data(...arguments); - if (this.config.module_pos_hr) { - this.hasLoggedIn = !this.config.module_pos_hr; - } + }, + async after_load_server_data() { + await this._super(...arguments); + if (this.config.module_pos_hr) { + this.hasLoggedIn = !this.config.module_pos_hr; } - reset_cashier() { - this.cashier = { - name: null, - id: null, - barcode: null, - user_id: null, - pin: null, - role: null, - }; + }, + reset_cashier() { + this.cashier = { + name: null, + id: null, + barcode: null, + user_id: null, + pin: null, + role: null, + }; + }, + set_cashier(employee) { + this.cashier = employee; + const selectedOrder = this.get_order(); + if (selectedOrder && !selectedOrder.get_orderlines().length) { + // Order without lines can be considered to be un-owned by any employee. + // We set the cashier on that order to the currently set employee. + selectedOrder.cashier = employee; } - set_cashier(employee) { - this.cashier = employee; - const selectedOrder = this.get_order(); - if (selectedOrder && !selectedOrder.get_orderlines().length) { - // Order without lines can be considered to be un-owned by any employee. - // We set the cashier on that order to the currently set employee. - selectedOrder.cashier = employee; - } - if (!this.cashierHasPriceControlRights() && this.numpadMode === "price") { - this.numpadMode = "quantity"; - } + if (!this.cashierHasPriceControlRights() && this.numpadMode === "price") { + this.numpadMode = "quantity"; } + }, + /**{name: null, id: null, barcode: null, user_id:null, pin:null} + * If pos_hr is activated, return {name: string, id: int, barcode: string, pin: string, user_id: int} + * @returns {null|*} + */ + get_cashier() { + if (this.config.module_pos_hr) { + return this.cashier; + } + return this._super(); + }, + get_cashier_user_id() { + if (this.config.module_pos_hr) { + return this.cashier.user_id ? this.cashier.user_id : null; + } + return this._super(); + }, +}); - /**{name: null, id: null, barcode: null, user_id:null, pin:null} - * If pos_hr is activated, return {name: string, id: int, barcode: string, pin: string, user_id: int} - * @returns {null|*} - */ - get_cashier() { - if (this.config.module_pos_hr) { - return this.cashier; - } - return super.get_cashier(); +patch(Order.prototype, "pos_hr.Order", { + setup(options) { + this._super(...arguments); + if (!options.json && this.pos.config.module_pos_hr) { + this.cashier = this.pos.get_cashier(); } - get_cashier_user_id() { - if (this.config.module_pos_hr) { - return this.cashier.user_id ? this.cashier.user_id : null; - } - return super.get_cashier_user_id(); + }, + init_from_JSON(json) { + this._super(...arguments); + if (this.pos.config.module_pos_hr && json.employee_id) { + this.cashier = this.pos.employee_by_id[json.employee_id]; } - }; -Registries.Model.extend(PosGlobalState, PosHrPosGlobalState); - -const PosHrOrder = (Order) => - class PosHrOrder extends Order { - constructor(obj, options) { - super(...arguments); - if (!options.json && this.pos.config.module_pos_hr) { - this.cashier = this.pos.get_cashier(); - } + }, + export_as_JSON() { + const json = this._super(...arguments); + if (this.pos.config.module_pos_hr) { + json.employee_id = this.cashier ? this.cashier.id : false; } - init_from_JSON(json) { - super.init_from_JSON(...arguments); - if (this.pos.config.module_pos_hr && json.employee_id) { - this.cashier = this.pos.employee_by_id[json.employee_id]; - } - } - export_as_JSON() { - const json = super.export_as_JSON(...arguments); - if (this.pos.config.module_pos_hr) { - json.employee_id = this.cashier ? this.cashier.id : false; - } - return json; - } - }; -Registries.Model.extend(Order, PosHrOrder); + return json; + }, +}); diff --git a/addons/pos_hr_restaurant/static/src/js/Chrome.js b/addons/pos_hr_restaurant/static/src/js/Chrome.js index 09f5e6b7cdd..2382a2fab15 100644 --- a/addons/pos_hr_restaurant/static/src/js/Chrome.js +++ b/addons/pos_hr_restaurant/static/src/js/Chrome.js @@ -1,14 +1,11 @@ /* @odoo-module alias=pos_restaurant_hr.chrome */ -import Chrome from '@point_of_sale/js/Chrome'; -import Registries from '@point_of_sale/js/Registries'; +import { Chrome } from '@point_of_sale/js/Chrome'; +import { patch } from "@web/core/utils/patch"; - -export const PosHrRestaurantChrome = (Chrome) => class extends Chrome { +patch(Chrome.prototype, "pos_hr_restaurant.Chrome", { //@override _shouldResetIdleTimer() { - return super._shouldResetIdleTimer() && this.state.tempScreen?.name !== 'LoginScreen'; + return this._super() && this.state.tempScreen?.name !== 'LoginScreen'; } -} - -Registries.Component.extend(Chrome, PosHrRestaurantChrome); +}); diff --git a/addons/pos_loyalty/static/src/js/ControlButtons/PromoCodeButton.js b/addons/pos_loyalty/static/src/js/ControlButtons/PromoCodeButton.js index 31c87fa7f61..5868320b13b 100644 --- a/addons/pos_loyalty/static/src/js/ControlButtons/PromoCodeButton.js +++ b/addons/pos_loyalty/static/src/js/ControlButtons/PromoCodeButton.js @@ -1,18 +1,20 @@ /** @odoo-module **/ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; +import { TextInputPopup } from "@point_of_sale/js/Popups/TextInputPopup"; export class PromoCodeButton extends PosComponent { + static template = "PromoCodeButton"; + setup() { super.setup(); useListener("click", this.onClick); } async onClick() { - let { confirmed, payload: code } = await this.showPopup("TextInputPopup", { + let { confirmed, payload: code } = await this.showPopup(TextInputPopup, { title: this.env._t("Enter Code"), startingValue: "", placeholder: this.env._t("Gift card or Discount code"), @@ -26,8 +28,6 @@ export class PromoCodeButton extends PosComponent { } } -PromoCodeButton.template = "PromoCodeButton"; - ProductScreen.addControlButton({ component: PromoCodeButton, condition: function () { @@ -36,5 +36,3 @@ ProductScreen.addControlButton({ ); }, }); - -Registries.Component.add(PromoCodeButton); diff --git a/addons/pos_loyalty/static/src/js/ControlButtons/ResetProgramsButton.js b/addons/pos_loyalty/static/src/js/ControlButtons/ResetProgramsButton.js index bd06a58cf15..5ec2742c6e8 100644 --- a/addons/pos_loyalty/static/src/js/ControlButtons/ResetProgramsButton.js +++ b/addons/pos_loyalty/static/src/js/ControlButtons/ResetProgramsButton.js @@ -1,11 +1,12 @@ /** @odoo-module **/ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; export class ResetProgramsButton extends PosComponent { + static template = "ResetProgramsButton"; + setup() { super.setup(); useListener("click", this.onClick); @@ -18,13 +19,9 @@ export class ResetProgramsButton extends PosComponent { } } -ResetProgramsButton.template = "ResetProgramsButton"; - ProductScreen.addControlButton({ component: ResetProgramsButton, condition: function () { return this.env.pos.programs.some((p) => ["coupons", "promotion"].includes(p.program_type)); }, }); - -Registries.Component.add(ResetProgramsButton); diff --git a/addons/pos_loyalty/static/src/js/ControlButtons/RewardButton.js b/addons/pos_loyalty/static/src/js/ControlButtons/RewardButton.js index 87dcf63e61c..5167644d62b 100644 --- a/addons/pos_loyalty/static/src/js/ControlButtons/RewardButton.js +++ b/addons/pos_loyalty/static/src/js/ControlButtons/RewardButton.js @@ -1,12 +1,14 @@ /** @odoo-module **/ import { Gui } from "@point_of_sale/js/Gui"; -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useListener } from "@web/core/utils/hooks"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; export class RewardButton extends PosComponent { + static template = "RewardButton"; + setup() { super.setup(); useListener("click", this.onClick); @@ -68,7 +70,7 @@ export class RewardButton extends PosComponent { label: this.env.pos.db.get_product_by_id(product_id).display_name, item: product_id, })); - const { confirmed, payload: selectedProduct } = await this.showPopup("SelectionPopup", { + const { confirmed, payload: selectedProduct } = await this.showPopup(SelectionPopup, { title: this.env._t("Please select a product for this reward"), list: productsList, }); @@ -112,7 +114,7 @@ export class RewardButton extends PosComponent { label: reward.reward.description, item: reward, })); - const { confirmed, payload: selectedReward } = await this.showPopup("SelectionPopup", { + const { confirmed, payload: selectedReward } = await this.showPopup(SelectionPopup, { title: this.env._t("Please select a reward"), list: rewardsList, }); @@ -128,13 +130,9 @@ export class RewardButton extends PosComponent { } } -RewardButton.template = "RewardButton"; - ProductScreen.addControlButton({ component: RewardButton, condition: function () { return this.env.pos.programs.length > 0; }, }); - -Registries.Component.add(RewardButton); diff --git a/addons/pos_loyalty/static/src/js/ControlButtons/eWalletButton.js b/addons/pos_loyalty/static/src/js/ControlButtons/eWalletButton.js index fa655d5415b..cbab3248efa 100644 --- a/addons/pos_loyalty/static/src/js/ControlButtons/eWalletButton.js +++ b/addons/pos_loyalty/static/src/js/ControlButtons/eWalletButton.js @@ -1,10 +1,13 @@ /** @odoo-module **/ -import PosComponent from "@point_of_sale/js/PosComponent"; -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PosComponent } from "@point_of_sale/js/PosComponent"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; export class eWalletButton extends PosComponent { + static template = "point_of_sale.eWalletButton"; + _getEWalletRewards(order) { const claimableRewards = order.getClaimableRewards(); const eWalletRewards = claimableRewards.filter( @@ -25,7 +28,7 @@ export class eWalletButton extends PosComponent { if (eWalletPrograms.length == 1) { selectedProgram = eWalletPrograms[0]; } else { - const { confirmed, payload } = await this.showPopup("SelectionPopup", { + const { confirmed, payload } = await this.showPopup(SelectionPopup, { title: this.env._t("Refund with eWallet"), list: eWalletPrograms.map((program) => ({ id: program.id, @@ -52,7 +55,7 @@ export class eWalletButton extends PosComponent { if (eWalletRewards.length == 1) { eWalletReward = eWalletRewards[0]; } else { - const { confirmed, payload } = await this.showPopup("SelectionPopup", { + const { confirmed, payload } = await this.showPopup(SelectionPopup, { title: this.env._t("Use eWallet to pay"), list: eWalletRewards.map(({ reward, coupon_id }) => ({ id: reward.id, @@ -72,7 +75,7 @@ export class eWalletButton extends PosComponent { ); if (result !== true) { // Returned an error - this.showPopup("ErrorPopup", { + this.showPopup(ErrorPopup, { title: this.env._t("Error"), body: result, }); @@ -94,7 +97,6 @@ export class eWalletButton extends PosComponent { } } } -eWalletButton.template = "point_of_sale.eWalletButton"; ProductScreen.addControlButton({ component: eWalletButton, @@ -102,5 +104,3 @@ ProductScreen.addControlButton({ return this.env.pos.programs.filter((p) => p.program_type == "ewallet").length > 0; }, }); - -Registries.Component.add(eWalletButton); diff --git a/addons/pos_loyalty/static/src/js/Loyalty.js b/addons/pos_loyalty/static/src/js/Loyalty.js index 9b6bce62000..0319ecc4461 100644 --- a/addons/pos_loyalty/static/src/js/Loyalty.js +++ b/addons/pos_loyalty/static/src/js/Loyalty.js @@ -1,12 +1,13 @@ /** @odoo-module **/ import { Order, Orderline, PosGlobalState } from "@point_of_sale/js/models"; -import Registries from "@point_of_sale/js/Registries"; import session from "web.session"; import concurrency from "web.concurrency"; import { Gui } from "@point_of_sale/js/Gui"; import { round_decimals, round_precision } from "web.utils"; import core from "web.core"; +import { patch } from "@web/core/utils/patch"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; const _t = core._t; const dropPrevious = new concurrency.MutexedDropPrevious(); // Used for queuing reward updates @@ -75,1658 +76,1607 @@ function computeFreeQuantity(numberItems, n, m) { return Math.floor(free + adjustment); } -const PosLoyaltyGlobalState = (PosGlobalState) => - class PosLoyaltyGlobalState extends PosGlobalState { - //@override - async _processData(loadedData) { +patch(PosGlobalState.prototype, "pos_loyalty.PosGlobalState", { + //@override + async _processData(loadedData) { + this.couponCache = {}; + await this._super(loadedData); + this.productId2ProgramIds = loadedData["product_id_to_program_ids"]; + this.programs = loadedData["loyalty.program"] || []; //TODO: rename to `loyaltyPrograms` etc + this.rules = loadedData["loyalty.rule"] || []; + this.rewards = loadedData["loyalty.reward"] || []; + this._loadLoyaltyData(); + }, + _loadLoyaltyData() { + this.program_by_id = {}; + this.reward_by_id = {}; + + for (const program of this.programs) { + this.program_by_id[program.id] = program; + if (program.date_to) { + program.date_to = new Date(program.date_to); + } + program.rules = []; + program.rewards = []; + } + for (const rule of this.rules) { + rule.valid_product_ids = new Set(rule.valid_product_ids); + rule.program_id = this.program_by_id[rule.program_id[0]]; + rule.program_id.rules.push(rule); + } + for (const reward of this.rewards) { + this.reward_by_id[reward.id] = reward; + reward.program_id = this.program_by_id[reward.program_id[0]]; + reward.discount_line_product_id = this.db.get_product_by_id( + reward.discount_line_product_id[0] + ); + reward.all_discount_product_ids = new Set(reward.all_discount_product_ids); + reward.program_id.rewards.push(reward); + } + }, + async load_server_data() { + await this._super(...arguments); + if (this.selectedOrder) { + this.selectedOrder._updateRewards(); + } + }, + set_order(order) { + const result = this._super(...arguments); + // FIXME - JCB: This is a temporary fix. + // When an order is selected, it doesn't always contain the reward lines. + // And the list of active programs are not always correct. This is because + // of the use of DropPrevious in _updateRewards. + if (order) { + order._updateRewards(); + } + return result; + }, + /** + * Fetches `loyalty.card` records from the server and adds/updates them in our cache. + * + * @param {domain} domain For the search + * @param {int} limit Default to 1 + */ + async fetchCoupons(domain, limit = 1) { + const result = await this.env.services.rpc({ + model: "loyalty.card", + method: "search_read", + kwargs: { + domain: domain, + fields: ["id", "points", "code", "partner_id", "program_id"], + limit: limit, + context: session.user_context, + }, + }); + if (Object.keys(this.couponCache).length + result.length > COUPON_CACHE_MAX_SIZE) { this.couponCache = {}; - await super._processData(loadedData); - this.productId2ProgramIds = loadedData["product_id_to_program_ids"]; - this.programs = loadedData["loyalty.program"] || []; //TODO: rename to `loyaltyPrograms` etc - this.rules = loadedData["loyalty.rule"] || []; - this.rewards = loadedData["loyalty.reward"] || []; - this._loadLoyaltyData(); - } - _loadLoyaltyData() { - this.program_by_id = {}; - this.reward_by_id = {}; - - for (const program of this.programs) { - this.program_by_id[program.id] = program; - if (program.date_to) { - program.date_to = new Date(program.date_to); - } - program.rules = []; - program.rewards = []; - } - for (const rule of this.rules) { - rule.valid_product_ids = new Set(rule.valid_product_ids); - rule.program_id = this.program_by_id[rule.program_id[0]]; - rule.program_id.rules.push(rule); - } - for (const reward of this.rewards) { - this.reward_by_id[reward.id] = reward; - reward.program_id = this.program_by_id[reward.program_id[0]]; - reward.discount_line_product_id = this.db.get_product_by_id( - reward.discount_line_product_id[0] - ); - reward.all_discount_product_ids = new Set(reward.all_discount_product_ids); - reward.program_id.rewards.push(reward); - } - } - async load_server_data() { - await super.load_server_data(...arguments); + // Make sure that the current order has no invalid data. if (this.selectedOrder) { - this.selectedOrder._updateRewards(); + this.selectedOrder.invalidCoupons = true; } } - set_order(order) { - const result = super.set_order(...arguments); - // FIXME - JCB: This is a temporary fix. - // When an order is selected, it doesn't always contain the reward lines. - // And the list of active programs are not always correct. This is because - // of the use of DropPrevious in _updateRewards. - if (order) { - order._updateRewards(); - } - return result; + const couponList = []; + for (const dbCoupon of result) { + const coupon = new PosLoyaltyCard( + dbCoupon.code, + dbCoupon.id, + dbCoupon.program_id[0], + dbCoupon.partner_id[0], + dbCoupon.points + ); + this.couponCache[coupon.id] = coupon; + couponList.push(coupon); } - /** - * Fetches `loyalty.card` records from the server and adds/updates them in our cache. - * - * @param {domain} domain For the search - * @param {int} limit Default to 1 - */ - async fetchCoupons(domain, limit = 1) { - const result = await this.env.services.rpc({ - model: "loyalty.card", - method: "search_read", - kwargs: { - domain: domain, - fields: ["id", "points", "code", "partner_id", "program_id"], - limit: limit, - context: session.user_context, - }, + return couponList; + }, + /** + * Fetches a loyalty card for the given program and partner, put in cache afterwards + * if a matching card is found in the cache, that one is used instead. + * If no card is found a local only card will be created until the order is validated. + * + * @param {int} programId + * @param {int} partnerId + */ + async fetchLoyaltyCard(programId, partnerId) { + for (const coupon of Object.values(this.couponCache)) { + if (coupon.partner_id === partnerId && coupon.program_id === programId) { + return coupon; + } + } + const fetchedCoupons = await this.fetchCoupons([ + ["partner_id", "=", partnerId], + ["program_id", "=", programId], + ]); + const dbCoupon = fetchedCoupons.length > 0 ? fetchedCoupons[0] : null; + return dbCoupon || new PosLoyaltyCard(null, null, programId, partnerId, 0); + }, + getLoyaltyCards(partner) { + const loyaltyCards = []; + for (const coupon of Object.values(this.couponCache)) { + if (coupon.partner_id === partner.id) { + loyaltyCards.push(coupon); + } + } + return loyaltyCards; + }, + addPartners(partners) { + const result = this._super(partners); + // cache the loyalty cards of the partners + for (const partner of partners) { + for (const [couponId, { code, program_id, points }] of Object.entries( + partner.loyalty_cards || {} + )) { + this.couponCache[couponId] = new PosLoyaltyCard( + code, + parseInt(couponId, 10), + program_id, + partner.id, + points + ); + } + } + return result; + }, +}); + +patch(Orderline.prototype, "pos_loyalty.Orderline", { + export_as_JSON() { + const result = this._super(...arguments); + result.is_reward_line = this.is_reward_line; + result.reward_id = this.reward_id; + result.reward_product_id = this.reward_product_id; + result.coupon_id = this.coupon_id; + result.reward_identifier_code = this.reward_identifier_code; + result.points_cost = this.points_cost; + result.giftBarcode = this.giftBarcode; + result.giftCardId = this.giftCardId; + result.eWalletGiftCardProgramId = this.eWalletGiftCardProgram + ? this.eWalletGiftCardProgram.id + : null; + return result; + }, + init_from_JSON(json) { + if (json.is_reward_line) { + this.is_reward_line = json.is_reward_line; + this.reward_id = json.reward_id; + this.reward_product_id = json.reward_product_id; + // Since non existing coupon have a negative id, of which the counter is lost upon reloading + // we make sure that they are kept the same between after a reload between the order and the lines. + this.coupon_id = this.order.oldCouponMapping[json.coupon_id] || json.coupon_id; + this.reward_identifier_code = json.reward_identifier_code; + this.points_cost = json.points_cost; + } + this.giftBarcode = json.giftBarcode; + this.giftCardId = json.giftCardId; + this.eWalletGiftCardProgram = this.pos.program_by_id[json.eWalletGiftCardProgramId]; + this._super(...arguments); + }, + set_quantity(quantity, keep_price) { + if (quantity === "remove" && this.is_reward_line) { + // Remove any line that is part of that same reward aswell. + const linesToRemove = []; + this.order.get_orderlines().forEach((line) => { + if ( + line != this && + line.reward_id === this.reward_id && + line.coupon_id === this.coupon_id && + line.reward_identifier_code === this.reward_identifier_code + ) { + linesToRemove.push(line); + } }); - if (Object.keys(this.couponCache).length + result.length > COUPON_CACHE_MAX_SIZE) { - this.couponCache = {}; - // Make sure that the current order has no invalid data. - if (this.selectedOrder) { - this.selectedOrder.invalidCoupons = true; - } + for (const line of linesToRemove) { + this.order.orderlines.remove(line); } - const couponList = []; - for (const dbCoupon of result) { - const coupon = new PosLoyaltyCard( - dbCoupon.code, - dbCoupon.id, - dbCoupon.program_id[0], - dbCoupon.partner_id[0], - dbCoupon.points - ); - this.couponCache[coupon.id] = coupon; - couponList.push(coupon); - } - return couponList; } - /** - * Fetches a loyalty card for the given program and partner, put in cache afterwards - * if a matching card is found in the cache, that one is used instead. - * If no card is found a local only card will be created until the order is validated. - * - * @param {int} programId - * @param {int} partnerId - */ - async fetchLoyaltyCard(programId, partnerId) { - for (const coupon of Object.values(this.couponCache)) { - if (coupon.partner_id === partnerId && coupon.program_id === programId) { - return coupon; - } - } - const fetchedCoupons = await this.fetchCoupons([ - ["partner_id", "=", partnerId], - ["program_id", "=", programId], - ]); - const dbCoupon = fetchedCoupons.length > 0 ? fetchedCoupons[0] : null; - return dbCoupon || new PosLoyaltyCard(null, null, programId, partnerId, 0); - } - getLoyaltyCards(partner) { - const loyaltyCards = []; - for (const coupon of Object.values(this.couponCache)) { - if (coupon.partner_id === partner.id) { - loyaltyCards.push(coupon); - } - } - return loyaltyCards; - } - addPartners(partners) { - const result = super.addPartners(partners); - // cache the loyalty cards of the partners - for (const partner of partners) { - for (const [couponId, { code, program_id, points }] of Object.entries( - partner.loyalty_cards || {} - )) { - this.couponCache[couponId] = new PosLoyaltyCard( - code, - parseInt(couponId, 10), - program_id, - partner.id, - points - ); - } - } - return result; - } - }; -Registries.Model.extend(PosGlobalState, PosLoyaltyGlobalState); + return this._super(...arguments); + }, + getEWalletGiftCardProgramType() { + return this.eWalletGiftCardProgram && this.eWalletGiftCardProgram.program_type; + }, + ignoreLoyaltyPoints({ program }) { + return ( + ["gift_card", "ewallet"].includes(program.program_type) && + this.eWalletGiftCardProgram && + this.eWalletGiftCardProgram.id !== program.id + ); + }, +}); -const PosLoyaltyOrderline = (Orderline) => - class PosLoyaltyOrderline extends Orderline { - export_as_JSON() { - const result = super.export_as_JSON(...arguments); - result.is_reward_line = this.is_reward_line; - result.reward_id = this.reward_id; - result.reward_product_id = this.reward_product_id; - result.coupon_id = this.coupon_id; - result.reward_identifier_code = this.reward_identifier_code; - result.points_cost = this.points_cost; - result.giftBarcode = this.giftBarcode; - result.giftCardId = this.giftCardId; - result.eWalletGiftCardProgramId = this.eWalletGiftCardProgram - ? this.eWalletGiftCardProgram.id - : null; - return result; - } - init_from_JSON(json) { - if (json.is_reward_line) { - this.is_reward_line = json.is_reward_line; - this.reward_id = json.reward_id; - this.reward_product_id = json.reward_product_id; - // Since non existing coupon have a negative id, of which the counter is lost upon reloading - // we make sure that they are kept the same between after a reload between the order and the lines. - this.coupon_id = this.order.oldCouponMapping[json.coupon_id] || json.coupon_id; - this.reward_identifier_code = json.reward_identifier_code; - this.points_cost = json.points_cost; - } - this.giftBarcode = json.giftBarcode; - this.giftCardId = json.giftCardId; - this.eWalletGiftCardProgram = this.pos.program_by_id[json.eWalletGiftCardProgramId]; - super.init_from_JSON(...arguments); - } - set_quantity(quantity, keep_price) { - if (quantity === "remove" && this.is_reward_line) { - // Remove any line that is part of that same reward aswell. - const linesToRemove = []; - this.order.get_orderlines().forEach((line) => { - if ( - line != this && - line.reward_id === this.reward_id && - line.coupon_id === this.coupon_id && - line.reward_identifier_code === this.reward_identifier_code - ) { - linesToRemove.push(line); - } - }); - for (const line of linesToRemove) { - this.order.orderlines.remove(line); +patch(Order.prototype, "pos_loyalty.Order", { + setup() { + this._super(...arguments); + this._initializePrograms({}); + // Always start with invalid coupons so that coupon for this + // order is properly assigned. @see _checkMissingCoupons + this.invalidCoupons = true; + }, + export_as_JSON() { + const json = this._super(...arguments); + json.disabledRewards = [...this.disabledRewards]; + json.codeActivatedProgramRules = this.codeActivatedProgramRules; + json.codeActivatedCoupons = this.codeActivatedCoupons; + json.couponPointChanges = this.couponPointChanges; + return json; + }, + init_from_JSON(json) { + this.couponPointChanges = json.couponPointChanges; + // Remapping of coupon_id for both couponPointChanges and Orderline.coupon_id + this.oldCouponMapping = {}; + if (this.couponPointChanges) { + for (const [key, pe] of Object.entries(this.couponPointChanges)) { + if (!this.pos.program_by_id[pe.program_id]) { + // Remove points changes for programs that are not available anymore. + delete this.couponPointChanges[key]; + continue; } + if (pe.coupon_id > 0) { + continue; + } + const newId = nextId--; + delete this.oldCouponMapping[pe.coupon_id]; + pe.coupon_id = newId; + this.couponPointChanges[newId] = pe; } - return super.set_quantity(...arguments); } - getEWalletGiftCardProgramType() { - return this.eWalletGiftCardProgram && this.eWalletGiftCardProgram.program_type; - } - ignoreLoyaltyPoints({ program }) { - return ( - ["gift_card", "ewallet"].includes(program.program_type) && - this.eWalletGiftCardProgram && - this.eWalletGiftCardProgram.id !== program.id + this._super(...arguments); + delete this.oldCouponMapping; + this.disabledRewards = new Set(json.disabledRewards); + this.codeActivatedProgramRules = json.codeActivatedProgramRules; + this.codeActivatedCoupons = json.codeActivatedCoupons; + }, + /** + * We need to update the rewards upon changing the partner as it may impact the points available + * for rewards. + * + * @override + */ + set_partner(partner) { + const oldPartner = this.get_partner(); + this._super(partner); + if (this.couponPointChanges && oldPartner !== this.get_partner()) { + // Remove couponPointChanges for cards in is_nominative programs. + // This makes sure that counting of points on loyalty and ewallet programs is updated after partner changes. + const loyaltyProgramIds = new Set( + this.pos.programs + .filter((program) => program.is_nominative) + .map((program) => program.id) ); - } - }; -Registries.Model.extend(Orderline, PosLoyaltyOrderline); - -const PosLoyaltyOrder = (Order) => - class PosLoyaltyOrder extends Order { - constructor() { - super(...arguments); - this._initializePrograms({}); - // Always start with invalid coupons so that coupon for this - // order is properly assigned. @see _checkMissingCoupons - this.invalidCoupons = true; - } - export_as_JSON() { - const json = super.export_as_JSON(...arguments); - json.disabledRewards = [...this.disabledRewards]; - json.codeActivatedProgramRules = this.codeActivatedProgramRules; - json.codeActivatedCoupons = this.codeActivatedCoupons; - json.couponPointChanges = this.couponPointChanges; - return json; - } - init_from_JSON(json) { - this.couponPointChanges = json.couponPointChanges; - // Remapping of coupon_id for both couponPointChanges and Orderline.coupon_id - this.oldCouponMapping = {}; - if (this.couponPointChanges) { - for (const [key, pe] of Object.entries(this.couponPointChanges)) { - if (!this.pos.program_by_id[pe.program_id]) { - // Remove points changes for programs that are not available anymore. - delete this.couponPointChanges[key]; - continue; - } - if (pe.coupon_id > 0) { - continue; - } - const newId = nextId--; - delete this.oldCouponMapping[pe.coupon_id]; - pe.coupon_id = newId; - this.couponPointChanges[newId] = pe; + for (const [key, pointChange] of Object.entries(this.couponPointChanges)) { + if (loyaltyProgramIds.has(pointChange.program_id)) { + delete this.couponPointChanges[key]; } } - super.init_from_JSON(...arguments); - delete this.oldCouponMapping; - this.disabledRewards = new Set(json.disabledRewards); - this.codeActivatedProgramRules = json.codeActivatedProgramRules; - this.codeActivatedCoupons = json.codeActivatedCoupons; - } - /** - * We need to update the rewards upon changing the partner as it may impact the points available - * for rewards. - * - * @override - */ - set_partner(partner) { - const oldPartner = this.get_partner(); - super.set_partner(partner); - if (this.couponPointChanges && oldPartner !== this.get_partner()) { - // Remove couponPointChanges for cards in is_nominative programs. - // This makes sure that counting of points on loyalty and ewallet programs is updated after partner changes. - const loyaltyProgramIds = new Set( - this.pos.programs - .filter((program) => program.is_nominative) - .map((program) => program.id) - ); - for (const [key, pointChange] of Object.entries(this.couponPointChanges)) { - if (loyaltyProgramIds.has(pointChange.program_id)) { - delete this.couponPointChanges[key]; - } - } - this._updateRewards(); - } - } - wait_for_push_order() { - return ( - !_.isEmpty(this.couponPointChanges) || - this._get_reward_lines().length || - super.wait_for_push_order(...arguments) - ); - } - /** - * Add additional information for our ticket, such as new coupons and loyalty point gains. - * - * @override - */ - export_for_printing() { - const result = super.export_for_printing(...arguments); - if (this.get_partner()) { - result.loyaltyStats = this.getLoyaltyPoints(); - } - result.new_coupon_info = this.new_coupon_info; - return result; - } - //@override - _get_ignored_product_ids_total_discount() { - const productIds = super._get_ignored_product_ids_total_discount(...arguments); - const giftCardPrograms = this.pos.programs.filter( - (p) => p.program_type === "gift_card" - ); - for (const program of giftCardPrograms) { - const giftCardProductId = [...program.rules[0].valid_product_ids][0]; - if (giftCardProductId) { - productIds.push(giftCardProductId); - } - } - return productIds; - } - get_orderlines() { - const orderlines = super.get_orderlines(this, arguments); - const rewardLines = []; - const nonRewardLines = []; - for (const line of orderlines) { - if (line.is_reward_line) { - rewardLines.push(line); - } else { - nonRewardLines.push(line); - } - } - return [...nonRewardLines, ...rewardLines]; - } - _get_reward_lines() { - const orderLines = super.get_orderlines(...arguments); - if (orderLines) { - return orderLines.filter((line) => line.is_reward_line); - } - return orderLines; - } - _get_regular_order_lines() { - const orderLines = super.get_orderlines(...arguments); - if (orderLines) { - return orderLines.filter( - (line) => !line.is_reward_line && !line.refunded_orderline_id - ); - } - return orderLines; - } - get_last_orderline() { - const orderLines = super - .get_orderlines(...arguments) - .filter((line) => !line.is_reward_line); - return orderLines[orderLines.length - 1]; - } - set_pricelist(pricelist) { - super.set_pricelist(...arguments); this._updateRewards(); } - set_orderline_options(line, options) { - super.set_orderline_options(...arguments); - if (options && options.is_reward_line) { - line.is_reward_line = options.is_reward_line; - line.reward_id = options.reward_id; - line.reward_product_id = options.reward_product_id; - line.coupon_id = options.coupon_id; - line.reward_identifier_code = options.reward_identifier_code; - line.points_cost = options.points_cost; - line.price_manually_set = true; - } - line.giftBarcode = options.giftBarcode; - line.giftCardId = options.giftCardId; - line.eWalletGiftCardProgram = options.eWalletGiftCardProgram; + }, + wait_for_push_order() { + return ( + !_.isEmpty(this.couponPointChanges) || + this._get_reward_lines().length || + this._super(...arguments) + ); + }, + /** + * Add additional information for our ticket, such as new coupons and loyalty point gains. + * + * @override + */ + export_for_printing() { + const result = this._super(...arguments); + if (this.get_partner()) { + result.loyaltyStats = this.getLoyaltyPoints(); } - add_product(product, options) { - super.add_product(...arguments); - this._updateRewards(); - } - - async _initializePrograms() { - // When deleting a reward line, a popup will be displayed if the reward was automatic, - // if confirmed the reward is added to this list and will not be claimed automatically again. - if (!this.disabledRewards) { - this.disabledRewards = new Set(); - } - // List of programs that require a code that are activated. - if (!this.codeActivatedProgramRules) { - this.codeActivatedProgramRules = []; - } - // List of coupons activated manually - if (!this.codeActivatedCoupons) { - this.codeActivatedCoupons = []; - } - // This field will hold the added points for each coupon. - // Points lost are directly linked to the order lines. - if (!this.couponPointChanges) { - this.couponPointChanges = {}; + result.new_coupon_info = this.new_coupon_info; + return result; + }, + //@override + _get_ignored_product_ids_total_discount() { + const productIds = this._super(...arguments); + const giftCardPrograms = this.pos.programs.filter((p) => p.program_type === "gift_card"); + for (const program of giftCardPrograms) { + const giftCardProductId = [...program.rules[0].valid_product_ids][0]; + if (giftCardProductId) { + productIds.push(giftCardProductId); } } - _resetPrograms() { + return productIds; + }, + get_orderlines() { + const orderlines = this._super(this, arguments); + const rewardLines = []; + const nonRewardLines = []; + for (const line of orderlines) { + if (line.is_reward_line) { + rewardLines.push(line); + } else { + nonRewardLines.push(line); + } + } + return [...nonRewardLines, ...rewardLines]; + }, + _get_reward_lines() { + if (this.orderlines) { + return this.orderlines.filter((line) => line.is_reward_line); + } + return this.orderlines; + }, + _get_regular_order_lines() { + if (this.orderlines) { + return this.orderlines.filter( + (line) => !line.is_reward_line && !line.refunded_orderline_id + ); + } + return this.orderlines; + }, + get_last_orderline() { + const orderLines = this.orderlines.filter((line) => !line.is_reward_line); + return orderLines[orderLines.length - 1]; + }, + set_pricelist(pricelist) { + this._super(...arguments); + this._updateRewards(); + }, + set_orderline_options(line, options) { + this._super(...arguments); + if (options && options.is_reward_line) { + line.is_reward_line = options.is_reward_line; + line.reward_id = options.reward_id; + line.reward_product_id = options.reward_product_id; + line.coupon_id = options.coupon_id; + line.reward_identifier_code = options.reward_identifier_code; + line.points_cost = options.points_cost; + line.price_manually_set = true; + } + line.giftBarcode = options.giftBarcode; + line.giftCardId = options.giftCardId; + line.eWalletGiftCardProgram = options.eWalletGiftCardProgram; + }, + add_product(product, options) { + this._super(...arguments); + this._updateRewards(); + }, + async _initializePrograms() { + // When deleting a reward line, a popup will be displayed if the reward was automatic, + // if confirmed the reward is added to this list and will not be claimed automatically again. + if (!this.disabledRewards) { this.disabledRewards = new Set(); - this.codeActivatedProgramRules = []; - this.codeActivatedCoupons = []; - this.couponPointChanges = {}; - this.orderlines.remove(this._get_reward_lines()); - this._updateRewards(); } - _updateRewards() { - // Calls are not expected to take some time besides on the first load + when loyalty programs are made applicable - if (this.pos.programs.length === 0) { + // List of programs that require a code that are activated. + if (!this.codeActivatedProgramRules) { + this.codeActivatedProgramRules = []; + } + // List of coupons activated manually + if (!this.codeActivatedCoupons) { + this.codeActivatedCoupons = []; + } + // This field will hold the added points for each coupon. + // Points lost are directly linked to the order lines. + if (!this.couponPointChanges) { + this.couponPointChanges = {}; + } + }, + _resetPrograms() { + this.disabledRewards = new Set(); + this.codeActivatedProgramRules = []; + this.codeActivatedCoupons = []; + this.couponPointChanges = {}; + this.orderlines.remove(this._get_reward_lines()); + this._updateRewards(); + }, + _updateRewards() { + // Calls are not expected to take some time besides on the first load + when loyalty programs are made applicable + if (this.pos.programs.length === 0) { + return; + } + dropPrevious + .exec(() => { + return this._updateLoyaltyPrograms().then(async () => { + // Try auto claiming rewards + const claimableRewards = this.getClaimableRewards(false, false, true); + let changed = false; + for (const { coupon_id, reward } of claimableRewards) { + if ( + reward.program_id.rewards.length === 1 && + !reward.program_id.is_nominative && + (reward.reward_type !== "product" || + (reward.reward_type == "product" && !reward.multi_product)) + ) { + this._applyReward(reward, coupon_id); + changed = true; + } + } + // Rewards may impact the number of points gained + if (changed) { + await this._updateLoyaltyPrograms(); + } + this._updateRewardLines(); + }); + }) + .catch(() => { + /* catch the reject of dp when calling `add` to avoid unhandledrejection */ + }); + }, + async _updateLoyaltyPrograms() { + await this._checkMissingCoupons(); + await this._updatePrograms(); + }, + /** + * Checks that all 'existing' coupons are in our cache, and if not load/update them. + */ + async _checkMissingCoupons() { + // This function must stay sequential to avoid potential concurrency errors. + await mutex.exec(async () => { + if (!this.invalidCoupons) { return; } - dropPrevious - .exec(() => { - return this._updateLoyaltyPrograms().then(async () => { - // Try auto claiming rewards - const claimableRewards = this.getClaimableRewards(false, false, true); - let changed = false; - for (const { coupon_id, reward } of claimableRewards) { - if ( - reward.program_id.rewards.length === 1 && - !reward.program_id.is_nominative && - (reward.reward_type !== "product" || - (reward.reward_type == "product" && !reward.multi_product)) - ) { - this._applyReward(reward, coupon_id); - changed = true; + this.invalidCoupons = false; + const allCoupons = []; + for (const pe of Object.values(this.couponPointChanges)) { + if (pe.coupon_id > 0) { + allCoupons.push(pe.coupon_id); + } + } + allCoupons.push(...this.codeActivatedCoupons.map((coupon) => coupon.id)); + const couponsToFetch = allCoupons.filter((elem) => !this.pos.couponCache[elem]); + if (couponsToFetch.length) { + await this.pos.fetchCoupons([["id", "in", couponsToFetch]], couponsToFetch.length); + // Remove coupons that could not be loaded from the db + this.codeActivatedCoupons = this.codeActivatedCoupons.filter( + (coupon) => this.pos.couponCache[coupon.id] + ); + this.couponPointChanges = Object.fromEntries( + Object.entries(this.couponPointChanges).filter( + ([k, pe]) => this.pos.couponCache[pe.coupon_id] + ) + ); + } + }); + }, + /** + * Refreshes the currently applied rewards, if they are not applicable anymore they are removed. + */ + _updateRewardLines() { + if (!this.orderlines.length) { + return; + } + const rewardLines = this._get_reward_lines(); + if (!rewardLines.length) { + return; + } + const productRewards = []; + const otherRewards = []; + const paymentRewards = []; // Gift card and ewallet rewards are considered payments and must stay at the end + for (const line of rewardLines) { + const claimedReward = { + reward: this.pos.reward_by_id[line.reward_id], + coupon_id: line.coupon_id, + args: { + product: line.reward_product_id, + }, + }; + if ( + claimedReward.reward.program_id.program_type === "gift_card" || + claimedReward.reward.program_id.program_type === "ewallet" + ) { + paymentRewards.push(claimedReward); + } else if (claimedReward.reward.reward_type === "product") { + productRewards.push(claimedReward); + } else { + otherRewards.push(claimedReward); + } + this.orderlines.remove(line); + } + for (const claimedReward of productRewards.concat(otherRewards).concat(paymentRewards)) { + // For existing coupons check that they are still claimed, they can exist in either `couponPointChanges` or `codeActivatedCoupons` + if ( + !this.codeActivatedCoupons.find( + (coupon) => coupon.id === claimedReward.coupon_id + ) && + !this.couponPointChanges[claimedReward.coupon_id] + ) { + continue; + } + this._applyReward(claimedReward.reward, claimedReward.coupon_id, claimedReward.args); + } + }, + /** + * Update our couponPointChanges, meaning the points/coupons each program give etc. + */ + async _updatePrograms() { + const changesPerProgram = {}; + const programsToCheck = new Set(); + // By default include all programs that are considered 'applicable' + for (const program of this.pos.programs) { + if (this._programIsApplicable(program)) { + programsToCheck.add(program.id); + } + } + for (const pe of Object.values(this.couponPointChanges)) { + if (!changesPerProgram[pe.program_id]) { + changesPerProgram[pe.program_id] = []; + programsToCheck.add(pe.program_id); + } + changesPerProgram[pe.program_id].push(pe); + } + for (const coupon of this.codeActivatedCoupons) { + programsToCheck.add(coupon.program_id); + } + const programs = [...programsToCheck].map((programId) => this.pos.program_by_id[programId]); + const pointsAddedPerProgram = this.pointsForPrograms(programs); + for (const program of this.pos.programs) { + // Future programs may split their points per unit paid (gift cards for example), consider a non applicable program to give no points + const pointsAdded = this._programIsApplicable(program) + ? pointsAddedPerProgram[program.id] + : []; + // For programs that apply to both (loyalty) we always add a change of 0 points, if there is none, since it makes it easier to + // track for claimable rewards, and makes sure to load the partner's loyalty card. + if (program.is_nominative && !pointsAdded.length && this.get_partner()) { + pointsAdded.push({ points: 0 }); + } + const oldChanges = changesPerProgram[program.id] || []; + // Update point changes for those that exist + for (let idx = 0; idx < Math.min(pointsAdded.length, oldChanges.length); idx++) { + Object.assign(oldChanges[idx], pointsAdded[idx]); + } + if (pointsAdded.length < oldChanges.length) { + const removedIds = oldChanges.map((pe) => pe.coupon_id); + this.couponPointChanges = Object.fromEntries( + Object.entries(this.couponPointChanges).filter(([k, pe]) => { + return !removedIds.includes(pe.coupon_id); + }) + ); + } else if (pointsAdded.length > oldChanges.length) { + for (const pa of pointsAdded.splice(oldChanges.length)) { + const coupon = await this._couponForProgram(program); + this.couponPointChanges[coupon.id] = { + points: pa.points, + program_id: program.id, + coupon_id: coupon.id, + barcode: pa.barcode, + }; + } + } + } + // Also remove coupons from codeActivatedCoupons if their program applies_on current orders and the program does not give any points + this.codeActivatedCoupons = this.codeActivatedCoupons.filter((coupon) => { + const program = this.pos.program_by_id[coupon.program_id]; + if ( + program.applies_on === "current" && + pointsAddedPerProgram[program.id].length === 0 + ) { + return false; + } + return true; + }); + }, + /** + * @typedef {{ won: number, spend: number, total: number, balance: number, name: string}} LoyaltyPoints + * @typedef {{ couponId: number, program: object, points: LoyaltyPoints}} LoyaltyStat + * @returns {Array} + */ + getLoyaltyPoints() { + // map: couponId -> LoyaltyPoints + const loyaltyPoints = {}; + for (const pointChange of Object.values(this.couponPointChanges)) { + const { coupon_id, points, program_id } = pointChange; + const program = this.pos.program_by_id[program_id]; + if (program.program_type !== "loyalty") { + // Not a loyalty program, skip + continue; + } + const loyaltyCard = this.pos.couponCache[coupon_id] || /* or new card */ { + id: coupon_id, + balance: 0, + }; + let [won, spent, total] = [0, 0, 0]; + const balance = loyaltyCard.balance; + won += points - this._getPointsCorrection(program); + if (coupon_id !== 0) { + for (const line of this._get_reward_lines()) { + if (line.coupon_id === coupon_id) { + spent += line.points_cost; + } + } + } + total = balance + won - spent; + const name = program.portal_visible ? program.portal_point_name : _t("Points"); + loyaltyPoints[coupon_id] = { + won: parseFloat(won.toFixed(2)), + spent: parseFloat(spent.toFixed(2)), + // Display total when order is ongoing. + total: parseFloat(total.toFixed(2)), + // Display balance when order is done. + balance: parseFloat(balance.toFixed(2)), + name, + program, + }; + } + return Object.entries(loyaltyPoints).map(([couponId, points]) => ({ + couponId, + points, + program: points.program, + })); + }, + /** + * The points in the couponPointChanges for free product reward is not correct. + * It doesn't take into account the points from the `free` product. Use this method + * to compute the necessary correction. + * @param {*} program + * @returns {number} + */ + _getPointsCorrection(program) { + const rewardLines = this.orderlines.filter((line) => line.is_reward_line); + let res = 0; + for (const rule of program.rules) { + for (const line of rewardLines) { + const reward = this.pos.reward_by_id[line.reward_id]; + if (reward.reward_type !== "product") { + continue; + } + if (rule.reward_point_mode === "order") { + res += rule.reward_point_amount; + } else if (rule.reward_point_mode === "money") { + res -= round_precision( + rule.reward_point_amount * line.get_price_with_tax(), + 0.01 + ); + } else if (rule.reward_point_mode === "unit") { + res += rule.reward_point_amount * line.get_quantity(); + } + } + } + return res; + }, + /** + * @returns {number} The points that are left for the given coupon for this order. + */ + _getRealCouponPoints(coupon_id) { + let points = 0; + const dbCoupon = this.pos.couponCache[coupon_id]; + if (dbCoupon) { + points += dbCoupon.balance; + } + Object.values(this.couponPointChanges).some((pe) => { + if (pe.coupon_id === coupon_id) { + if (this.pos.program_by_id[pe.program_id].applies_on !== "future") { + points += pe.points; + } + // couponPointChanges is not supposed to have a coupon multiple times + return true; + } + return false; + }); + for (const line of this.get_orderlines()) { + if (line.is_reward_line && line.coupon_id === coupon_id) { + points -= line.points_cost; + } + } + return points; + }, + /** + * Depending on the program type returns a new (local) instance of coupon or tries to retrieve the coupon in case of loyalty cards. + * Existing coupons are put in a cache which is also used to fetch the coupons. + * + * @param {object} program + */ + async _couponForProgram(program) { + if (program.is_nominative) { + return this.pos.fetchLoyaltyCard(program.id, this.get_partner().id); + } + // This type of coupons don't need to really exist up until validating the order, so no need to cache + return new PosLoyaltyCard(null, null, program.id, (this.get_partner() || { id: -1 }).id, 0); + }, + _programIsApplicable(program) { + if ( + program.trigger === "auto" && + !program.rules.find( + (rule) => rule.mode === "auto" || this.codeActivatedProgramRules.includes(rule.id) + ) + ) { + return false; + } + if ( + program.trigger === "with_code" && + !program.rules.find((rule) => this.codeActivatedProgramRules.includes(rule.id)) + ) { + return false; + } + if (program.is_nominative && !this.get_partner()) { + return false; + } + if (program.date_to && program.date_to <= new Date()) { + return false; + } + if (program.limit_usage && program.total_order_count >= program.max_usage) { + return false; + } + return true; + }, + /** + * Computes how much points each program gives. + * + * @param {Array} programs list of loyalty.program + * @returns {Object} Containing the points gained per program + */ + pointsForPrograms(programs) { + const totalTaxed = this.get_total_with_tax(); + const totalUntaxed = this.get_total_without_tax(); + const totalsPerProgram = Object.fromEntries( + programs.map((program) => [program.id, { untaxed: totalUntaxed, taxed: totalTaxed }]) + ); + const orderLines = this.get_orderlines(); + for (const line of orderLines) { + if (!line.reward_id) { + continue; + } + const reward = this.pos.reward_by_id[line.reward_id]; + if (reward.reward_type !== "discount") { + continue; + } + const rewardProgram = reward.program_id; + for (const program of programs) { + // Remove automatic discount and this program's discounts from the totals. + if (program.id === rewardProgram.id || rewardProgram.trigger === "auto") { + totalsPerProgram[program.id]["taxed"] -= line.get_price_with_tax(); + totalsPerProgram[program.id]["untaxed"] -= line.get_price_without_tax(); + } + } + } + const result = {}; + for (const program of programs) { + let points = 0; + const splitPoints = []; + for (const rule of program.rules) { + if ( + rule.mode === "with_code" && + !this.codeActivatedProgramRules.includes(rule.id) + ) { + continue; + } + const amountCheck = + (rule.minimum_amount_tax_mode === "incl" && + totalsPerProgram[program.id]["taxed"]) || + totalsPerProgram[program.id]["untaxed"]; + if (rule.minimum_amount > amountCheck) { + // NOTE: big doutes par rapport au fait de compter tous les produits + continue; + } + let totalProductQty = 0; + // Only count points for paid lines. + const qtyPerProduct = {}; + let orderedProductPaid = 0; + for (const line of orderLines) { + if ( + ((!line.reward_product_id && + (rule.any_product || + rule.valid_product_ids.has(line.get_product().id))) || + (line.reward_product_id && + (rule.any_product || + rule.valid_product_ids.has(line.reward_product_id)))) && + !line.ignoreLoyaltyPoints({ program }) + ) { + // We only count reward products from the same program to avoid unwanted feedback loops + if (line.reward_product_id) { + const reward = this.pos.reward_by_id[line.reward_id]; + if (program.id !== reward.program_id) { + continue; } } - // Rewards may impact the number of points gained - if (changed) { - await this._updateLoyaltyPrograms(); + const lineQty = line.reward_product_id + ? -line.get_quantity() + : line.get_quantity(); + totalProductQty += lineQty; + if (qtyPerProduct[line.reward_product_id || line.get_product().id]) { + qtyPerProduct[line.reward_product_id || line.get_product().id] += + lineQty; + } else { + qtyPerProduct[line.reward_product_id || line.get_product().id] = + lineQty; } - this._updateRewardLines(); - }); - }) - .catch(() => { - /* catch the reject of dp when calling `add` to avoid unhandledrejection */ - }); - } - async _updateLoyaltyPrograms() { - await this._checkMissingCoupons(); - await this._updatePrograms(); - } - /** - * Checks that all 'existing' coupons are in our cache, and if not load/update them. - */ - async _checkMissingCoupons() { - // This function must stay sequential to avoid potential concurrency errors. - await mutex.exec(async () => { - if (!this.invalidCoupons) { - return; - } - this.invalidCoupons = false; - const allCoupons = []; - for (const pe of Object.values(this.couponPointChanges)) { - if (pe.coupon_id > 0) { - allCoupons.push(pe.coupon_id); + orderedProductPaid += line.get_price_with_tax(); } } - allCoupons.push(...this.codeActivatedCoupons.map((coupon) => coupon.id)); - const couponsToFetch = allCoupons.filter((elem) => !this.pos.couponCache[elem]); - if (couponsToFetch.length) { - await this.pos.fetchCoupons( - [["id", "in", couponsToFetch]], - couponsToFetch.length - ); - // Remove coupons that could not be loaded from the db - this.codeActivatedCoupons = this.codeActivatedCoupons.filter( - (coupon) => this.pos.couponCache[coupon.id] - ); - this.couponPointChanges = Object.fromEntries( - Object.entries(this.couponPointChanges).filter( - ([k, pe]) => this.pos.couponCache[pe.coupon_id] - ) - ); - } - }); - } - /** - * Refreshes the currently applied rewards, if they are not applicable anymore they are removed. - */ - _updateRewardLines() { - if (!this.orderlines.length) { - return; - } - const rewardLines = this._get_reward_lines(); - if (!rewardLines.length) { - return; - } - const productRewards = []; - const otherRewards = []; - const paymentRewards = []; // Gift card and ewallet rewards are considered payments and must stay at the end - for (const line of rewardLines) { - const claimedReward = { - reward: this.pos.reward_by_id[line.reward_id], - coupon_id: line.coupon_id, - args: { - product: line.reward_product_id, - }, - }; - if ( - claimedReward.reward.program_id.program_type === "gift_card" || - claimedReward.reward.program_id.program_type === "ewallet" - ) { - paymentRewards.push(claimedReward); - } else if (claimedReward.reward.reward_type === "product") { - productRewards.push(claimedReward); - } else { - otherRewards.push(claimedReward); - } - this.orderlines.remove(line); - } - for (const claimedReward of productRewards - .concat(otherRewards) - .concat(paymentRewards)) { - // For existing coupons check that they are still claimed, they can exist in either `couponPointChanges` or `codeActivatedCoupons` - if ( - !this.codeActivatedCoupons.find( - (coupon) => coupon.id === claimedReward.coupon_id - ) && - !this.couponPointChanges[claimedReward.coupon_id] - ) { + if (totalProductQty < rule.minimum_qty) { + // Should also count the points from negative quantities. + // For example, when refunding an ewallet payment. See TicketScreen override in this addon. continue; } - this._applyReward( - claimedReward.reward, - claimedReward.coupon_id, - claimedReward.args - ); - } - } - /** - * Update our couponPointChanges, meaning the points/coupons each program give etc. - */ - async _updatePrograms() { - const changesPerProgram = {}; - const programsToCheck = new Set(); - // By default include all programs that are considered 'applicable' - for (const program of this.pos.programs) { - if (this._programIsApplicable(program)) { - programsToCheck.add(program.id); - } - } - for (const pe of Object.values(this.couponPointChanges)) { - if (!changesPerProgram[pe.program_id]) { - changesPerProgram[pe.program_id] = []; - programsToCheck.add(pe.program_id); - } - changesPerProgram[pe.program_id].push(pe); - } - for (const coupon of this.codeActivatedCoupons) { - programsToCheck.add(coupon.program_id); - } - const programs = [...programsToCheck].map( - (programId) => this.pos.program_by_id[programId] - ); - const pointsAddedPerProgram = this.pointsForPrograms(programs); - for (const program of this.pos.programs) { - // Future programs may split their points per unit paid (gift cards for example), consider a non applicable program to give no points - const pointsAdded = this._programIsApplicable(program) - ? pointsAddedPerProgram[program.id] - : []; - // For programs that apply to both (loyalty) we always add a change of 0 points, if there is none, since it makes it easier to - // track for claimable rewards, and makes sure to load the partner's loyalty card. - if (program.is_nominative && !pointsAdded.length && this.get_partner()) { - pointsAdded.push({ points: 0 }); - } - const oldChanges = changesPerProgram[program.id] || []; - // Update point changes for those that exist - for (let idx = 0; idx < Math.min(pointsAdded.length, oldChanges.length); idx++) { - Object.assign(oldChanges[idx], pointsAdded[idx]); - } - if (pointsAdded.length < oldChanges.length) { - const removedIds = oldChanges.map((pe) => pe.coupon_id); - this.couponPointChanges = Object.fromEntries( - Object.entries(this.couponPointChanges).filter(([k, pe]) => { - return !removedIds.includes(pe.coupon_id); - }) - ); - } else if (pointsAdded.length > oldChanges.length) { - for (const pa of pointsAdded.splice(oldChanges.length)) { - const coupon = await this._couponForProgram(program); - this.couponPointChanges[coupon.id] = { - points: pa.points, - program_id: program.id, - coupon_id: coupon.id, - barcode: pa.barcode, - }; - } - } - } - // Also remove coupons from codeActivatedCoupons if their program applies_on current orders and the program does not give any points - this.codeActivatedCoupons = this.codeActivatedCoupons.filter((coupon) => { - const program = this.pos.program_by_id[coupon.program_id]; if ( - program.applies_on === "current" && - pointsAddedPerProgram[program.id].length === 0 + program.applies_on === "future" && + rule.reward_point_split && + rule.reward_point_mode !== "order" ) { - return false; - } - return true; - }); - } - /** - * @typedef {{ won: number, spend: number, total: number, balance: number, name: string}} LoyaltyPoints - * @typedef {{ couponId: number, program: object, points: LoyaltyPoints}} LoyaltyStat - * @returns {Array} - */ - getLoyaltyPoints() { - // map: couponId -> LoyaltyPoints - const loyaltyPoints = {}; - for (const pointChange of Object.values(this.couponPointChanges)) { - const { coupon_id, points, program_id } = pointChange; - const program = this.pos.program_by_id[program_id]; - if (program.program_type !== "loyalty") { - // Not a loyalty program, skip - continue; - } - const loyaltyCard = this.pos.couponCache[coupon_id] || /* or new card */ { - id: coupon_id, - balance: 0, - }; - let [won, spent, total] = [0, 0, 0]; - const balance = loyaltyCard.balance; - won += points - this._getPointsCorrection(program); - if (coupon_id !== 0) { - for (const line of this._get_reward_lines()) { - if (line.coupon_id === coupon_id) { - spent += line.points_cost; - } - } - } - total = balance + won - spent; - const name = program.portal_visible ? program.portal_point_name : _t("Points"); - loyaltyPoints[coupon_id] = { - won: parseFloat(won.toFixed(2)), - spent: parseFloat(spent.toFixed(2)), - // Display total when order is ongoing. - total: parseFloat(total.toFixed(2)), - // Display balance when order is done. - balance: parseFloat(balance.toFixed(2)), - name, - program, - }; - } - return Object.entries(loyaltyPoints).map(([couponId, points]) => ({ - couponId, - points, - program: points.program, - })); - } - /** - * The points in the couponPointChanges for free product reward is not correct. - * It doesn't take into account the points from the `free` product. Use this method - * to compute the necessary correction. - * @param {*} program - * @returns {number} - */ - _getPointsCorrection(program) { - const rewardLines = this.orderlines.filter((line) => line.is_reward_line); - let res = 0; - for (const rule of program.rules) { - for (const line of rewardLines) { - const reward = this.pos.reward_by_id[line.reward_id]; - if (reward.reward_type !== "product") { - continue; - } - if (rule.reward_point_mode === "order") { - res += rule.reward_point_amount; + // In this case we count the points per rule + if (rule.reward_point_mode === "unit") { + splitPoints.push( + ...Array.apply(null, Array(totalProductQty)).map((_) => { + return { points: rule.reward_point_amount }; + }) + ); } else if (rule.reward_point_mode === "money") { - res -= round_precision( - rule.reward_point_amount * line.get_price_with_tax(), + for (const line of orderLines) { + if ( + line.is_reward_line || + !rule.valid_product_ids.has(line.get_product().id) || + line.get_quantity() <= 0 || + line.ignoreLoyaltyPoints({ program }) + ) { + continue; + } + const pointsPerUnit = round_precision( + (rule.reward_point_amount * line.get_price_with_tax()) / + line.get_quantity(), + 0.01 + ); + if (pointsPerUnit > 0) { + splitPoints.push( + ...Array.apply(null, Array(line.get_quantity())).map(() => { + if (line.giftBarcode && line.get_quantity() == 1) { + return { + points: pointsPerUnit, + barcode: line.giftBarcode, + giftCardId: line.giftCardId, + }; + } + return { points: pointsPerUnit }; + }) + ); + } + } + } + } else { + // In this case we add on to the global point count + if (rule.reward_point_mode === "order") { + points += rule.reward_point_amount; + } else if (rule.reward_point_mode === "money") { + // NOTE: unlike in sale_loyalty this performs a round half-up instead of round down + points += round_precision( + rule.reward_point_amount * orderedProductPaid, 0.01 ); } else if (rule.reward_point_mode === "unit") { - res += rule.reward_point_amount * line.get_quantity(); + points += rule.reward_point_amount * totalProductQty; } } } - return res; - } - /** - * @returns {number} The points that are left for the given coupon for this order. - */ - _getRealCouponPoints(coupon_id) { - let points = 0; - const dbCoupon = this.pos.couponCache[coupon_id]; - if (dbCoupon) { - points += dbCoupon.balance; + const res = points ? [{ points }] : []; + if (splitPoints.length) { + res.push(...splitPoints); } - Object.values(this.couponPointChanges).some((pe) => { - if (pe.coupon_id === coupon_id) { - if (this.pos.program_by_id[pe.program_id].applies_on !== "future") { - points += pe.points; - } - // couponPointChanges is not supposed to have a coupon multiple times - return true; - } - return false; - }); - for (const line of this.get_orderlines()) { - if (line.is_reward_line && line.coupon_id === coupon_id) { - points -= line.points_cost; - } - } - return points; + result[program.id] = res; } - /** - * Depending on the program type returns a new (local) instance of coupon or tries to retrieve the coupon in case of loyalty cards. - * Existing coupons are put in a cache which is also used to fetch the coupons. - * - * @param {object} program - */ - async _couponForProgram(program) { - if (program.is_nominative) { - return this.pos.fetchLoyaltyCard(program.id, this.get_partner().id); + return result; + }, + /** + * @returns {Array} List of lines composing the global discount + */ + _getGlobalDiscountLines() { + return this.get_orderlines().filter( + (line) => line.reward_id && this.pos.reward_by_id[line.reward_id].is_global_discount + ); + }, + /** + * Returns the number of product items in the order based on the given rule. + * @param {*} rule + */ + _computeNItems(rule) { + return this._get_regular_order_lines().reduce((nItems, line) => { + let increment = 0; + if (rule.any_product || rule.valid_product_ids.has(line.product.id)) { + increment = line.get_quantity(); } - // This type of coupons don't need to really exist up until validating the order, so no need to cache - return new PosLoyaltyCard( - null, - null, - program.id, - (this.get_partner() || { id: -1 }).id, - 0 - ); - } - _programIsApplicable(program) { - if ( - program.trigger === "auto" && - !program.rules.find( - (rule) => - rule.mode === "auto" || this.codeActivatedProgramRules.includes(rule.id) - ) - ) { + return nItems + increment; + }, 0); + }, + /** + * Checks whether this order is allowed to generate rewards + * from the given coupon program. + * @param {*} couponProgram + */ + _canGenerateRewards(couponProgram, orderTotalWithTax, orderTotalWithoutTax) { + for (const rule of couponProgram.rules) { + const amountToCompare = + rule.minimum_amount_tax_mode == "incl" ? orderTotalWithTax : orderTotalWithoutTax; + if (rule.minimum_amount > amountToCompare) { return false; } - if ( - program.trigger === "with_code" && - !program.rules.find((rule) => this.codeActivatedProgramRules.includes(rule.id)) - ) { + const nItems = this._computeNItems(rule); + if (rule.minimum_qty > nItems) { return false; } - if (program.is_nominative && !this.get_partner()) { - return false; - } - if (program.date_to && program.date_to <= new Date()) { - return false; - } - if (program.limit_usage && program.total_order_count >= program.max_usage) { - return false; - } - return true; } - /** - * Computes how much points each program gives. - * - * @param {Array} programs list of loyalty.program - * @returns {Object} Containing the points gained per program - */ - pointsForPrograms(programs) { - const totalTaxed = this.get_total_with_tax(); - const totalUntaxed = this.get_total_without_tax(); - const totalsPerProgram = Object.fromEntries( - programs.map((program) => [ - program.id, - { untaxed: totalUntaxed, taxed: totalTaxed }, - ]) - ); - const orderLines = this.get_orderlines(); - for (const line of orderLines) { - if (!line.reward_id) { - continue; - } - const reward = this.pos.reward_by_id[line.reward_id]; - if (reward.reward_type !== "discount") { - continue; - } - const rewardProgram = reward.program_id; - for (const program of programs) { - // Remove automatic discount and this program's discounts from the totals. - if (program.id === rewardProgram.id || rewardProgram.trigger === "auto") { - totalsPerProgram[program.id]["taxed"] -= line.get_price_with_tax(); - totalsPerProgram[program.id]["untaxed"] -= line.get_price_without_tax(); - } - } - } - const result = {}; - for (const program of programs) { - let points = 0; - const splitPoints = []; - for (const rule of program.rules) { - if ( - rule.mode === "with_code" && - !this.codeActivatedProgramRules.includes(rule.id) - ) { - continue; - } - const amountCheck = - (rule.minimum_amount_tax_mode === "incl" && - totalsPerProgram[program.id]["taxed"]) || - totalsPerProgram[program.id]["untaxed"]; - if (rule.minimum_amount > amountCheck) { - // NOTE: big doutes par rapport au fait de compter tous les produits - continue; - } - let totalProductQty = 0; - // Only count points for paid lines. - const qtyPerProduct = {}; - let orderedProductPaid = 0; - for (const line of orderLines) { - if ( - ((!line.reward_product_id && - (rule.any_product || - rule.valid_product_ids.has(line.get_product().id))) || - (line.reward_product_id && - (rule.any_product || - rule.valid_product_ids.has(line.reward_product_id)))) && - !line.ignoreLoyaltyPoints({ program }) - ) { - // We only count reward products from the same program to avoid unwanted feedback loops - if (line.reward_product_id) { - const reward = this.pos.reward_by_id[line.reward_id]; - if (program.id !== reward.program_id) { - continue; - } - } - const lineQty = line.reward_product_id - ? -line.get_quantity() - : line.get_quantity(); - totalProductQty += lineQty; - if (qtyPerProduct[line.reward_product_id || line.get_product().id]) { - qtyPerProduct[line.reward_product_id || line.get_product().id] += - lineQty; - } else { - qtyPerProduct[line.reward_product_id || line.get_product().id] = - lineQty; - } - orderedProductPaid += line.get_price_with_tax(); - } - } - if (totalProductQty < rule.minimum_qty) { - // Should also count the points from negative quantities. - // For example, when refunding an ewallet payment. See TicketScreen override in this addon. - continue; - } - if ( - program.applies_on === "future" && - rule.reward_point_split && - rule.reward_point_mode !== "order" - ) { - // In this case we count the points per rule - if (rule.reward_point_mode === "unit") { - splitPoints.push( - ...Array.apply(null, Array(totalProductQty)).map((_) => { - return { points: rule.reward_point_amount }; - }) - ); - } else if (rule.reward_point_mode === "money") { - for (const line of orderLines) { - if ( - line.is_reward_line || - !rule.valid_product_ids.has(line.get_product().id) || - line.get_quantity() <= 0 || - line.ignoreLoyaltyPoints({ program }) - ) { - continue; - } - const pointsPerUnit = round_precision( - (rule.reward_point_amount * line.get_price_with_tax()) / - line.get_quantity(), - 0.01 - ); - if (pointsPerUnit > 0) { - splitPoints.push( - ...Array.apply(null, Array(line.get_quantity())).map(() => { - if (line.giftBarcode && line.get_quantity() == 1) { - return { - points: pointsPerUnit, - barcode: line.giftBarcode, - giftCardId: line.giftCardId, - }; - } - return { points: pointsPerUnit }; - }) - ); - } - } - } - } else { - // In this case we add on to the global point count - if (rule.reward_point_mode === "order") { - points += rule.reward_point_amount; - } else if (rule.reward_point_mode === "money") { - // NOTE: unlike in sale_loyalty this performs a round half-up instead of round down - points += round_precision( - rule.reward_point_amount * orderedProductPaid, - 0.01 - ); - } else if (rule.reward_point_mode === "unit") { - points += rule.reward_point_amount * totalProductQty; - } - } - } - const res = points ? [{ points }] : []; - if (splitPoints.length) { - res.push(...splitPoints); - } - result[program.id] = res; - } - return result; - } - /** - * @returns {Array} List of lines composing the global discount - */ - _getGlobalDiscountLines() { - return this.get_orderlines().filter( - (line) => line.reward_id && this.pos.reward_by_id[line.reward_id].is_global_discount - ); - } - /** - * Returns the number of product items in the order based on the given rule. - * @param {*} rule - */ - _computeNItems(rule) { - return this._get_regular_order_lines().reduce((nItems, line) => { - let increment = 0; - if (rule.any_product || rule.valid_product_ids.has(line.product.id)) { - increment = line.get_quantity(); - } - return nItems + increment; - }, 0); - } - /** - * Checks whether this order is allowed to generate rewards - * from the given coupon program. - * @param {*} couponProgram - */ - _canGenerateRewards(couponProgram, orderTotalWithTax, orderTotalWithoutTax) { - for (const rule of couponProgram.rules) { - const amountToCompare = - rule.minimum_amount_tax_mode == "incl" - ? orderTotalWithTax - : orderTotalWithoutTax; - if (rule.minimum_amount > amountToCompare) { - return false; - } - const nItems = this._computeNItems(rule); - if (rule.minimum_qty > nItems) { - return false; - } - } - return true; - } - /** - * @param {Integer} coupon_id (optional) Coupon id - * @param {Integer} program_id (optional) Program id - * @returns {Array} List of {Object} containing the coupon_id and reward keys - */ - getClaimableRewards(coupon_id = false, program_id = false, auto = false) { - const allCouponPrograms = Object.values(this.couponPointChanges) - .map((pe) => { + return true; + }, + /** + * @param {Integer} coupon_id (optional) Coupon id + * @param {Integer} program_id (optional) Program id + * @returns {Array} List of {Object} containing the coupon_id and reward keys + */ + getClaimableRewards(coupon_id = false, program_id = false, auto = false) { + const allCouponPrograms = Object.values(this.couponPointChanges) + .map((pe) => { + return { + program_id: pe.program_id, + coupon_id: pe.coupon_id, + }; + }) + .concat( + this.codeActivatedCoupons.map((coupon) => { return { - program_id: pe.program_id, - coupon_id: pe.coupon_id, + program_id: coupon.program_id, + coupon_id: coupon.id, }; }) - .concat( - this.codeActivatedCoupons.map((coupon) => { - return { - program_id: coupon.program_id, - coupon_id: coupon.id, - }; - }) - ); - const result = []; - const totalWithTax = this.get_total_with_tax(); - const totalWithoutTax = this.get_total_without_tax(); - const totalIsZero = totalWithTax === 0; - const globalDiscountLines = this._getGlobalDiscountLines(); - const globalDiscountPercent = globalDiscountLines.length - ? this.pos.reward_by_id[globalDiscountLines[0].reward_id].discount - : 0; - for (const couponProgram of allCouponPrograms) { - const program = this.pos.program_by_id[couponProgram.program_id]; - if (program.trigger == "with_code") { - // For coupon programs, the rules become conditions. - // Points to purchase rewards will only come from the scanned coupon. - if (!this._canGenerateRewards(program, totalWithTax, totalWithoutTax)) { - continue; - } - } - if ( - (coupon_id && couponProgram.coupon_id !== coupon_id) || - (program_id && couponProgram.program_id !== program_id) - ) { + ); + const result = []; + const totalWithTax = this.get_total_with_tax(); + const totalWithoutTax = this.get_total_without_tax(); + const totalIsZero = totalWithTax === 0; + const globalDiscountLines = this._getGlobalDiscountLines(); + const globalDiscountPercent = globalDiscountLines.length + ? this.pos.reward_by_id[globalDiscountLines[0].reward_id].discount + : 0; + for (const couponProgram of allCouponPrograms) { + const program = this.pos.program_by_id[couponProgram.program_id]; + if (program.trigger == "with_code") { + // For coupon programs, the rules become conditions. + // Points to purchase rewards will only come from the scanned coupon. + if (!this._canGenerateRewards(program, totalWithTax, totalWithoutTax)) { continue; } - const points = this._getRealCouponPoints(couponProgram.coupon_id); - for (const reward of program.rewards) { - if (points < reward.required_points) { + } + if ( + (coupon_id && couponProgram.coupon_id !== coupon_id) || + (program_id && couponProgram.program_id !== program_id) + ) { + continue; + } + const points = this._getRealCouponPoints(couponProgram.coupon_id); + for (const reward of program.rewards) { + if (points < reward.required_points) { + continue; + } + if (auto && this.disabledRewards.has(reward.id)) { + continue; + } + // Try to filter out rewards that will not be claimable anyway. + if (reward.is_global_discount && reward.discount <= globalDiscountPercent) { + continue; + } + if (reward.reward_type === "discount" && totalIsZero) { + continue; + } + if (reward.reward_type === "product" && !reward.multi_product) { + const product = this.pos.db.get_product_by_id(reward.reward_product_ids[0]); + const unclaimedQty = this._computeUnclaimedFreeProductQty( + reward, + couponProgram.coupon_id, + product, + points + ); + if (unclaimedQty <= 0) { continue; } - if (auto && this.disabledRewards.has(reward.id)) { + } + result.push({ + coupon_id: couponProgram.coupon_id, + reward: reward, + }); + } + } + return result; + }, + /** + * Returns the reward such that when its reward product is added + * in the order, it will be added as free. That is, when added, + * it comes with the corresponding reward product line. + */ + getPotentialFreeProductRewards() { + const allCouponPrograms = Object.values(this.couponPointChanges) + .map((pe) => { + return { + program_id: pe.program_id, + coupon_id: pe.coupon_id, + }; + }) + .concat( + this.codeActivatedCoupons.map((coupon) => { + return { + program_id: coupon.program_id, + coupon_id: coupon.id, + }; + }) + ); + const result = []; + for (const couponProgram of allCouponPrograms) { + const program = this.pos.program_by_id[couponProgram.program_id]; + const points = this._getRealCouponPoints(couponProgram.coupon_id); + const hasLine = this.orderlines.filter((line) => !line.is_reward_line).length > 0; + for (const reward of program.rewards.filter( + (reward) => reward.reward_type == "product" + )) { + if (points < reward.required_points) { + continue; + } + // Loyalty program (applies_on == 'both') should needs an orderline before it can apply a reward. + const considerTheReward = + program.applies_on !== "both" || (program.applies_on == "both" && hasLine); + if (reward.reward_type === "product" && considerTheReward) { + const product = this.pos.db.get_product_by_id(reward.reward_product_ids[0]); + const potentialQty = this._computePotentialFreeProductQty( + reward, + product, + points + ); + if (potentialQty <= 0) { continue; } - // Try to filter out rewards that will not be claimable anyway. - if (reward.is_global_discount && reward.discount <= globalDiscountPercent) { - continue; - } - if (reward.reward_type === "discount" && totalIsZero) { - continue; - } - if (reward.reward_type === "product" && !reward.multi_product) { - const product = this.pos.db.get_product_by_id(reward.reward_product_ids[0]); - const unclaimedQty = this._computeUnclaimedFreeProductQty( - reward, - couponProgram.coupon_id, - product, - points - ); - if (unclaimedQty <= 0) { - continue; - } - } result.push({ coupon_id: couponProgram.coupon_id, reward: reward, + potentialQty, }); } } - return result; } - /** - * Returns the reward such that when its reward product is added - * in the order, it will be added as free. That is, when added, - * it comes with the corresponding reward product line. - */ - getPotentialFreeProductRewards() { - const allCouponPrograms = Object.values(this.couponPointChanges) - .map((pe) => { - return { - program_id: pe.program_id, - coupon_id: pe.coupon_id, - }; - }) - .concat( - this.codeActivatedCoupons.map((coupon) => { - return { - program_id: coupon.program_id, - coupon_id: coupon.id, - }; - }) - ); - const result = []; - for (const couponProgram of allCouponPrograms) { - const program = this.pos.program_by_id[couponProgram.program_id]; - const points = this._getRealCouponPoints(couponProgram.coupon_id); - const hasLine = this.orderlines.filter((line) => !line.is_reward_line).length > 0; - for (const reward of program.rewards.filter( - (reward) => reward.reward_type == "product" - )) { - if (points < reward.required_points) { + return result; + }, + /** + * Applies a reward to the order, `_updateRewards` is expected to be called right after. + * + * @param {loyalty.reward} reward + * @param {Integer} coupon_id + * @param {Object} args Reward options + * @returns True if everything went right or an error message + */ + _applyReward(reward, coupon_id, args) { + if (this._getRealCouponPoints(coupon_id) < reward.required_points) { + return _t("There are not enough points on the coupon to claim this reward."); + } + if (reward.is_global_discount) { + const globalDiscountLines = this._getGlobalDiscountLines(); + if (globalDiscountLines.length) { + const rewardId = globalDiscountLines[0].reward_id; + if ( + rewardId != reward.id && + this.pos.reward_by_id[rewardId].discount >= reward.discount + ) { + return _t("A better global discount is already applied."); + } else if (rewardId != rewardId.id) { + for (const line of globalDiscountLines) { + this.orderlines.remove(line); + } + } + } + } + args = args || {}; + const rewardLines = this._getRewardLineValues({ + reward: reward, + coupon_id: coupon_id, + product: args["product"] || null, + }); + if (!Array.isArray(rewardLines)) { + return rewardLines; // Returned an error. + } + if (!rewardLines.length) { + return _t("The reward could not be applied."); + } + for (const rewardLine of rewardLines) { + this.orderlines.add(this._createLineFromVals(rewardLine)); + } + return true; + }, + _createLineFromVals(vals) { + vals["lst_price"] = vals["price"]; + const line = new Orderline({}, { pos: this.pos, order: this, product: vals["product"] }); + this.fix_tax_included_price(line); + this.set_orderline_options(line, vals); + return line; + }, + /** + * @param {loyalty.reward} reward + * @returns the discountable and discountable per tax for this discount on order reward. + */ + _getDiscountableOnOrder(reward) { + let discountable = 0; + const discountablePerTax = {}; + for (const line of this.get_orderlines()) { + if (!line.get_quantity()) { + continue; + } + const taxKey = line.get_taxes().map((t) => t.id); + discountable += line.get_price_with_tax(); + if (!discountablePerTax[taxKey]) { + discountablePerTax[taxKey] = 0; + } + discountablePerTax[taxKey] += line.get_base_price(); + } + return { discountable, discountablePerTax }; + }, + /** + * @returns the order's cheapest line + */ + _getCheapestLine() { + let cheapestLine; + for (const line of this.get_orderlines()) { + if (line.reward_id || !line.get_quantity()) { + continue; + } + if (!cheapestLine || cheapestLine.price > line.price) { + cheapestLine = line; + } + } + return cheapestLine; + }, + /** + * @param {loyalty.reward} reward + * @returns the discountable and discountable per tax for this discount on cheapest reward. + */ + _getDiscountableOnCheapest(reward) { + const cheapestLine = this._getCheapestLine(); + if (!cheapestLine) { + return { discountable: 0, discountablePerTax: {} }; + } + const taxKey = cheapestLine.get_taxes().map((t) => t.id); + return { + discountable: cheapestLine.price, + discountablePerTax: Object.fromEntries([[taxKey, cheapestLine.price]]), + }; + }, + /** + * @param {loyalty.reward} reward + * @returns all lines to which the reward applies. + */ + _getSpecificDiscountableLines(reward) { + const discountableLines = []; + const applicableProducts = reward.all_discount_product_ids; + for (const line of this.get_orderlines()) { + if (!line.get_quantity()) { + continue; + } + if ( + applicableProducts.has(line.get_product().id) || + applicableProducts.has(line.reward_product_id) + ) { + discountableLines.push(line); + } + } + return discountableLines; + }, + /** + * For a 'specific' type of discount it is more complicated as we have to make sure that we never + * discount more than what is available on a per line basis. + * @param {loyalty.reward} reward + * @returns the discountable and discountable per tax for this discount on specific reward. + */ + _getDiscountableOnSpecific(reward) { + const applicableProducts = reward.all_discount_product_ids; + const linesToDiscount = []; + const discountLinesPerReward = {}; + const orderLines = this.get_orderlines(); + const remainingAmountPerLine = {}; + for (const line of orderLines) { + if (!line.get_quantity() || !line.price) { + continue; + } + remainingAmountPerLine[line.cid] = line.get_price_with_tax(); + if ( + applicableProducts.has(line.get_product().id) || + (line.reward_product_id && applicableProducts.has(line.reward_product_id)) + ) { + linesToDiscount.push(line); + } else if (line.reward_id) { + const lineReward = this.pos.reward_by_id[line.reward_id]; + if (lineReward.id === reward.id) { + continue; + } + if (!discountLinesPerReward[line.reward_identifier_code]) { + discountLinesPerReward[line.reward_identifier_code] = []; + } + discountLinesPerReward[line.reward_identifier_code].push(line); + } + } + + let cheapestLine = false; + for (const lines of Object.values(discountLinesPerReward)) { + const lineReward = this.pos.reward_by_id[lines[0].reward_id]; + let discountedLines = orderLines; + if (lineReward.discount_applicability === "cheapest") { + cheapestLine = cheapestLine || this._getCheapestLine(); + discountedLines = [cheapestLine]; + } else if (lineReward.discount_applicability === "specific") { + discountedLines = this._getSpecificDiscountableLines(lineReward); + } + if (!discountedLines.length) { + continue; + } + const commonLines = linesToDiscount.filter((line) => discountedLines.includes(line)); + if (lineReward.discount_mode === "percent") { + const discount = lineReward.discount / 100; + for (const line of discountedLines) { + if (line.reward_id) { continue; } - // Loyalty program (applies_on == 'both') should needs an orderline before it can apply a reward. - const considerTheReward = - program.applies_on !== "both" || (program.applies_on == "both" && hasLine); - if (reward.reward_type === "product" && considerTheReward) { - const product = this.pos.db.get_product_by_id(reward.reward_product_ids[0]); - const potentialQty = this._computePotentialFreeProductQty( - reward, - product, - points - ); - if (potentialQty <= 0) { - continue; - } - result.push({ - coupon_id: couponProgram.coupon_id, - reward: reward, - potentialQty, - }); + if (lineReward.discount_applicability === "cheapest") { + remainingAmountPerLine[line.cid] *= 1 - discount / line.get_quantity(); + } else { + remainingAmountPerLine[line.cid] *= 1 - discount; } } - } - return result; - } - /** - * Applies a reward to the order, `_updateRewards` is expected to be called right after. - * - * @param {loyalty.reward} reward - * @param {Integer} coupon_id - * @param {Object} args Reward options - * @returns True if everything went right or an error message - */ - _applyReward(reward, coupon_id, args) { - if (this._getRealCouponPoints(coupon_id) < reward.required_points) { - return _t("There are not enough points on the coupon to claim this reward."); - } - if (reward.is_global_discount) { - const globalDiscountLines = this._getGlobalDiscountLines(); - if (globalDiscountLines.length) { - const rewardId = globalDiscountLines[0].reward_id; - if ( - rewardId != reward.id && - this.pos.reward_by_id[rewardId].discount >= reward.discount - ) { - return _t("A better global discount is already applied."); - } else if (rewardId != rewardId.id) { - for (const line of globalDiscountLines) { - this.orderlines.remove(line); - } - } - } - } - args = args || {}; - const rewardLines = this._getRewardLineValues({ - reward: reward, - coupon_id: coupon_id, - product: args["product"] || null, - }); - if (!Array.isArray(rewardLines)) { - return rewardLines; // Returned an error. - } - if (!rewardLines.length) { - return _t("The reward could not be applied."); - } - for (const rewardLine of rewardLines) { - this.orderlines.add(this._createLineFromVals(rewardLine)); - } - return true; - } - _createLineFromVals(vals) { - vals["lst_price"] = vals["price"]; - const line = Orderline.create( - {}, - { pos: this.pos, order: this, product: vals["product"] } - ); - this.fix_tax_included_price(line); - this.set_orderline_options(line, vals); - return line; - } - /** - * @param {loyalty.reward} reward - * @returns the discountable and discountable per tax for this discount on order reward. - */ - _getDiscountableOnOrder(reward) { - let discountable = 0; - const discountablePerTax = {}; - for (const line of this.get_orderlines()) { - if (!line.get_quantity()) { - continue; - } - const taxKey = line.get_taxes().map((t) => t.id); - discountable += line.get_price_with_tax(); - if (!discountablePerTax[taxKey]) { - discountablePerTax[taxKey] = 0; - } - discountablePerTax[taxKey] += line.get_base_price(); - } - return { discountable, discountablePerTax }; - } - /** - * @returns the order's cheapest line - */ - _getCheapestLine() { - let cheapestLine; - for (const line of this.get_orderlines()) { - if (line.reward_id || !line.get_quantity()) { - continue; - } - if (!cheapestLine || cheapestLine.price > line.price) { - cheapestLine = line; - } - } - return cheapestLine; - } - /** - * @param {loyalty.reward} reward - * @returns the discountable and discountable per tax for this discount on cheapest reward. - */ - _getDiscountableOnCheapest(reward) { - const cheapestLine = this._getCheapestLine(); - if (!cheapestLine) { - return { discountable: 0, discountablePerTax: {} }; - } - const taxKey = cheapestLine.get_taxes().map((t) => t.id); - return { - discountable: cheapestLine.price, - discountablePerTax: Object.fromEntries([[taxKey, cheapestLine.price]]), - }; - } - /** - * @param {loyalty.reward} reward - * @returns all lines to which the reward applies. - */ - _getSpecificDiscountableLines(reward) { - const discountableLines = []; - const applicableProducts = reward.all_discount_product_ids; - for (const line of this.get_orderlines()) { - if (!line.get_quantity()) { - continue; - } - if ( - applicableProducts.has(line.get_product().id) || - applicableProducts.has(line.reward_product_id) - ) { - discountableLines.push(line); - } - } - return discountableLines; - } - /** - * For a 'specific' type of discount it is more complicated as we have to make sure that we never - * discount more than what is available on a per line basis. - * @param {loyalty.reward} reward - * @returns the discountable and discountable per tax for this discount on specific reward. - */ - _getDiscountableOnSpecific(reward) { - const applicableProducts = reward.all_discount_product_ids; - const linesToDiscount = []; - const discountLinesPerReward = {}; - const orderLines = this.get_orderlines(); - const remainingAmountPerLine = {}; - for (const line of orderLines) { - if (!line.get_quantity() || !line.price) { - continue; - } - remainingAmountPerLine[line.cid] = line.get_price_with_tax(); - if ( - applicableProducts.has(line.get_product().id) || - (line.reward_product_id && applicableProducts.has(line.reward_product_id)) - ) { - linesToDiscount.push(line); - } else if (line.reward_id) { - const lineReward = this.pos.reward_by_id[line.reward_id]; - if (lineReward.id === reward.id) { - continue; - } - if (!discountLinesPerReward[line.reward_identifier_code]) { - discountLinesPerReward[line.reward_identifier_code] = []; - } - discountLinesPerReward[line.reward_identifier_code].push(line); - } - } - - let cheapestLine = false; - for (const lines of Object.values(discountLinesPerReward)) { - const lineReward = this.pos.reward_by_id[lines[0].reward_id]; - let discountedLines = orderLines; - if (lineReward.discount_applicability === "cheapest") { - cheapestLine = cheapestLine || this._getCheapestLine(); - discountedLines = [cheapestLine]; - } else if (lineReward.discount_applicability === "specific") { - discountedLines = this._getSpecificDiscountableLines(lineReward); - } - if (!discountedLines.length) { - continue; - } - const commonLines = linesToDiscount.filter((line) => - discountedLines.includes(line) + } else { + const nonCommonLines = discountedLines.filter( + (line) => !linesToDiscount.includes(line) ); - if (lineReward.discount_mode === "percent") { - const discount = lineReward.discount / 100; - for (const line of discountedLines) { - if (line.reward_id) { - continue; - } - if (lineReward.discount_applicability === "cheapest") { - remainingAmountPerLine[line.cid] *= 1 - discount / line.get_quantity(); - } else { - remainingAmountPerLine[line.cid] *= 1 - discount; - } + const discountedAmounts = lines.reduce((map, line) => { + map[line.get_taxes().map((t) => t.id)]; + return map; + }, {}); + const process = (line) => { + const key = line.get_taxes().map((t) => t.id); + if (!discountedAmounts[key] || line.reward_id) { + return; } - } else { - const nonCommonLines = discountedLines.filter( - (line) => !linesToDiscount.includes(line) - ); - const discountedAmounts = lines.reduce((map, line) => { - map[line.get_taxes().map((t) => t.id)]; - return map; - }, {}); - const process = (line) => { - const key = line.get_taxes().map((t) => t.id); - if (!discountedAmounts[key] || line.reward_id) { - return; - } - const remaining = remainingAmountPerLine[line.cid]; - const consumed = Math.min(remaining, discountedAmounts[key]); - discountedAmounts[key] -= consumed; - remainingAmountPerLine[line.cid] -= consumed; - }; - nonCommonLines.forEach(process); - commonLines.forEach(process); - } + const remaining = remainingAmountPerLine[line.cid]; + const consumed = Math.min(remaining, discountedAmounts[key]); + discountedAmounts[key] -= consumed; + remainingAmountPerLine[line.cid] -= consumed; + }; + nonCommonLines.forEach(process); + commonLines.forEach(process); } - - let discountable = 0; - const discountablePerTax = {}; - for (const line of linesToDiscount) { - discountable += remainingAmountPerLine[line.cid]; - const taxKey = line.get_taxes().map((t) => t.id); - if (!discountablePerTax[taxKey]) { - discountablePerTax[taxKey] = 0; - } - discountablePerTax[taxKey] += - line.get_base_price() * - (remainingAmountPerLine[line.cid] / line.get_price_with_tax()); - } - return { discountable, discountablePerTax }; } - /** - * @param {Object} args See `_applyReward` - * @returns {Array} List of values to create the reward lines - */ - _getRewardLineValues(args) { - const reward = args["reward"]; - if (reward.reward_type === "discount") { - return this._getRewardLineValuesDiscount(args); - } else if (reward.reward_type === "product") { - return this._getRewardLineValuesProduct(args); + + let discountable = 0; + const discountablePerTax = {}; + for (const line of linesToDiscount) { + discountable += remainingAmountPerLine[line.cid]; + const taxKey = line.get_taxes().map((t) => t.id); + if (!discountablePerTax[taxKey]) { + discountablePerTax[taxKey] = 0; } - // NOTE: we may reach this step if for some reason there is a free shipping reward + discountablePerTax[taxKey] += + line.get_base_price() * + (remainingAmountPerLine[line.cid] / line.get_price_with_tax()); + } + return { discountable, discountablePerTax }; + }, + /** + * @param {Object} args See `_applyReward` + * @returns {Array} List of values to create the reward lines + */ + _getRewardLineValues(args) { + const reward = args["reward"]; + if (reward.reward_type === "discount") { + return this._getRewardLineValuesDiscount(args); + } else if (reward.reward_type === "product") { + return this._getRewardLineValuesProduct(args); + } + // NOTE: we may reach this step if for some reason there is a free shipping reward + return []; + }, + /** + * @param {Object} args See `_applyReward` + * @returns {Array} List of values to create the discount lines + */ + _getRewardLineValuesDiscount(args) { + const reward = args["reward"]; + const coupon_id = args["coupon_id"]; + const rewardAppliesTo = reward.discount_applicability; + let getDiscountable; + if (rewardAppliesTo === "order") { + getDiscountable = this._getDiscountableOnOrder.bind(this); + } else if (rewardAppliesTo === "cheapest") { + getDiscountable = this._getDiscountableOnCheapest.bind(this); + } else if (rewardAppliesTo === "specific") { + getDiscountable = this._getDiscountableOnSpecific.bind(this); + } + if (!getDiscountable) { + return _t("Unknown discount type"); + } + let { discountable, discountablePerTax } = getDiscountable(reward); + discountable = Math.min(this.get_total_with_tax(), discountable); + if (!discountable) { return []; } - /** - * @param {Object} args See `_applyReward` - * @returns {Array} List of values to create the discount lines - */ - _getRewardLineValuesDiscount(args) { - const reward = args["reward"]; - const coupon_id = args["coupon_id"]; - const rewardAppliesTo = reward.discount_applicability; - let getDiscountable; - if (rewardAppliesTo === "order") { - getDiscountable = this._getDiscountableOnOrder.bind(this); - } else if (rewardAppliesTo === "cheapest") { - getDiscountable = this._getDiscountableOnCheapest.bind(this); - } else if (rewardAppliesTo === "specific") { - getDiscountable = this._getDiscountableOnSpecific.bind(this); - } - if (!getDiscountable) { - return _t("Unknown discount type"); - } - let { discountable, discountablePerTax } = getDiscountable(reward); - discountable = Math.min(this.get_total_with_tax(), discountable); - if (!discountable) { - return []; - } - let maxDiscount = reward.discount_max_amount || Infinity; - if (reward.discount_mode === "per_point") { - maxDiscount = Math.min( - maxDiscount, - reward.discount * this._getRealCouponPoints(coupon_id) - ); - } else if (reward.discount_mode === "per_order") { - maxDiscount = Math.min(maxDiscount, reward.discount); - } else if (reward.discount_mode === "percent") { - maxDiscount = Math.min(maxDiscount, discountable * (reward.discount / 100)); - } - const rewardCode = _newRandomRewardCode(); - let pointCost = reward.clear_wallet - ? this._getRealCouponPoints(coupon_id) - : reward.required_points; - if (reward.discount_mode === "per_point" && !reward.clear_wallet) { - pointCost = Math.min(maxDiscount, discountable) / reward.discount; - } - // These are considered payments and do not require to be either taxed or split by tax - const discountProduct = reward.discount_line_product_id; - if (["ewallet", "gift_card"].includes(reward.program_id.program_type)) { - return [ - { - product: discountProduct, - price: -Math.min(maxDiscount, discountable), - quantity: 1, - reward_id: reward.id, - is_reward_line: true, - coupon_id: coupon_id, - points_cost: pointCost, - reward_identifier_code: rewardCode, - merge: false, - }, - ]; - } - const discountFactor = discountable ? Math.min(1, maxDiscount / discountable) : 1; - const result = Object.entries(discountablePerTax).reduce((lst, entry) => { - // Ignore 0 price lines - if (!entry[1]) { - return; - } - const taxIds = - entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str)); - lst.push({ + let maxDiscount = reward.discount_max_amount || Infinity; + if (reward.discount_mode === "per_point") { + maxDiscount = Math.min( + maxDiscount, + reward.discount * this._getRealCouponPoints(coupon_id) + ); + } else if (reward.discount_mode === "per_order") { + maxDiscount = Math.min(maxDiscount, reward.discount); + } else if (reward.discount_mode === "percent") { + maxDiscount = Math.min(maxDiscount, discountable * (reward.discount / 100)); + } + const rewardCode = _newRandomRewardCode(); + let pointCost = reward.clear_wallet + ? this._getRealCouponPoints(coupon_id) + : reward.required_points; + if (reward.discount_mode === "per_point" && !reward.clear_wallet) { + pointCost = Math.min(maxDiscount, discountable) / reward.discount; + } + // These are considered payments and do not require to be either taxed or split by tax + const discountProduct = reward.discount_line_product_id; + if (["ewallet", "gift_card"].includes(reward.program_id.program_type)) { + return [ + { product: discountProduct, - price: -(entry[1] * discountFactor), + price: -Math.min(maxDiscount, discountable), quantity: 1, reward_id: reward.id, is_reward_line: true, coupon_id: coupon_id, - points_cost: 0, + points_cost: pointCost, reward_identifier_code: rewardCode, - tax_ids: taxIds, - merge: false, - }); - return lst; - }, []); - if (result.length) { - result[0]["points_cost"] = pointCost; - } - return result; - } - _isRewardProductPartOfRules(reward, product) { - return ( - reward.program_id.rules.filter( - (rule) => rule.any_product || rule.valid_product_ids.has(product.id) - ).length > 0 - ); - } - /** - * Tries to compute how many free product can be given out for the given product. - * Contrary to sale_loyalty, the product must be in the order lines in order to give it out - * (resulting in discount lines for the product's value). - * As such we need to approximate the effect of removing 1 quantity on the counting of points in order - * to avoid feedback loops between giving a product and it removing the required points for it. - * - * @param {loyalty.reward} reward - * @param {Integer} coupon_id - * @param {Product} product - * @returns {Integer} Available quantity to be given as reward for the given product - */ - _computeUnclaimedFreeProductQty(reward, coupon_id, product, remainingPoints) { - let claimed = 0; - let available = 0; - let shouldCorrectRemainingPoints = false; - for (const line of this.get_orderlines()) { - if (line.get_product().id === product.id) { - available += line.get_quantity(); - } else if (line.reward_product_id === product.id) { - if (line.reward_id == reward.id) { - claimed += line.get_quantity(); - } else { - shouldCorrectRemainingPoints = true; - } - } - } - let freeQty; - if (reward.program_id.trigger == "auto") { - if (this._isRewardProductPartOfRules(reward, product)) { - // OPTIMIZATION: Pre-calculate the factors for each reward-product combination during the loading. - // For points not based on quantity, need to normalize the points to compute free quantity. - const factor = reward.program_id.rules.reduce((result, rule) => { - if (rule.any_product || rule.valid_product_ids.has(product.id)) { - if (rule.reward_point_mode === "order") { - result += rule.reward_point_amount; - } else if (rule.reward_point_mode === "money") { - result += round_precision( - rule.reward_point_amount * product.lst_price, - 0.01 - ); - } else if (rule.reward_point_mode === "unit") { - result += rule.reward_point_amount; - } - } - return result; - }, 0); - const correction = shouldCorrectRemainingPoints - ? this._getPointsCorrection(reward.program_id) - : 0; - freeQty = computeFreeQuantity( - (remainingPoints - correction) / factor, - reward.required_points / factor, - reward.reward_product_qty - ); - } else { - freeQty = Math.floor( - (remainingPoints / reward.required_points) * reward.reward_product_qty - ); - } - } else if (reward.program_id.trigger == "with_code") { - freeQty = Math.floor( - (remainingPoints / reward.required_points) * reward.reward_product_qty - ); - } - return Math.min(available, freeQty) - claimed; - } - _computePotentialFreeProductQty(reward, product, remainingPoints) { - if (reward.program_id.trigger == "auto") { - if (this._isRewardProductPartOfRules(reward, product)) { - const line = this.get_orderlines().find( - (line) => line.reward_product_id === product.id - ); - // Compute the correction points once even if there are multiple reward lines. - // This is because _getPointsCorrection is taking into account all the lines already. - const claimedPoints = line ? this._getPointsCorrection(reward.program_id) : 0; - return Math.floor( - ((remainingPoints - claimedPoints) / reward.required_points) * - reward.reward_product_qty - ); - } else { - return Math.floor( - (remainingPoints / reward.required_points) * reward.reward_product_qty - ); - } - } else if (reward.program_id.trigger == "with_code") { - return Math.floor( - (remainingPoints / reward.required_points) * reward.reward_product_qty - ); - } - } - /** - * @param {Object} args See `_applyReward` - * @returns {Array} List of values to create the reward lines - */ - _getRewardLineValuesProduct(args) { - const reward = args["reward"]; - const product = this.pos.db.get_product_by_id( - args["product"] || reward.reward_product_ids[0] - ); - const points = this._getRealCouponPoints(args["coupon_id"]); - const unclaimedQty = this._computeUnclaimedFreeProductQty( - reward, - args["coupon_id"], - product, - points - ); - if (unclaimedQty <= 0) { - return _t("There are not enough products in the basket to claim this reward."); - } - const claimable_count = reward.clear_wallet - ? 1 - : Math.min( - Math.ceil(unclaimedQty / reward.reward_product_qty), - Math.floor(points / reward.required_points) - ); - const cost = reward.clear_wallet ? points : claimable_count * reward.required_points; - // In case the reward is the product multiple times, give it as many times as possible - const freeQuantity = Math.min( - unclaimedQty, - reward.reward_product_qty * claimable_count - ); - return [ - { - product: reward.discount_line_product_id, - price: -round_decimals( - product.get_price(this.pricelist, freeQuantity), - this.pos.currency.decimal_places - ), - tax_ids: product.taxes_id, - quantity: freeQuantity, - reward_id: reward.id, - is_reward_line: true, - reward_product_id: product.id, - coupon_id: args["coupon_id"], - points_cost: cost, - reward_identifier_code: _newRandomRewardCode(), merge: false, }, ]; } - /** - * Full routine for activating a code for the order. - * If only one more reward is claimable after activating the code, that reward is claimed - * directly, to avoid more steps than necessary. - * If more rewards are claimable, the employee will have to manually select the reward - * in the reward selection menu. - * - * @param {String} code - * @returns true if everything went right, error message if not. - */ - async _activateCode(code) { - const rule = this.pos.rules.find((rule) => { - return ( - rule.mode === "with_code" && (rule.promo_barcode === code || rule.code === code) - ); + const discountFactor = discountable ? Math.min(1, maxDiscount / discountable) : 1; + const result = Object.entries(discountablePerTax).reduce((lst, entry) => { + // Ignore 0 price lines + if (!entry[1]) { + return; + } + const taxIds = entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str)); + lst.push({ + product: discountProduct, + price: -(entry[1] * discountFactor), + quantity: 1, + reward_id: reward.id, + is_reward_line: true, + coupon_id: coupon_id, + points_cost: 0, + reward_identifier_code: rewardCode, + tax_ids: taxIds, + merge: false, }); - let claimableRewards = null; - if (rule) { - if (this.codeActivatedProgramRules.includes(rule.id)) { - return _t("That promo code program has already been activated."); + return lst; + }, []); + if (result.length) { + result[0]["points_cost"] = pointCost; + } + return result; + }, + _isRewardProductPartOfRules(reward, product) { + return ( + reward.program_id.rules.filter( + (rule) => rule.any_product || rule.valid_product_ids.has(product.id) + ).length > 0 + ); + }, + /** + * Tries to compute how many free product can be given out for the given product. + * Contrary to sale_loyalty, the product must be in the order lines in order to give it out + * (resulting in discount lines for the product's value). + * As such we need to approximate the effect of removing 1 quantity on the counting of points in order + * to avoid feedback loops between giving a product and it removing the required points for it. + * + * @param {loyalty.reward} reward + * @param {Integer} coupon_id + * @param {Product} product + * @returns {Integer} Available quantity to be given as reward for the given product + */ + _computeUnclaimedFreeProductQty(reward, coupon_id, product, remainingPoints) { + let claimed = 0; + let available = 0; + let shouldCorrectRemainingPoints = false; + for (const line of this.get_orderlines()) { + if (line.get_product().id === product.id) { + available += line.get_quantity(); + } else if (line.reward_product_id === product.id) { + if (line.reward_id == reward.id) { + claimed += line.get_quantity(); + } else { + shouldCorrectRemainingPoints = true; } - this.codeActivatedProgramRules.push(rule.id); - await this._updateLoyaltyPrograms(); - claimableRewards = this.getClaimableRewards(false, rule.program_id.id); - } else { - if (this.codeActivatedCoupons.find((coupon) => coupon.code === code)) { - return _t("That coupon code has already been scanned and activated."); - } - const customer = this.get_partner(); - const { successful, payload } = await this.pos.env.services.rpc({ - model: "pos.config", - method: "use_coupon_code", - args: [ - [this.pos.config.id], - code, - this.creation_date, - customer ? customer.id : false, - ], - kwargs: { context: session.user_context }, - }); - if (successful) { - // Allow rejecting a gift card that is not yet paid. - const program = this.pos.program_by_id[payload.program_id]; - if ( - program && - program.program_type === "gift_card" && - !payload.has_source_order - ) { - const { confirmed } = await Gui.showPopup("ConfirmPopup", { - title: _t("Unpaid gift card"), - body: _t( - "This gift card is not linked to any order. Do you really want to apply its reward?" - ), - }); - if (!confirmed) { - return _t("Unpaid gift card rejected."); + } + } + let freeQty; + if (reward.program_id.trigger == "auto") { + if (this._isRewardProductPartOfRules(reward, product)) { + // OPTIMIZATION: Pre-calculate the factors for each reward-product combination during the loading. + // For points not based on quantity, need to normalize the points to compute free quantity. + const factor = reward.program_id.rules.reduce((result, rule) => { + if (rule.any_product || rule.valid_product_ids.has(product.id)) { + if (rule.reward_point_mode === "order") { + result += rule.reward_point_amount; + } else if (rule.reward_point_mode === "money") { + result += round_precision( + rule.reward_point_amount * product.lst_price, + 0.01 + ); + } else if (rule.reward_point_mode === "unit") { + result += rule.reward_point_amount; } } - const coupon = new PosLoyaltyCard( - code, - payload.coupon_id, - payload.program_id, - payload.partner_id, - payload.points - ); - this.pos.couponCache[coupon.id] = coupon; - this.codeActivatedCoupons.push(coupon); - await this._updateLoyaltyPrograms(); - claimableRewards = this.getClaimableRewards(coupon.id); - } else { - return payload.error_message; + return result; + }, 0); + const correction = shouldCorrectRemainingPoints + ? this._getPointsCorrection(reward.program_id) + : 0; + freeQty = computeFreeQuantity( + (remainingPoints - correction) / factor, + reward.required_points / factor, + reward.reward_product_qty + ); + } else { + freeQty = Math.floor( + (remainingPoints / reward.required_points) * reward.reward_product_qty + ); + } + } else if (reward.program_id.trigger == "with_code") { + freeQty = Math.floor( + (remainingPoints / reward.required_points) * reward.reward_product_qty + ); + } + return Math.min(available, freeQty) - claimed; + }, + _computePotentialFreeProductQty(reward, product, remainingPoints) { + if (reward.program_id.trigger == "auto") { + if (this._isRewardProductPartOfRules(reward, product)) { + const line = this.get_orderlines().find( + (line) => line.reward_product_id === product.id + ); + // Compute the correction points once even if there are multiple reward lines. + // This is because _getPointsCorrection is taking into account all the lines already. + const claimedPoints = line ? this._getPointsCorrection(reward.program_id) : 0; + return Math.floor( + ((remainingPoints - claimedPoints) / reward.required_points) * + reward.reward_product_qty + ); + } else { + return Math.floor( + (remainingPoints / reward.required_points) * reward.reward_product_qty + ); + } + } else if (reward.program_id.trigger == "with_code") { + return Math.floor( + (remainingPoints / reward.required_points) * reward.reward_product_qty + ); + } + }, + /** + * @param {Object} args See `_applyReward` + * @returns {Array} List of values to create the reward lines + */ + _getRewardLineValuesProduct(args) { + const reward = args["reward"]; + const product = this.pos.db.get_product_by_id( + args["product"] || reward.reward_product_ids[0] + ); + const points = this._getRealCouponPoints(args["coupon_id"]); + const unclaimedQty = this._computeUnclaimedFreeProductQty( + reward, + args["coupon_id"], + product, + points + ); + if (unclaimedQty <= 0) { + return _t("There are not enough products in the basket to claim this reward."); + } + const claimable_count = reward.clear_wallet + ? 1 + : Math.min( + Math.ceil(unclaimedQty / reward.reward_product_qty), + Math.floor(points / reward.required_points) + ); + const cost = reward.clear_wallet ? points : claimable_count * reward.required_points; + // In case the reward is the product multiple times, give it as many times as possible + const freeQuantity = Math.min(unclaimedQty, reward.reward_product_qty * claimable_count); + return [ + { + product: reward.discount_line_product_id, + price: -round_decimals( + product.get_price(this.pricelist, freeQuantity), + this.pos.currency.decimal_places + ), + tax_ids: product.taxes_id, + quantity: freeQuantity, + reward_id: reward.id, + is_reward_line: true, + reward_product_id: product.id, + coupon_id: args["coupon_id"], + points_cost: cost, + reward_identifier_code: _newRandomRewardCode(), + merge: false, + }, + ]; + }, + /** + * Full routine for activating a code for the order. + * If only one more reward is claimable after activating the code, that reward is claimed + * directly, to avoid more steps than necessary. + * If more rewards are claimable, the employee will have to manually select the reward + * in the reward selection menu. + * + * @param {String} code + * @returns true if everything went right, error message if not. + */ + async _activateCode(code) { + const rule = this.pos.rules.find((rule) => { + return rule.mode === "with_code" && (rule.promo_barcode === code || rule.code === code); + }); + let claimableRewards = null; + if (rule) { + if (this.codeActivatedProgramRules.includes(rule.id)) { + return _t("That promo code program has already been activated."); + } + this.codeActivatedProgramRules.push(rule.id); + await this._updateLoyaltyPrograms(); + claimableRewards = this.getClaimableRewards(false, rule.program_id.id); + } else { + if (this.codeActivatedCoupons.find((coupon) => coupon.code === code)) { + return _t("That coupon code has already been scanned and activated."); + } + const customer = this.get_partner(); + const { successful, payload } = await this.pos.env.services.rpc({ + model: "pos.config", + method: "use_coupon_code", + args: [ + [this.pos.config.id], + code, + this.creation_date, + customer ? customer.id : false, + ], + kwargs: { context: session.user_context }, + }); + if (successful) { + // Allow rejecting a gift card that is not yet paid. + const program = this.pos.program_by_id[payload.program_id]; + if (program && program.program_type === "gift_card" && !payload.has_source_order) { + const { confirmed } = await Gui.showPopup(ConfirmPopup, { + title: _t("Unpaid gift card"), + body: _t( + "This gift card is not linked to any order. Do you really want to apply its reward?" + ), + }); + if (!confirmed) { + return _t("Unpaid gift card rejected."); + } } - } - if (claimableRewards && claimableRewards.length === 1) { - if ( - claimableRewards[0].reward.reward_type !== "product" || - !claimableRewards[0].reward.multi_product - ) { - this._applyReward(claimableRewards[0].reward, claimableRewards[0].coupon_id); - this._updateRewards(); - } - } - return true; - } - async activateCode(code) { - const res = await this._activateCode(code); - if (res !== true) { - Gui.showNotification(res); + const coupon = new PosLoyaltyCard( + code, + payload.coupon_id, + payload.program_id, + payload.partner_id, + payload.points + ); + this.pos.couponCache[coupon.id] = coupon; + this.codeActivatedCoupons.push(coupon); + await this._updateLoyaltyPrograms(); + claimableRewards = this.getClaimableRewards(coupon.id); + } else { + return payload.error_message; } } - isProgramsResettable() { - const array = [ - this.disabledRewards, - this.codeActivatedProgramRules, - this.codeActivatedCoupons, - this.couponPointChanges, - this._get_reward_lines(), - ]; - return array.some((elem) => elem.length > 0); + if (claimableRewards && claimableRewards.length === 1) { + if ( + claimableRewards[0].reward.reward_type !== "product" || + !claimableRewards[0].reward.multi_product + ) { + this._applyReward(claimableRewards[0].reward, claimableRewards[0].coupon_id); + this._updateRewards(); + } } - }; -Registries.Model.extend(Order, PosLoyaltyOrder); + return true; + }, + async activateCode(code) { + const res = await this._activateCode(code); + if (res !== true) { + Gui.showNotification(res); + } + }, + isProgramsResettable() { + const array = [ + this.disabledRewards, + this.codeActivatedProgramRules, + this.codeActivatedCoupons, + this.couponPointChanges, + this._get_reward_lines(), + ]; + return array.some((elem) => elem.length > 0); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/OrderSummary.js b/addons/pos_loyalty/static/src/js/OrderSummary.js index 7c3d423eae0..78084e72760 100644 --- a/addons/pos_loyalty/static/src/js/OrderSummary.js +++ b/addons/pos_loyalty/static/src/js/OrderSummary.js @@ -1,14 +1,11 @@ /** @odoo-module **/ -import OrderSummary from "@point_of_sale/js/Screens/ProductScreen/OrderSummary"; -import Registries from "@point_of_sale/js/Registries"; +import { OrderSummary } from "@point_of_sale/js/Screens/ProductScreen/OrderSummary"; +import { patch } from "@web/core/utils/patch"; -export const PosLoyaltyOrderSummary = (OrderSummary) => - class PosLoyaltyOrderSummary extends OrderSummary { - getLoyaltyPoints() { - const order = this.env.pos.get_order(); - return order.getLoyaltyPoints(); - } - }; - -Registries.Component.extend(OrderSummary, PosLoyaltyOrderSummary); +patch(OrderSummary.prototype, "pos_loyalty.OrderSummary", { + getLoyaltyPoints() { + const order = this.env.pos.get_order(); + return order.getLoyaltyPoints(); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/OrderWidget.js b/addons/pos_loyalty/static/src/js/OrderWidget.js index c41796991e0..aacc6979c16 100644 --- a/addons/pos_loyalty/static/src/js/OrderWidget.js +++ b/addons/pos_loyalty/static/src/js/OrderWidget.js @@ -1,45 +1,42 @@ /** @odoo-module **/ -import OrderWidget from "@point_of_sale/js/Screens/ProductScreen/OrderWidget"; -import Registries from "@point_of_sale/js/Registries"; +import { OrderWidget } from "@point_of_sale/js/Screens/ProductScreen/OrderWidget"; +import { patch } from "@web/core/utils/patch"; -export const PosLoyaltyOrderWidget = (OrderWidget) => - class PosLoyaltyOrderWidget extends OrderWidget { - getActiveProgramsAndRewards() { - const order = this.env.pos.get_order(); - const activePrograms = Object.values(order.couponPointChanges).map( - (pe) => this.env.pos.program_by_id[pe.program_id] - ); - const seenRewards = new Set(); - const activeRewards = []; - for (const line of order._get_reward_lines()) { - const key = - line.reward_id + "-" + line.coupon_id + "-" + line.reward_identifier_code; - if (seenRewards.has(key)) { - continue; - } - seenRewards.add(key); - const dbCoupon = this.env.pos.couponCache[line.coupon_id]; - const couponCode = dbCoupon ? dbCoupon.code : false; - activeRewards.push({ - id: line.cid, - name: line.get_product().display_name, - code: couponCode, - }); +patch(OrderWidget.prototype, "pos_loyalty.OrderWidget", { + getActiveProgramsAndRewards() { + const order = this.env.pos.get_order(); + const activePrograms = Object.values(order.couponPointChanges).map( + (pe) => this.env.pos.program_by_id[pe.program_id] + ); + const seenRewards = new Set(); + const activeRewards = []; + for (const line of order._get_reward_lines()) { + const key = + line.reward_id + "-" + line.coupon_id + "-" + line.reward_identifier_code; + if (seenRewards.has(key)) { + continue; } - const activeCoupons = order.codeActivatedCoupons.map((coupon) => { - const program = this.env.pos.program_by_id[coupon.program_id]; - return { - programName: program.name, - code: coupon.code, - }; + seenRewards.add(key); + const dbCoupon = this.env.pos.couponCache[line.coupon_id]; + const couponCode = dbCoupon ? dbCoupon.code : false; + activeRewards.push({ + id: line.cid, + name: line.get_product().display_name, + code: couponCode, }); - return { - activePrograms: [...new Set(activePrograms)], - activeCoupons, - activeRewards, - }; } - }; - -Registries.Component.extend(OrderWidget, PosLoyaltyOrderWidget); + const activeCoupons = order.codeActivatedCoupons.map((coupon) => { + const program = this.env.pos.program_by_id[coupon.program_id]; + return { + programName: program.name, + code: coupon.code, + }; + }); + return { + activePrograms: [...new Set(activePrograms)], + activeCoupons, + activeRewards, + }; + }, +}); diff --git a/addons/pos_loyalty/static/src/js/Orderline.js b/addons/pos_loyalty/static/src/js/Orderline.js index 33eef0c76e3..758ed774388 100644 --- a/addons/pos_loyalty/static/src/js/Orderline.js +++ b/addons/pos_loyalty/static/src/js/Orderline.js @@ -1,34 +1,31 @@ /** @odoo-module **/ -import Orderline from "@point_of_sale/js/Screens/ProductScreen/Orderline"; -import Registries from "@point_of_sale/js/Registries"; +import { Orderline } from "@point_of_sale/js/Screens/ProductScreen/Orderline"; +import { patch } from "@web/core/utils/patch"; -export const PosLoyaltyOrderline = (Orderline) => - class extends Orderline { - get addedClasses() { - return Object.assign( - { "program-reward": this.props.line.is_reward_line }, - super.addedClasses +patch(Orderline.prototype, "pos_loyalty.Orderline", { + get addedClasses() { + return Object.assign( + { "program-reward": this.props.line.is_reward_line }, + this._super() + ); + }, + _isGiftCardOrEWalletReward() { + const coupon = this.env.pos.couponCache[this.props.line.coupon_id]; + if (coupon) { + const program = this.env.pos.program_by_id[coupon.program_id]; + return ( + ["ewallet", "gift_card"].includes(program.program_type) && + this.props.line.is_reward_line ); } - _isGiftCardOrEWalletReward() { - const coupon = this.env.pos.couponCache[this.props.line.coupon_id]; - if (coupon) { - const program = this.env.pos.program_by_id[coupon.program_id]; - return ( - ["ewallet", "gift_card"].includes(program.program_type) && - this.props.line.is_reward_line - ); - } - return false; + return false; + }, + _getGiftCardOrEWalletBalance() { + const coupon = this.env.pos.couponCache[this.props.line.coupon_id]; + if (coupon) { + return this.env.pos.format_currency(coupon.balance); } - _getGiftCardOrEWalletBalance() { - const coupon = this.env.pos.couponCache[this.props.line.coupon_id]; - if (coupon) { - return this.env.pos.format_currency(coupon.balance); - } - return this.env.pos.format_currency(0); - } - }; - -Registries.Component.extend(Orderline, PosLoyaltyOrderline); + return this.env.pos.format_currency(0); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/PartnerLine.js b/addons/pos_loyalty/static/src/js/PartnerLine.js index a883368f068..963d9136164 100644 --- a/addons/pos_loyalty/static/src/js/PartnerLine.js +++ b/addons/pos_loyalty/static/src/js/PartnerLine.js @@ -1,23 +1,18 @@ /** @odoo-module */ -import PartnerLine from "@point_of_sale/js/Screens/PartnerListScreen/PartnerLine"; -import Registries from "@point_of_sale/js/Registries"; +import { PartnerLine } from "@point_of_sale/js/Screens/PartnerListScreen/PartnerLine"; +import { patch } from "@web/core/utils/patch"; -const PosLoyaltyPartnerLine = (PartnerLine) => - class extends PartnerLine { - _getLoyaltyPointsRepr(loyaltyCard) { - const program = this.env.pos.program_by_id[loyaltyCard.program_id]; - if (program.program_type === "ewallet") { - return `${program.name}: ${this.env.pos.format_currency(loyaltyCard.balance)}`; - } - const balanceRepr = this.env.pos.format_pr(loyaltyCard.balance, 0.01); - if (program.portal_visible) { - return `${balanceRepr} ${program.portal_point_name}`; - } - return _.str.sprintf(this.env._t("%s Points"), balanceRepr); +patch(PartnerLine.prototype, "pos_loyalty.PartnerLine", { + _getLoyaltyPointsRepr(loyaltyCard) { + const program = this.env.pos.program_by_id[loyaltyCard.program_id]; + if (program.program_type === "ewallet") { + return `${program.name}: ${this.env.pos.format_currency(loyaltyCard.balance)}`; } - }; - -Registries.Component.extend(PartnerLine, PosLoyaltyPartnerLine); - -export default PartnerLine; + const balanceRepr = this.env.pos.format_pr(loyaltyCard.balance, 0.01); + if (program.portal_visible) { + return `${balanceRepr} ${program.portal_point_name}`; + } + return _.str.sprintf(this.env._t("%s Points"), balanceRepr); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/PartnerListScreen.js b/addons/pos_loyalty/static/src/js/PartnerListScreen.js index 1f212e48c35..5ab7d96da10 100644 --- a/addons/pos_loyalty/static/src/js/PartnerListScreen.js +++ b/addons/pos_loyalty/static/src/js/PartnerListScreen.js @@ -1,19 +1,14 @@ /** @odoo-module */ -import PartnerListScreen from "@point_of_sale/js/Screens/PartnerListScreen/PartnerListScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PartnerListScreen } from "@point_of_sale/js/Screens/PartnerListScreen/PartnerListScreen"; +import { patch } from "@web/core/utils/patch"; -const PosLoyaltyPartnerListScreen = (PartnerListScreen) => - class extends PartnerListScreen { - /** - * Needs to be set to true to show the loyalty points in the partner list. - * @override - */ - get isBalanceDisplayed() { - return true; - } - }; - -Registries.Component.extend(PartnerListScreen, PosLoyaltyPartnerListScreen); - -export default PartnerListScreen; +patch(PartnerListScreen.prototype, "pos_loyalty.PartnerListScreen", { + /** + * Needs to be set to true to show the loyalty points in the partner list. + * @override + */ + get isBalanceDisplayed() { + return true; + }, +}); diff --git a/addons/pos_loyalty/static/src/js/PaymentScreen.js b/addons/pos_loyalty/static/src/js/PaymentScreen.js index d965bff676b..c0f2368ec90 100644 --- a/addons/pos_loyalty/static/src/js/PaymentScreen.js +++ b/addons/pos_loyalty/static/src/js/PaymentScreen.js @@ -1,176 +1,175 @@ /** @odoo-module **/ -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { patch } from "@web/core/utils/patch"; import session from "web.session"; import { PosLoyaltyCard } from "@pos_loyalty/js/Loyalty"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; -export const PosLoyaltyPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - //@override - async validateOrder(isForceValidate) { - const pointChanges = {}; - const newCodes = []; - for (const pe of Object.values(this.currentOrder.couponPointChanges)) { - if (pe.coupon_id > 0) { - pointChanges[pe.coupon_id] = pe.points; - } else if (pe.barcode && !pe.giftCardId) { - // New coupon with a specific code, validate that it does not exist - newCodes.push(pe.barcode); - } +patch(PaymentScreen.prototype, "pos_loyalty.PaymentScreen", { + //@override + async validateOrder(isForceValidate) { + const _super = this._super; + const pointChanges = {}; + const newCodes = []; + for (const pe of Object.values(this.currentOrder.couponPointChanges)) { + if (pe.coupon_id > 0) { + pointChanges[pe.coupon_id] = pe.points; + } else if (pe.barcode && !pe.giftCardId) { + // New coupon with a specific code, validate that it does not exist + newCodes.push(pe.barcode); } - for (const line of this.currentOrder._get_reward_lines()) { - if (line.coupon_id < 1) { - continue; - } - if (!pointChanges[line.coupon_id]) { - pointChanges[line.coupon_id] = -line.points_cost; - } else { - pointChanges[line.coupon_id] -= line.points_cost; - } - } - if (!(await this._isOrderValid(isForceValidate))) { - return; - } - // No need to do an rpc if no existing coupon is being used. - if (!_.isEmpty(pointChanges) || newCodes.length) { - try { - const { successful, payload } = await this.rpc({ - model: "pos.order", - method: "validate_coupon_programs", - args: [[], pointChanges, newCodes], - kwargs: { context: session.user_context }, - }); - // Payload may contain the points of the concerned coupons to be updated in case of error. (So that rewards can be corrected) - if (payload && payload.updated_points) { - for (const pointChange of Object.entries(payload.updated_points)) { - if (this.env.pos.couponCache[pointChange[0]]) { - this.env.pos.couponCache[pointChange[0]].balance = pointChange[1]; - } - } - } - if (payload && payload.removed_coupons) { - for (const couponId of payload.removed_coupons) { - if (this.env.pos.couponCache[couponId]) { - delete this.env.pos.couponCache[couponId]; - } - } - this.currentOrder.codeActivatedCoupons = - this.currentOrder.codeActivatedCoupons.filter( - (coupon) => !payload.removed_coupons.includes(coupon.id) - ); - } - if (!successful) { - this.showPopup("ErrorPopup", { - title: this.env._t("Error validating rewards"), - body: payload.message, - }); - return; - } - } catch { - // Do nothing with error, while this validation step is nice for error messages - // it should not be blocking. - } - } - await super.validateOrder(...arguments); } - - /** - * @override - */ - async _postPushOrderResolve(order, server_ids) { - // Compile data for our function - const rewardLines = order._get_reward_lines(); - const partner = order.get_partner(); - let couponData = Object.values(order.couponPointChanges).reduce((agg, pe) => { - agg[pe.coupon_id] = Object.assign({}, pe, { - points: - pe.points - - order._getPointsCorrection(this.env.pos.program_by_id[pe.program_id]), - }); - const program = this.env.pos.program_by_id[pe.program_id]; - if (program.is_nominative && partner) { - agg[pe.coupon_id].partner_id = partner.id; - } - return agg; - }, {}); - for (const line of rewardLines) { - const reward = this.env.pos.reward_by_id[line.reward_id]; - if (!couponData[line.coupon_id]) { - couponData[line.coupon_id] = { - points: 0, - program_id: reward.program_id.id, - coupon_id: line.coupon_id, - barcode: false, - }; - } - if (!couponData[line.coupon_id].line_codes) { - couponData[line.coupon_id].line_codes = []; - } - if (!couponData[line.coupon_id].line_codes.includes(line.reward_identifier_code)) { - !couponData[line.coupon_id].line_codes.push(line.reward_identifier_code); - } - couponData[line.coupon_id].points -= line.points_cost; + for (const line of this.currentOrder._get_reward_lines()) { + if (line.coupon_id < 1) { + continue; } - // We actually do not care about coupons for 'current' programs that did not claim any reward, they will be lost if not validated - couponData = Object.fromEntries( - Object.entries(couponData).filter(([key, value]) => { - const program = this.env.pos.program_by_id[value.program_id]; - if (program.applies_on === "current") { - return value.line_codes && value.line_codes.length; - } - return true; - }) - ); - if (!_.isEmpty(couponData)) { - const payload = await this.rpc({ + if (!pointChanges[line.coupon_id]) { + pointChanges[line.coupon_id] = -line.points_cost; + } else { + pointChanges[line.coupon_id] -= line.points_cost; + } + } + if (!(await this._isOrderValid(isForceValidate))) { + return; + } + // No need to do an rpc if no existing coupon is being used. + if (!_.isEmpty(pointChanges) || newCodes.length) { + try { + const { successful, payload } = await this.rpc({ model: "pos.order", - method: "confirm_coupon_programs", - args: [server_ids, couponData], + method: "validate_coupon_programs", + args: [[], pointChanges, newCodes], kwargs: { context: session.user_context }, }); - if (payload.coupon_updates) { - for (const couponUpdate of payload.coupon_updates) { - let dbCoupon = this.env.pos.couponCache[couponUpdate.old_id]; - if (dbCoupon) { - dbCoupon.id = couponUpdate.id; - dbCoupon.balance = couponUpdate.points; - dbCoupon.code = couponUpdate.code; - } else { - dbCoupon = new PosLoyaltyCard( - couponUpdate.code, - couponUpdate.id, - couponUpdate.program_id, - couponUpdate.partner_id, - couponUpdate.points - ); - } - delete this.env.pos.couponCache[couponUpdate.old_id]; - this.env.pos.couponCache[couponUpdate.id] = dbCoupon; - } - } - // Update the usage count since it is checked based on local data - if (payload.program_updates) { - for (const programUpdate of payload.program_updates) { - const program = this.env.pos.program_by_id[programUpdate.program_id]; - if (program) { - program.total_order_count = programUpdate.usages; + // Payload may contain the points of the concerned coupons to be updated in case of error. (So that rewards can be corrected) + if (payload && payload.updated_points) { + for (const pointChange of Object.entries(payload.updated_points)) { + if (this.env.pos.couponCache[pointChange[0]]) { + this.env.pos.couponCache[pointChange[0]].balance = pointChange[1]; } } } - if (payload.coupon_report) { - for (const report_entry of Object.entries(payload.coupon_report)) { - await this.env.legacyActionManager.do_action(report_entry[0], { - additional_context: { - active_ids: report_entry[1], - }, - }); + if (payload && payload.removed_coupons) { + for (const couponId of payload.removed_coupons) { + if (this.env.pos.couponCache[couponId]) { + delete this.env.pos.couponCache[couponId]; + } } + this.currentOrder.codeActivatedCoupons = + this.currentOrder.codeActivatedCoupons.filter( + (coupon) => !payload.removed_coupons.includes(coupon.id) + ); } - order.new_coupon_info = payload.new_coupon_info; + if (!successful) { + this.showPopup(ErrorPopup, { + title: this.env._t("Error validating rewards"), + body: payload.message, + }); + return; + } + } catch { + // Do nothing with error, while this validation step is nice for error messages + // it should not be blocking. } - return super._postPushOrderResolve(order, server_ids); } - }; - -Registries.Component.extend(PaymentScreen, PosLoyaltyPaymentScreen); + await _super(...arguments); + }, + /** + * @override + */ + async _postPushOrderResolve(order, server_ids) { + // Compile data for our function + const _super = this._super; + const rewardLines = order._get_reward_lines(); + const partner = order.get_partner(); + let couponData = Object.values(order.couponPointChanges).reduce((agg, pe) => { + agg[pe.coupon_id] = Object.assign({}, pe, { + points: + pe.points - + order._getPointsCorrection(this.env.pos.program_by_id[pe.program_id]), + }); + const program = this.env.pos.program_by_id[pe.program_id]; + if (program.is_nominative && partner) { + agg[pe.coupon_id].partner_id = partner.id; + } + return agg; + }, {}); + for (const line of rewardLines) { + const reward = this.env.pos.reward_by_id[line.reward_id]; + if (!couponData[line.coupon_id]) { + couponData[line.coupon_id] = { + points: 0, + program_id: reward.program_id.id, + coupon_id: line.coupon_id, + barcode: false, + }; + } + if (!couponData[line.coupon_id].line_codes) { + couponData[line.coupon_id].line_codes = []; + } + if (!couponData[line.coupon_id].line_codes.includes(line.reward_identifier_code)) { + !couponData[line.coupon_id].line_codes.push(line.reward_identifier_code); + } + couponData[line.coupon_id].points -= line.points_cost; + } + // We actually do not care about coupons for 'current' programs that did not claim any reward, they will be lost if not validated + couponData = Object.fromEntries( + Object.entries(couponData).filter(([key, value]) => { + const program = this.env.pos.program_by_id[value.program_id]; + if (program.applies_on === "current") { + return value.line_codes && value.line_codes.length; + } + return true; + }) + ); + if (!_.isEmpty(couponData)) { + const payload = await this.rpc({ + model: "pos.order", + method: "confirm_coupon_programs", + args: [server_ids, couponData], + kwargs: { context: session.user_context }, + }); + if (payload.coupon_updates) { + for (const couponUpdate of payload.coupon_updates) { + let dbCoupon = this.env.pos.couponCache[couponUpdate.old_id]; + if (dbCoupon) { + dbCoupon.id = couponUpdate.id; + dbCoupon.balance = couponUpdate.points; + dbCoupon.code = couponUpdate.code; + } else { + dbCoupon = new PosLoyaltyCard( + couponUpdate.code, + couponUpdate.id, + couponUpdate.program_id, + couponUpdate.partner_id, + couponUpdate.points + ); + } + delete this.env.pos.couponCache[couponUpdate.old_id]; + this.env.pos.couponCache[couponUpdate.id] = dbCoupon; + } + } + // Update the usage count since it is checked based on local data + if (payload.program_updates) { + for (const programUpdate of payload.program_updates) { + const program = this.env.pos.program_by_id[programUpdate.program_id]; + if (program) { + program.total_order_count = programUpdate.usages; + } + } + } + if (payload.coupon_report) { + for (const report_entry of Object.entries(payload.coupon_report)) { + await this.env.legacyActionManager.do_action(report_entry[0], { + additional_context: { + active_ids: report_entry[1], + }, + }); + } + } + order.new_coupon_info = payload.new_coupon_info; + } + return _super(order, server_ids); + }, +}); diff --git a/addons/pos_loyalty/static/src/js/ProductScreen.js b/addons/pos_loyalty/static/src/js/ProductScreen.js index a1d53fb106a..477159c2467 100644 --- a/addons/pos_loyalty/static/src/js/ProductScreen.js +++ b/addons/pos_loyalty/static/src/js/ProductScreen.js @@ -1,256 +1,256 @@ /** @odoo-module **/ -import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; -import Registries from "@point_of_sale/js/Registries"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; +import { patch } from "@web/core/utils/patch"; +import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup"; +import { TextInputPopup } from "@point_of_sale/js/Popups/TextInputPopup"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; -export const PosLoyaltyProductScreen = (ProductScreen) => - class extends ProductScreen { - setup() { - super.setup(); - useBarcodeReader({ - coupon: this._onCouponScan, +patch(ProductScreen.prototype, "pos_loyalty.ProductScreen", { + setup() { + this._super(...arguments); + useBarcodeReader({ + coupon: this._onCouponScan, + }); + }, + async _onClickPay() { + const order = this.env.pos.get_order(); + const eWalletLine = order + .get_orderlines() + .find((line) => line.getEWalletGiftCardProgramType() === "ewallet"); + if (eWalletLine && !order.get_partner()) { + const { confirmed } = await this.showPopup(ConfirmPopup, { + title: this.env._t("Customer needed"), + body: this.env._t("eWallet requires a customer to be selected"), }); - } - async _onClickPay() { - const order = this.env.pos.get_order(); - const eWalletLine = order - .get_orderlines() - .find((line) => line.getEWalletGiftCardProgramType() === "ewallet"); - if (eWalletLine && !order.get_partner()) { - const { confirmed } = await this.showPopup("ConfirmPopup", { - title: this.env._t("Customer needed"), - body: this.env._t("eWallet requires a customer to be selected"), - }); + if (confirmed) { + const { confirmed, payload: newPartner } = await this.showTempScreen( + "PartnerListScreen", + { partner: null } + ); if (confirmed) { - const { confirmed, payload: newPartner } = await this.showTempScreen( - "PartnerListScreen", - { partner: null } - ); - if (confirmed) { - order.set_partner(newPartner); - order.updatePricelist(newPartner); - } + order.set_partner(newPartner); + order.updatePricelist(newPartner); + } + } + } else { + return this._super(...arguments); + } + }, + /** + * Sets up the options for the gift card product. + * @param {object} program + * @param {object} options + * @returns {Promise} whether to proceed with adding the product or not + */ + async _setupGiftCardOptions(program, options) { + options.quantity = 1; + options.merge = false; + options.eWalletGiftCardProgram = program; + + // If gift card program setting is 'scan_use', ask for the code. + if (this.env.pos.config.gift_card_settings == "scan_use") { + const { confirmed, payload: code } = await this.showPopup(TextInputPopup, { + title: this.env._t("Generate a Gift Card"), + startingValue: "", + placeholder: this.env._t("Enter the gift card code"), + }); + if (!confirmed) { + return false; + } + const trimmedCode = code.trim(); + if (trimmedCode && trimmedCode.startsWith("044")) { + // check if the code exist in the database + // if so, use its balance, otherwise, use the unit price of the gift card product + const fetchedGiftCard = await this.rpc({ + model: "loyalty.card", + method: "search_read", + args: [ + [ + ["code", "=", trimmedCode], + ["program_id", "=", program.id], + ], + ["points", "source_pos_order_id"], + ], + }); + // There should be maximum one gift card for a given code. + const giftCard = fetchedGiftCard[0]; + if (giftCard && giftCard.source_pos_order_id) { + this.showPopup(ErrorPopup, { + title: this.env._t("This gift card has already been sold"), + body: this.env._t( + "You cannot sell a gift card that has already been sold." + ), + }); + return false; + } + options.giftBarcode = trimmedCode; + if (giftCard) { + // Use the balance of the gift card as the price of the orderline. + // NOTE: No need to convert the points to price because when opening a session, + // the gift card programs are made sure to have 1 point = 1 currency unit. + options.price = giftCard.points; + options.giftCardId = giftCard.id; } } else { - return super._onClickPay(...arguments); + this.showNotification("Please enter a valid gift card code."); + return false; } } - /** - * Sets up the options for the gift card product. - * @param {object} program - * @param {object} options - * @returns {Promise} whether to proceed with adding the product or not - */ - async _setupGiftCardOptions(program, options) { - options.quantity = 1; - options.merge = false; - options.eWalletGiftCardProgram = program; - - // If gift card program setting is 'scan_use', ask for the code. - if (this.env.pos.config.gift_card_settings == "scan_use") { - const { confirmed, payload: code } = await this.showPopup("TextInputPopup", { - title: this.env._t("Generate a Gift Card"), - startingValue: "", - placeholder: this.env._t("Enter the gift card code"), - }); - if (!confirmed) { - return false; - } - const trimmedCode = code.trim(); - if (trimmedCode && trimmedCode.startsWith("044")) { - // check if the code exist in the database - // if so, use its balance, otherwise, use the unit price of the gift card product - const fetchedGiftCard = await this.rpc({ - model: "loyalty.card", - method: "search_read", - args: [ - [ - ["code", "=", trimmedCode], - ["program_id", "=", program.id], - ], - ["points", "source_pos_order_id"], - ], - }); - // There should be maximum one gift card for a given code. - const giftCard = fetchedGiftCard[0]; - if (giftCard && giftCard.source_pos_order_id) { - this.showPopup("ErrorPopup", { - title: this.env._t("This gift card has already been sold"), - body: this.env._t( - "You cannot sell a gift card that has already been sold." - ), - }); - return false; - } - options.giftBarcode = trimmedCode; - if (giftCard) { - // Use the balance of the gift card as the price of the orderline. - // NOTE: No need to convert the points to price because when opening a session, - // the gift card programs are made sure to have 1 point = 1 currency unit. - options.price = giftCard.points; - options.giftCardId = giftCard.id; - } - } else { - this.showNotification("Please enter a valid gift card code."); - return false; - } - } - return true; - } - async setupEWalletOptions(program, options) { - options.quantity = 1; - options.merge = false; - options.eWalletGiftCardProgram = program; - return true; - } - /** - * If the product is a potential reward, also apply the reward. - * @override - */ - async _addProduct(product, options) { - const linkedProgramIds = this.env.pos.productId2ProgramIds[product.id] || []; - const linkedPrograms = linkedProgramIds.map((id) => this.env.pos.program_by_id[id]); - let selectedProgram = null; - if (linkedPrograms.length > 1) { - const { confirmed, payload: program } = await this.showPopup("SelectionPopup", { - title: this.env._t("Select program"), - list: linkedPrograms.map((program) => ({ - id: program.id, - item: program, - label: program.name, - })), - }); - if (confirmed) { - selectedProgram = program; - } else { - // Do nothing here if the selection is cancelled. - return; - } - } else if (linkedPrograms.length === 1) { - selectedProgram = linkedPrograms[0]; - } - if (selectedProgram && selectedProgram.program_type == "gift_card") { - const shouldProceed = await this._setupGiftCardOptions(selectedProgram, options); - if (!shouldProceed) { - return; - } - } else if (selectedProgram && selectedProgram.program_type == "ewallet") { - const shouldProceed = await this.setupEWalletOptions(selectedProgram, options); - if (!shouldProceed) { - return; - } - } - const order = this.env.pos.get_order(); - const potentialRewards = order.getPotentialFreeProductRewards(); - const rewardsToApply = []; - for (const reward of potentialRewards) { - for (const reward_product_id of reward.reward.reward_product_ids) { - if (reward_product_id == product.id) { - rewardsToApply.push(reward); - } - } - } - await super._addProduct(product, options); - await order._updatePrograms(); - if (rewardsToApply.length == 1) { - const reward = rewardsToApply[0]; - order._applyReward(reward.reward, reward.coupon_id, { product: product.id }); - } - } - - _onCouponScan(code) { - // IMPROVEMENT: Ability to understand if the scanned code is to be paid or to be redeemed. - this.currentOrder.activateCode(code.base_code); - } - - async _updateSelectedOrderline(event) { - const selectedLine = this.currentOrder.get_selected_orderline(); - if (event.detail.key === "-") { - if (selectedLine.eWalletGiftCardProgram) { - // Do not allow negative quantity or price in a gift card or ewallet orderline. - // Refunding gift card or ewallet is not supported. - this.showNotification( - this.env._t( - "You cannot set negative quantity or price to gift card or ewallet." - ), - 4000 - ); - return; - } - } - if ( - selectedLine && - selectedLine.is_reward_line && - !selectedLine.manual_reward && - (event.detail.key === "Backspace" || event.detail.key === "Delete") - ) { - const reward = this.env.pos.reward_by_id[selectedLine.reward_id]; - const { confirmed } = await this.showPopup("ConfirmPopup", { - title: this.env._t("Deactivating reward"), - body: _.str.sprintf( - this.env._t( - "Are you sure you want to remove %s from this order?\n You will still be able to claim it through the reward button." - ), - reward.description - ), - cancelText: this.env._t("No"), - confirmText: this.env._t("Yes"), - }); - if (confirmed) { - event.detail.buffer = null; - } else { - // Cancel backspace - return; - } - } - return super._updateSelectedOrderline(...arguments); - } - - /** - * 1/ Perform the usual set value operation (super._setValue) if the line being modified - * is not a reward line or if it is a reward line, the `val` being set is '' or 'remove' only. - * - * 2/ Update activated programs and coupons when removing a reward line. - * - * 3/ Trigger 'update-rewards' if the line being modified is a regular line or - * if removing a reward line. - * - * @override - */ - _setValue(val) { - const selectedLine = this.currentOrder.get_selected_orderline(); - if ( - !selectedLine || - !selectedLine.is_reward_line || - (selectedLine.is_reward_line && ["", "remove"].includes(val)) - ) { - super._setValue(val); - } - if (!selectedLine) { + return true; + }, + async setupEWalletOptions(program, options) { + options.quantity = 1; + options.merge = false; + options.eWalletGiftCardProgram = program; + return true; + }, + /** + * If the product is a potential reward, also apply the reward. + * @override + */ + async _addProduct(product, options) { + const _super = this._super; + const linkedProgramIds = this.env.pos.productId2ProgramIds[product.id] || []; + const linkedPrograms = linkedProgramIds.map((id) => this.env.pos.program_by_id[id]); + let selectedProgram = null; + if (linkedPrograms.length > 1) { + const { confirmed, payload: program } = await this.showPopup(SelectionPopup, { + title: this.env._t("Select program"), + list: linkedPrograms.map((program) => ({ + id: program.id, + item: program, + label: program.name, + })), + }); + if (confirmed) { + selectedProgram = program; + } else { + // Do nothing here if the selection is cancelled. return; } - if (selectedLine.is_reward_line && val === "remove") { - this.currentOrder.disabledRewards.add(selectedLine.reward_id); - const coupon = this.env.pos.couponCache[selectedLine.coupon_id]; - if ( - coupon && - coupon.id > 0 && - this.currentOrder.codeActivatedCoupons.find((c) => c.code === coupon.code) - ) { - delete this.env.pos.couponCache[selectedLine.coupon_id]; - this.currentOrder.codeActivatedCoupons.splice( - this.currentOrder.codeActivatedCoupons.findIndex((coupon) => { - return coupon.id === selectedLine.coupon_id; - }), - 1 - ); - } + } else if (linkedPrograms.length === 1) { + selectedProgram = linkedPrograms[0]; + } + if (selectedProgram && selectedProgram.program_type == "gift_card") { + const shouldProceed = await this._setupGiftCardOptions(selectedProgram, options); + if (!shouldProceed) { + return; } - if (!selectedLine.is_reward_line || (selectedLine.is_reward_line && val === "remove")) { - selectedLine.order._updateRewards(); + } else if (selectedProgram && selectedProgram.program_type == "ewallet") { + const shouldProceed = await this.setupEWalletOptions(selectedProgram, options); + if (!shouldProceed) { + return; } } - }; - -Registries.Component.extend(ProductScreen, PosLoyaltyProductScreen); + const order = this.env.pos.get_order(); + const potentialRewards = order.getPotentialFreeProductRewards(); + const rewardsToApply = []; + for (const reward of potentialRewards) { + for (const reward_product_id of reward.reward.reward_product_ids) { + if (reward_product_id == product.id) { + rewardsToApply.push(reward); + } + } + } + await _super(product, options); + await order._updatePrograms(); + if (rewardsToApply.length == 1) { + const reward = rewardsToApply[0]; + order._applyReward(reward.reward, reward.coupon_id, { product: product.id }); + } + }, + _onCouponScan(code) { + // IMPROVEMENT: Ability to understand if the scanned code is to be paid or to be redeemed. + this.currentOrder.activateCode(code.base_code); + }, + async _updateSelectedOrderline(event) { + const _super = this._super; + const selectedLine = this.currentOrder.get_selected_orderline(); + if (event.detail.key === "-") { + if (selectedLine.eWalletGiftCardProgram) { + // Do not allow negative quantity or price in a gift card or ewallet orderline. + // Refunding gift card or ewallet is not supported. + this.showNotification( + this.env._t( + "You cannot set negative quantity or price to gift card or ewallet." + ), + 4000 + ); + return; + } + } + if ( + selectedLine && + selectedLine.is_reward_line && + !selectedLine.manual_reward && + (event.detail.key === "Backspace" || event.detail.key === "Delete") + ) { + const reward = this.env.pos.reward_by_id[selectedLine.reward_id]; + const { confirmed } = await this.showPopup(ConfirmPopup, { + title: this.env._t("Deactivating reward"), + body: _.str.sprintf( + this.env._t( + "Are you sure you want to remove %s from this order?\n You will still be able to claim it through the reward button." + ), + reward.description + ), + cancelText: this.env._t("No"), + confirmText: this.env._t("Yes"), + }); + if (confirmed) { + event.detail.buffer = null; + } else { + // Cancel backspace + return; + } + } + return _super(...arguments); + }, + /** + * 1/ Perform the usual set value operation (this._super(val)) if the line being modified + * is not a reward line or if it is a reward line, the `val` being set is '' or 'remove' only. + * + * 2/ Update activated programs and coupons when removing a reward line. + * + * 3/ Trigger 'update-rewards' if the line being modified is a regular line or + * if removing a reward line. + * + * @override + */ + _setValue(val) { + const selectedLine = this.currentOrder.get_selected_orderline(); + if ( + !selectedLine || + !selectedLine.is_reward_line || + (selectedLine.is_reward_line && ["", "remove"].includes(val)) + ) { + this._super(val); + } + if (!selectedLine) { + return; + } + if (selectedLine.is_reward_line && val === "remove") { + this.currentOrder.disabledRewards.add(selectedLine.reward_id); + const coupon = this.env.pos.couponCache[selectedLine.coupon_id]; + if ( + coupon && + coupon.id > 0 && + this.currentOrder.codeActivatedCoupons.find((c) => c.code === coupon.code) + ) { + delete this.env.pos.couponCache[selectedLine.coupon_id]; + this.currentOrder.codeActivatedCoupons.splice( + this.currentOrder.codeActivatedCoupons.findIndex((coupon) => { + return coupon.id === selectedLine.coupon_id; + }), + 1 + ); + } + } + if (!selectedLine.is_reward_line || (selectedLine.is_reward_line && val === "remove")) { + selectedLine.order._updateRewards(); + } + }, +}); diff --git a/addons/pos_loyalty/static/src/js/TicketScreen.js b/addons/pos_loyalty/static/src/js/TicketScreen.js index 444ec85a844..053b8688498 100644 --- a/addons/pos_loyalty/static/src/js/TicketScreen.js +++ b/addons/pos_loyalty/static/src/js/TicketScreen.js @@ -1,58 +1,55 @@ /** @odoo-module **/ -import TicketScreen from "@point_of_sale/js/Screens/TicketScreen/TicketScreen"; -import Registries from "@point_of_sale/js/Registries"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { TicketScreen } from "@point_of_sale/js/Screens/TicketScreen/TicketScreen"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; +import { patch } from "@web/core/utils/patch"; /** * Prevent refunding ewallet/gift card lines. */ -export const PosLoyaltyTicketScreen = (TicketScreen) => - class PosLoyaltyTicketScreen extends TicketScreen { - _onUpdateSelectedOrderline() { - const order = this.getSelectedSyncedOrder(); - if (!order) { - return NumberBuffer.reset(); - } - const selectedOrderlineId = this.getSelectedOrderlineId(); - const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); - if (orderline && this._isEWalletGiftCard(orderline)) { - this._showNotAllowedRefundNotification(); - return NumberBuffer.reset(); - } - return super._onUpdateSelectedOrderline(...arguments); +patch(TicketScreen.prototype, "pos_loyalty.TicketScreen", { + _onUpdateSelectedOrderline() { + const order = this.getSelectedSyncedOrder(); + if (!order) { + return numberBuffer.reset(); } - _prepareAutoRefundOnOrder(order) { - const selectedOrderlineId = this.getSelectedOrderlineId(); - const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); - if (this._isEWalletGiftCard(orderline)) { - this._showNotAllowedRefundNotification(); - return false; - } - return super._prepareAutoRefundOnOrder(...arguments); + const selectedOrderlineId = this.getSelectedOrderlineId(); + const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); + if (orderline && this._isEWalletGiftCard(orderline)) { + this._showNotAllowedRefundNotification(); + return numberBuffer.reset(); } - _showNotAllowedRefundNotification() { - this.showNotification( - this.env._t( - "Refunding a top up or reward product for an eWallet or gift card program is not allowed." - ), - 5000 - ); - } - _isEWalletGiftCard(orderline) { - const linkedProgramIds = this.env.pos.productId2ProgramIds[orderline.product.id]; - if (linkedProgramIds) { - return linkedProgramIds.length > 0; - } - if (orderline.is_reward_line) { - const reward = this.env.pos.reward_by_id[orderline.reward_id]; - const program = reward && reward.program_id; - if (program && ["gift_card", "ewallet"].includes(program.program_type)) { - return true; - } - } + return this._super(...arguments); + }, + _prepareAutoRefundOnOrder(order) { + const selectedOrderlineId = this.getSelectedOrderlineId(); + const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId); + if (this._isEWalletGiftCard(orderline)) { + this._showNotAllowedRefundNotification(); return false; } - }; - -Registries.Component.extend(TicketScreen, PosLoyaltyTicketScreen); + return this._super(...arguments); + }, + _showNotAllowedRefundNotification() { + this.showNotification( + this.env._t( + "Refunding a top up or reward product for an eWallet or gift card program is not allowed." + ), + 5000 + ); + }, + _isEWalletGiftCard(orderline) { + const linkedProgramIds = this.env.pos.productId2ProgramIds[orderline.product.id]; + if (linkedProgramIds) { + return linkedProgramIds.length > 0; + } + if (orderline.is_reward_line) { + const reward = this.env.pos.reward_by_id[orderline.reward_id]; + const program = reward && reward.program_id; + if (program && ["gift_card", "ewallet"].includes(program.program_type)) { + return true; + } + } + return false; + }, +}); diff --git a/addons/pos_mercury/static/src/js/OrderReceipt.js b/addons/pos_mercury/static/src/js/OrderReceipt.js index d7dffe12674..e35c4baddb5 100644 --- a/addons/pos_mercury/static/src/js/OrderReceipt.js +++ b/addons/pos_mercury/static/src/js/OrderReceipt.js @@ -1,24 +1,19 @@ /** @odoo-module */ -import OrderReceipt from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt"; -import Registries from "@point_of_sale/js/Registries"; +import { OrderReceipt } from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt"; +import { patch } from "@web/core/utils/patch"; -const PosMercuryOrderReceipt = (OrderReceipt) => - class extends OrderReceipt { - /** - * The receipt has signature if one of the paymentlines - * is paid with mercury. - */ - get hasPosMercurySignature() { - for (const line of this.paymentlines) { - if (line.mercury_data) { - return true; - } +patch(OrderReceipt.prototype, "pos_mercury.OrderReceipt", { + /** + * The receipt has signature if one of the paymentlines + * is paid with mercury. + */ + get hasPosMercurySignature() { + for (const line of this.paymentlines) { + if (line.mercury_data) { + return true; } - return false; } - }; - -Registries.Component.extend(OrderReceipt, PosMercuryOrderReceipt); - -export default OrderReceipt; + return false; + }, +}); diff --git a/addons/pos_mercury/static/src/js/PaymentScreen.js b/addons/pos_mercury/static/src/js/PaymentScreen.js index ab9ee9c2e2f..3f0af630101 100644 --- a/addons/pos_mercury/static/src/js/PaymentScreen.js +++ b/addons/pos_mercury/static/src/js/PaymentScreen.js @@ -1,10 +1,13 @@ /** @odoo-module */ import { _t } from "web.core"; -import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; -import Registries from "@point_of_sale/js/Registries"; -import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer"; +import { patch } from "@web/core/utils/patch"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { numberBuffer } from "@point_of_sale/js/Misc/NumberBuffer"; import { useBarcodeReader } from "@point_of_sale/js/custom_hooks"; +import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; +import { PaymentTransactionPopup } from "./PaymentTransactionPopup"; +import { SelectionPopup } from "@point_of_sale/js/Popups/SelectionPopup"; // Lookup table to store status and error messages const lookUpCodeTransaction = { @@ -91,497 +94,475 @@ const lookUpCodeTransaction = { }, }; -const PosMercuryPaymentScreen = (PaymentScreen) => - class extends PaymentScreen { - setup() { - super.setup(); - if (this.env.pos.getOnlinePaymentMethods().length !== 0) { - useBarcodeReader({ - credit: this.credit_code_action, - }); +patch(PaymentScreen.prototype, "pos_mercury.PaymentScreen", { + setup() { + this._super(...arguments); + if (this.env.pos.getOnlinePaymentMethods().length !== 0) { + useBarcodeReader({ + credit: this.credit_code_action, + }); + } + // How long we wait for the odoo server to deliver the response of + // a Vantiv transaction + this.server_timeout_in_ms = 95000; + + // How many Vantiv transactions we send without receiving a + // response + this.server_retries = 3; + }, + /** + * The card reader acts as a barcode scanner. This sets up + * the numberBuffer to not immediately act on keyboard + * input. + * + * @override + */ + get _getNumberBufferConfig() { + const res = this._super(); + res["useWithBarcode"] = true; + return res; + }, + /** + * Finish any pending input before trying to validate. + * + * @override + */ + async validateOrder(isForceValidate) { + numberBuffer.capture(); + return this._super(...arguments); + }, + /** + * Finish any pending input before sending a request to a terminal. + * + * @override + */ + async _sendPaymentRequest({ detail: line }) { + numberBuffer.capture(); + return this._super(...arguments); + }, + _get_swipe_pending_line() { + var i = 0; + var lines = this.env.pos.get_order().get_paymentlines(); + + for (i = 0; i < lines.length; i++) { + if (lines[i].mercury_swipe_pending) { + return lines[i]; } - // How long we wait for the odoo server to deliver the response of - // a Vantiv transaction - this.server_timeout_in_ms = 95000; - - // How many Vantiv transactions we send without receiving a - // response - this.server_retries = 3; } - /** - * The card reader acts as a barcode scanner. This sets up - * the NumberBuffer to not immediately act on keyboard - * input. - * - * @override - */ - get _getNumberBufferConfig() { - const res = super._getNumberBufferConfig; - res["useWithBarcode"] = true; - return res; - } + return 0; + }, + _does_credit_payment_line_exist(amount, card_number, card_brand, card_owner_name) { + var i = 0; + var lines = this.env.pos.get_order().get_paymentlines(); - /** - * Finish any pending input before trying to validate. - * - * @override - */ - async validateOrder(isForceValidate) { - NumberBuffer.capture(); - return super.validateOrder(...arguments); - } - - /** - * Finish any pending input before sending a request to a terminal. - * - * @override - */ - async _sendPaymentRequest({ detail: line }) { - NumberBuffer.capture(); - return super._sendPaymentRequest(...arguments); - } - - _get_swipe_pending_line() { - var i = 0; - var lines = this.env.pos.get_order().get_paymentlines(); - - for (i = 0; i < lines.length; i++) { - if (lines[i].mercury_swipe_pending) { - return lines[i]; - } + for (i = 0; i < lines.length; i++) { + if ( + lines[i].mercury_amount === amount && + lines[i].mercury_card_number === card_number && + lines[i].mercury_card_brand === card_brand && + lines[i].mercury_card_owner_name === card_owner_name + ) { + return true; } - - return 0; } - _does_credit_payment_line_exist(amount, card_number, card_brand, card_owner_name) { - var i = 0; - var lines = this.env.pos.get_order().get_paymentlines(); + return false; + }, + retry_mercury_transaction(def, response, retry_nr, can_connect_to_server, callback, args) { + var self = this; + var message = ""; - for (i = 0; i < lines.length; i++) { - if ( - lines[i].mercury_amount === amount && - lines[i].mercury_card_number === card_number && - lines[i].mercury_card_brand === card_brand && - lines[i].mercury_card_owner_name === card_owner_name - ) { - return true; - } - } - - return false; - } - - retry_mercury_transaction(def, response, retry_nr, can_connect_to_server, callback, args) { - var self = this; - var message = ""; - - if (retry_nr < self.server_retries) { - if (response) { - message = "Retry #" + (retry_nr + 1) + "...

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

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