Skip to content

Commit

Permalink
[IMP] point_of_sale: lazy reactive getters
Browse files Browse the repository at this point in the history
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: odoo/owl#1499
  • Loading branch information
caburj committed Nov 18, 2024
1 parent 3b2a26c commit d729ca3
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 37 deletions.
2 changes: 1 addition & 1 deletion addons/point_of_sale/static/src/app/models/pos_order.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
30 changes: 19 additions & 11 deletions addons/point_of_sale/static/src/app/models/pos_order_line.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
119 changes: 102 additions & 17 deletions addons/point_of_sale/static/src/app/models/related_models.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 || {};
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
10 changes: 4 additions & 6 deletions addons/pos_restaurant/static/src/app/services/pos_store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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" &&
Expand Down

0 comments on commit d729ca3

Please sign in to comment.