diff --git a/addons/point_of_sale/static/src/app/models/pos_order.js b/addons/point_of_sale/static/src/app/models/pos_order.js index e83185772a1f1..2556be4fd6a61 100644 --- a/addons/point_of_sale/static/src/app/models/pos_order.js +++ b/addons/point_of_sale/static/src/app/models/pos_order.js @@ -686,7 +686,7 @@ export class PosOrder extends Base { get_tax_details() { const taxDetails = {}; for (const line of this.lines) { - for (const taxData of line.get_all_prices().taxesData) { + for (const taxData of line.allPrices.taxesData) { const taxId = taxData.id; if (!taxDetails[taxId]) { taxDetails[taxId] = Object.assign({}, taxData, { diff --git a/addons/point_of_sale/static/src/app/models/pos_order_line.js b/addons/point_of_sale/static/src/app/models/pos_order_line.js index f403e314c9c88..b590f556adb3a 100644 --- a/addons/point_of_sale/static/src/app/models/pos_order_line.js +++ b/addons/point_of_sale/static/src/app/models/pos_order_line.js @@ -385,17 +385,17 @@ export class PosOrderline extends Base { get_unit_display_price() { if (this.config.iface_tax_included === "total") { - return this.get_all_prices(1).priceWithTax; + return this.allUnitPrices.priceWithTax; } else { - return this.get_all_prices(1).priceWithoutTax; + return this.allUnitPrices.priceWithoutTax; } } getUnitDisplayPriceBeforeDiscount() { if (this.config.iface_tax_included === "total") { - return this.get_all_prices(1).priceWithTaxBeforeDiscount; + return this.allUnitPrices.priceWithTaxBeforeDiscount; } else { - return this.get_all_prices(1).priceWithoutTaxBeforeDiscount; + return this.allUnitPrices.priceWithoutTaxBeforeDiscount; } } get_base_price() { @@ -444,23 +444,23 @@ export class PosOrderline extends Base { } get_price_without_tax() { - return this.get_all_prices().priceWithoutTax; + return this.allPrices.priceWithoutTax; } get_price_with_tax() { - return this.get_all_prices().priceWithTax; + return this.allPrices.priceWithTax; } get_price_with_tax_before_discount() { - return this.get_all_prices().priceWithTaxBeforeDiscount; + return this.allPrices.priceWithTaxBeforeDiscount; } get_tax() { - return this.get_all_prices().tax; + return this.allPrices.tax; } get_tax_details() { - return this.get_all_prices().taxDetails; + return this.allPrices.taxDetails; } get_total_taxes_included_in_price() { @@ -542,6 +542,14 @@ export class PosOrderline extends Base { }; } + get allPrices() { + return this.get_all_prices(); + } + + get allUnitPrices() { + return this.get_all_prices(1); + } + display_discount_policy() { // Sales dropped `discount_policy`, and we only show discount if applied pricelist rule // is a percentage discount. However we don't have that information in pos @@ -619,11 +627,11 @@ export class PosOrderline extends Base { getComboTotalPrice() { const allLines = this.getAllLinesInCombo(); - return allLines.reduce((total, line) => total + line.get_all_prices(1).priceWithTax, 0); + return allLines.reduce((total, line) => total + line.allUnitPrices.priceWithTax, 0); } getComboTotalPriceWithoutTax() { const allLines = this.getAllLinesInCombo(); - return allLines.reduce((total, line) => total + line.get_all_prices(1).priceWithoutTax, 0); + return allLines.reduce((total, line) => total + line.allUnitPrices.priceWithoutTax, 0); } get_old_unit_display_price() { diff --git a/addons/point_of_sale/static/src/app/models/related_models.js b/addons/point_of_sale/static/src/app/models/related_models.js index 5de5d05725224..3f38d275af90b 100644 --- a/addons/point_of_sale/static/src/app/models/related_models.js +++ b/addons/point_of_sale/static/src/app/models/related_models.js @@ -1,8 +1,32 @@ import { reactive, toRaw } from "@odoo/owl"; import { uuidv4 } from "@point_of_sale/utils"; +import { effect } from "@web/core/utils/reactive"; const ID_CONTAINER = {}; +function lazyComputed(obj, propName, compute) { + const key = Symbol(propName); + Object.defineProperty(obj, propName, { + get() { + return this[key](); + }, + configurable: true, + }); + + effect( + function recompute(obj) { + const value = []; + obj[key] = () => { + if (!value.length) { + value.push(compute(obj)); + } + return value[0]; + }; + }, + [obj] + ); +} + function uuid(model) { if (!(model in ID_CONTAINER)) { ID_CONTAINER[model] = 1; @@ -303,6 +327,22 @@ export class Base { } } +function getAllGetters(proto) { + const getterNames = new Set(); + const getters = new Set(); + while (proto !== null) { + const descriptors = Object.getOwnPropertyDescriptors(proto); + for (const [name, descriptor] of Object.entries(descriptors)) { + if (descriptor.get && !getterNames.has(name)) { + getterNames.add(name); + getters.add([name, descriptor.get]); + } + } + proto = Object.getPrototypeOf(proto); + } + return getters; +} + export function createRelatedModels(modelDefs, modelClasses = {}, opts = {}) { const indexes = opts.databaseIndex || {}; const database = opts.databaseTable || {}; @@ -457,29 +497,76 @@ export function createRelatedModels(modelDefs, modelClasses = {}, opts = {}) { ); } - const proxyTraps = {}; - function getProxyTrap(model) { - if (model in proxyTraps) { - return proxyTraps[model]; + const modelClassesAndProxyTrapsCache = {}; + function getClassAndProxyTrap(model) { + if (model in modelClassesAndProxyTrapsCache) { + return modelClassesAndProxyTrapsCache[model]; } const fields = getFields(model); const proxyTrap = { - set(target, prop, value) { + set(target, prop, value, receiver) { if (proxyTrapDisabled || !(prop in fields)) { - return Reflect.set(target, prop, value); + return Reflect.set(target, prop, value, receiver); } - const field = fields[prop]; - if (field && X2MANY_TYPES.has(field.type)) { - if (!isX2ManyCommands(value)) { - value = [["clear"], ["link", ...value]]; + const updateRecord = withoutProxyTrap(() => { + const field = fields[prop]; + if (field && X2MANY_TYPES.has(field.type)) { + if (!isX2ManyCommands(value)) { + value = [["clear"], ["link", ...value]]; + } } + receiver.update({ [prop]: value }); + return true; + }); + return updateRecord(); + }, + get(target, prop, receiver) { + if (proxyTrapDisabled || !getters.has(prop)) { + return Reflect.get(target, prop, receiver); } - target.update({ [prop]: value }); - return true; + const getLazyGetterValue = withoutProxyTrap(() => { + const [lazyName] = getters.get(prop); + // For a getter, we should get the value from the receiver. + // Because the receiver is linked to the reactivity. + // We want to read the getter from it to make sure that the getter + // is part of the reactivity as well. + // To avoid infinite recursion, we disable this proxy trap + // during the time the lazy getter is accessed. + return receiver[lazyName]; + }); + return getLazyGetterValue(); }, }; - proxyTraps[model] = proxyTrap; - return proxyTrap; + + const ModelClass = modelClasses[model] || Base; + const getters = new Map(); + for (const [name, func] of getAllGetters(ModelClass.prototype)) { + if (name.startsWith("__") && name.endsWith("__")) { + continue; + } + getters.set(name, [ + `__lazy_${name}`, + (obj) => { + return func.call(obj); + }, + ]); + } + class ModelWithLazyGetters extends ModelClass { + constructor(...args) { + const result = super(...args); + for (const [lazyName, func] of getters.values()) { + lazyComputed(this, lazyName, func); + } + return result; + } + } + modelClassesAndProxyTrapsCache[model] = [ModelWithLazyGetters, proxyTrap]; + return [ModelWithLazyGetters, proxyTrap]; + } + + function instantiateModel(model, { models, records }) { + const [Model, proxyTrap] = getClassAndProxyTrap(model); + return new Model({ models, records, model: models[model], proxyTrap }); } const create = withoutProxyTrap(_create); @@ -494,9 +581,7 @@ export function createRelatedModels(modelDefs, modelClasses = {}, opts = {}) { vals["id"] = uuid(model); } - const Model = modelClasses[model] || Base; - const proxyTrap = getProxyTrap(model); - const record = reactive(new Model({ models, records, model: models[model], proxyTrap })); + const record = reactive(instantiateModel(model, { models, records })); const id = vals["id"]; record.id = id; diff --git a/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt_screen.js b/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt_screen.js index 110e22e72593f..e29d5c338c216 100644 --- a/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt_screen.js +++ b/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt_screen.js @@ -49,7 +49,7 @@ export class ReceiptScreen extends Component { const tipLine = order .get_orderlines() .find((line) => tip_product_id && line.product_id.id === tip_product_id); - const tipAmount = tipLine ? tipLine.get_all_prices().priceWithTax : 0; + const tipAmount = tipLine ? tipLine.allPrices.priceWithTax : 0; const orderAmountStr = this.env.utils.formatCurrency(orderTotalAmount - tipAmount); if (!tipAmount) { return orderAmountStr; diff --git a/addons/pos_restaurant/static/src/app/models/restaurant_table.js b/addons/pos_restaurant/static/src/app/models/restaurant_table.js index 213605198da04..91d1b40429d3a 100644 --- a/addons/pos_restaurant/static/src/app/models/restaurant_table.js +++ b/addons/pos_restaurant/static/src/app/models/restaurant_table.js @@ -62,7 +62,7 @@ export class RestaurantTable extends Base { y: this.getY() + this.height / 2, }; } - get orders() { + getOrders() { return this.models["pos.order"].filter( (o) => o.table_id?.id === this.id && diff --git a/addons/pos_restaurant/static/src/app/services/pos_store.js b/addons/pos_restaurant/static/src/app/services/pos_store.js index 317996888c146..064c5c15436bd 100644 --- a/addons/pos_restaurant/static/src/app/services/pos_store.js +++ b/addons/pos_restaurant/static/src/app/services/pos_store.js @@ -219,11 +219,9 @@ patch(PosStore.prototype, { async setTable(table, orderUuid = null) { this.loadingOrderState = true; - const tableOrders = table.orders; - - let currentOrder = tableOrders.find((order) => - orderUuid ? order.uuid === orderUuid : !order.finalized - ); + let currentOrder = table + .getOrders() + .find((order) => (orderUuid ? order.uuid === orderUuid : !order.finalized)); if (currentOrder) { this.set_order(currentOrder); @@ -244,7 +242,7 @@ patch(PosStore.prototype, { this.loadingOrderState = true; const orders = await this.syncAllOrders({ throw: true }); const orderUuids = orders.map((order) => order.uuid); - for (const order of table.orders) { + for (const order of table.getOrders()) { if ( !orderUuids.includes(order.uuid) && typeof order.id === "number" &&