From 148495e6dcc4df06351ab74b7bb2aedf48615924 Mon Sep 17 00:00:00 2001 From: Carlos Roca Date: Tue, 21 Nov 2023 15:18:34 +0100 Subject: [PATCH 1/2] [FIX] web_pivot_computed_measure: Use js eval to avoid different types operation error --- .../static/src/helpers/utils.esm.js | 27 +++++++++++++++++++ .../static/src/pivot/pivot_model.esm.js | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 web_pivot_computed_measure/static/src/helpers/utils.esm.js diff --git a/web_pivot_computed_measure/static/src/helpers/utils.esm.js b/web_pivot_computed_measure/static/src/helpers/utils.esm.js new file mode 100644 index 000000000..c50480220 --- /dev/null +++ b/web_pivot_computed_measure/static/src/helpers/utils.esm.js @@ -0,0 +1,27 @@ +/** @odoo-module **/ +/* Copyright 2022 Tecnativa - Carlos Roca + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ + +/** + * Helper function to eval text for a given object + * + * @param {String} text + * @param {Object} vals + * @returns {any} + */ +export const evalOperation = (text, vals) => { + for (const variable in vals) { + if (vals.hasOwnProperty(variable)) { + const regex = new RegExp(variable, "g"); + text = text.replace(regex, vals[variable]); + } + } + try { + // eslint-disable-next-line no-eval + const res = eval(text); + return res; + } catch (error) { + console.error("Error trying to eval operation:", error); + return; + } +}; diff --git a/web_pivot_computed_measure/static/src/pivot/pivot_model.esm.js b/web_pivot_computed_measure/static/src/pivot/pivot_model.esm.js index e7172b1bd..76f02bb69 100644 --- a/web_pivot_computed_measure/static/src/pivot/pivot_model.esm.js +++ b/web_pivot_computed_measure/static/src/pivot/pivot_model.esm.js @@ -6,6 +6,7 @@ import {PivotModel} from "@web/views/pivot/pivot_model"; import {patch} from "web.utils"; import {computeReportMeasures} from "@web/views/helpers/utils"; +import {evalOperation} from "../helpers/utils.esm"; patch(PivotModel.prototype, "web_pivot_computed_measure.PivotModel", { /** @@ -148,7 +149,7 @@ patch(PivotModel.prototype, "web_pivot_computed_measure.PivotModel", { subGroupData[cm.id] = false; } else { // eslint-disable-next-line no-undef - subGroupData[cm.id] = py.eval(cm.operation, subGroupData); + subGroupData[cm.id] = evalOperation(cm.operation, subGroupData); } } }, From e97a883a6f2fc6bd2cb0cfa141a4f109cca62cf2 Mon Sep 17 00:00:00 2001 From: Carlos Roca Date: Mon, 27 Nov 2023 11:09:47 +0100 Subject: [PATCH 2/2] [IMP] web_pivot_computed_measure: Function that eval operations --- .../static/src/helpers/utils.esm.js | 131 +++++++++++++++--- 1 file changed, 114 insertions(+), 17 deletions(-) diff --git a/web_pivot_computed_measure/static/src/helpers/utils.esm.js b/web_pivot_computed_measure/static/src/helpers/utils.esm.js index c50480220..df95c84bc 100644 --- a/web_pivot_computed_measure/static/src/helpers/utils.esm.js +++ b/web_pivot_computed_measure/static/src/helpers/utils.esm.js @@ -1,27 +1,124 @@ /** @odoo-module **/ -/* Copyright 2022 Tecnativa - Carlos Roca +/* Copyright 2023 Tecnativa - Carlos Roca * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ /** - * Helper function to eval text for a given object + * Function that traverse the text given character by character * * @param {String} text - * @param {Object} vals - * @returns {any} + * @returns {Array} */ -export const evalOperation = (text, vals) => { - for (const variable in vals) { - if (vals.hasOwnProperty(variable)) { - const regex = new RegExp(variable, "g"); - text = text.replace(regex, vals[variable]); +function getTokensFromText(text) { + const symbols = ["+", "-", "*", "/", "(", ")"]; + const tokens = []; + let token = ""; + for (let i = 0; i < text.length; i++) { + const c = text[i]; + if (c === " ") continue; + if (symbols.includes(c)) { + if (token !== "") { + tokens.push(token); + token = ""; + } + tokens.push(c); + } else { + token += c; } } - try { - // eslint-disable-next-line no-eval - const res = eval(text); - return res; - } catch (error) { - console.error("Error trying to eval operation:", error); - return; + if (token !== "") { + tokens.push(token); } -}; + return tokens; +} + +/** + * Function that executes an operation between the last two operands in the operands stack + * and the last operator in the operators stack, and saves the result in the operands stack. + * + * @param {Array} operands + * @param {Array} operators + */ +function executeOperation(operands, operators) { + const b = operands.pop(); + const a = operands.pop(); + const op = operators.pop(); + switch (op) { + case "+": + operands.push(a + b); + break; + case "-": + operands.push(a - b); + break; + case "*": + operands.push(a * b); + break; + case "/": + operands.push(a / b); + break; + } +} + +/** + * Function that returns the precedence of an operator + * + * @param {String} op + * @returns {Number} + */ +function precedence(op) { + if (op === "+" || op === "-") { + return 1; + } + if (op === "*" || op === "/") { + return 2; + } + if (op === "(" || op === ")") { + return 0; + } +} + +/** + * Helper function that takes a mathematical expression in text form and an object + * of variable values, evaluates the expression, and returns the result. + * + * @param {String} text + * @param {Object} values + * @returns {any} + */ +export function evalOperation(text, values) { + const tokens = getTokensFromText(text); + const operands = []; + const operators = []; + for (const token of tokens) { + if (!isNaN(token)) { + // If the token is a number, convert it to a number and add it to the operands stack + operands.push(Number(token)); + } else if (token in values) { + // If the token is a variable, get its value from the object and add it to the operands stack + operands.push(values[token]); + } else if (token === "(") { + // If the token is an open parenthesis, add it to the operators stack + operators.push(token); + } else if (token === ")") { + // If the token is a closing parenthesis, pop and execute operators from the stack until an open parenthesis is found + while (operators.length > 0 && operators[operators.length - 1] !== "(") { + executeOperation(operands, operators); + } + // Pop the open parenthesis from the operators stack + operators.pop(); + } else { + // If the token is an operator, pop and execute operators from the stack while they have equal or higher precedence than the token + while ( + operators.length > 0 && + precedence(operators[operators.length - 1]) >= precedence(token) + ) { + executeOperation(operands, operators); + } + // Add the token to the operators stack + operators.push(token); + } + } + while (operators.length > 0) { + executeOperation(operands, operators); + } + return operands.pop(); +}