From 174514e8bbfc4364908d875db21d1715d29cb831 Mon Sep 17 00:00:00 2001 From: "Joseph (jcb)" Date: Sat, 6 Jan 2024 12:22:48 +0100 Subject: [PATCH] [IMP] point_of_sale: lazy reactive getters This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: https://github.com/odoo/owl/pull/1499 --- .../static/src/app/models/pos_order.js | 2 +- .../static/src/app/models/pos_order_line.js | 28 +++-- .../static/src/app/models/related_models.js | 119 +++++++++++++++--- .../screens/receipt_screen/receipt_screen.js | 2 +- .../static/src/app/models/restaurant_table.js | 2 +- .../static/src/app/services/pos_store.js | 10 +- 6 files changed, 127 insertions(+), 36 deletions(-) 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 a132793b13b88..2d27d5bb6f286 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 @@ -669,7 +669,7 @@ export class PosOrder extends Base { getTaxDetails() { const taxDetails = {}; for (const line of this.lines) { - for (const taxData of line.getAllPrices().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 35890b2492d9a..1dfd329734d87 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 @@ -360,17 +360,17 @@ export class PosOrderline extends Base { getUnitDisplayPrice() { if (this.config.iface_tax_included === "total") { - return this.getAllPrices(1).priceWithTax; + return this.allUnitPrices.priceWithTax; } else { - return this.getAllPrices(1).priceWithoutTax; + return this.allUnitPrices.priceWithoutTax; } } getUnitDisplayPriceBeforeDiscount() { if (this.config.iface_tax_included === "total") { - return this.getAllPrices(1).priceWithTaxBeforeDiscount; + return this.allUnitPrices.priceWithTaxBeforeDiscount; } else { - return this.getAllPrices(1).priceWithoutTaxBeforeDiscount; + return this.allUnitPrices.priceWithoutTaxBeforeDiscount; } } getBasePrice() { @@ -419,19 +419,19 @@ export class PosOrderline extends Base { } getPriceWithoutTax() { - return this.getAllPrices().priceWithoutTax; + return this.allPrices.priceWithoutTax; } getPriceWithTax() { - return this.getAllPrices().priceWithTax; + return this.allPrices.priceWithTax; } getTax() { - return this.getAllPrices().tax; + return this.allPrices.tax; } getTaxDetails() { - return this.getAllPrices().taxDetails; + return this.allPrices.taxDetails; } getTotalTaxesIncludedInPrice() { @@ -513,6 +513,14 @@ export class PosOrderline extends Base { }; } + get allPrices() { + return this.get_all_prices(); + } + + get allUnitPrices() { + return this.get_all_prices(1); + } + displayDiscountPolicy() { // 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 @@ -581,11 +589,11 @@ export class PosOrderline extends Base { getComboTotalPrice() { const allLines = this.getAllLinesInCombo(); - return allLines.reduce((total, line) => total + line.getAllPrices(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.getAllPrices(1).priceWithoutTax, 0); + return allLines.reduce((total, line) => total + line.allUnitPrices.priceWithoutTax, 0); } getOldUnitDisplayPrice() { 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 e3df4cadf20f2..7da5bdeecd2ab 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 .getOrderlines() .find((line) => tip_product_id && line.product_id.id === tip_product_id); - const tipAmount = tipLine ? tipLine.getAllPrices().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 61d1bf0f4f885..4ae83d6cfbe94 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.setOrder(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" &&