From ed44ef7a2277a5df0bde9c773359269f2877aae6 Mon Sep 17 00:00:00 2001 From: bosd Date: Fri, 12 Sep 2025 18:40:00 +0200 Subject: [PATCH 1/3] [IMP] mrp_bom_line_formula_quantity: allow using ceil and floor --- mrp_bom_line_formula_quantity/models/mrp_bom_line.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mrp_bom_line_formula_quantity/models/mrp_bom_line.py b/mrp_bom_line_formula_quantity/models/mrp_bom_line.py index ebb468a7803..3e1f62fbce1 100644 --- a/mrp_bom_line_formula_quantity/models/mrp_bom_line.py +++ b/mrp_bom_line_formula_quantity/models/mrp_bom_line.py @@ -1,9 +1,11 @@ # Copyright 2024 Simone Rubino - Aion Tech # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import math + from odoo import api, fields, models from odoo.exceptions import ValidationError -from odoo.tools.safe_eval import safe_eval, test_python_expr +from odoo.tools.safe_eval import safe_eval, test_python_expr, wrap_module class MRPBomLine(models.Model): @@ -55,6 +57,7 @@ def _quantity_formula_values( "product_uom": product_uom, "product_uom_qty": product_uom_qty, "production": production, + "math": wrap_module(math, ["ceil", "floor"]), } def _eval_quantity_formula( From 7a1325e66c85d3385bde36e0c3b8f2d2d2055e76 Mon Sep 17 00:00:00 2001 From: bosd Date: Fri, 12 Sep 2025 18:42:30 +0200 Subject: [PATCH 2/3] [IMP] mrp_bom_line_formula_quantity: Validate the formula on save --- .../models/mrp_bom_line.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/mrp_bom_line_formula_quantity/models/mrp_bom_line.py b/mrp_bom_line_formula_quantity/models/mrp_bom_line.py index 3e1f62fbce1..acc928061cd 100644 --- a/mrp_bom_line_formula_quantity/models/mrp_bom_line.py +++ b/mrp_bom_line_formula_quantity/models/mrp_bom_line.py @@ -3,7 +3,7 @@ import math -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.exceptions import ValidationError from odoo.tools.safe_eval import safe_eval, test_python_expr, wrap_module @@ -31,9 +31,14 @@ class MRPBomLine(models.Model): "quantity_formula", ) def _constrain_quantity_formula(self): + # We also need to add all modules we use for validation in this list. + # This will be used in the _quantity_formula_values method. + # This will allow us to test the formula right away before saving it. + # This is a very robust way of validating the formula. for line in self: quantity_formula = line.quantity_formula if quantity_formula: + # First, check for syntax errors error_message = test_python_expr( expr=quantity_formula, mode="exec", @@ -41,6 +46,31 @@ def _constrain_quantity_formula(self): if error_message: raise ValidationError(error_message) + # Then, check for runtime errors by trying to evaluate the formula + # with a simple test case. We're using a try/except block here + # to catch any errors that occur during the evaluation. + try: + test_values = line._quantity_formula_values( + product=self.env["product.product"], + product_uom=self.env["uom.uom"], + product_uom_qty=1.0, + production=self.env["mrp.production"], + ) + safe_eval( + quantity_formula, + globals_dict=test_values, + mode="exec", + nocopy=True, + ) + except Exception as e: + raise ValidationError( + _( + "The formula is invalid. " + "A runtime error occurred during test evaluation: %s" + ) + % e + ) from e + def _quantity_formula_values( self, product, From 625e76f68979a31a454ec35bfea6c38cbcdaf92b Mon Sep 17 00:00:00 2001 From: bosd Date: Fri, 12 Sep 2025 19:04:13 +0200 Subject: [PATCH 3/3] [IMP] mrp_bom_line_formula_quantity: update tests --- .../tests/test_mrp_bom_line.py | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/mrp_bom_line_formula_quantity/tests/test_mrp_bom_line.py b/mrp_bom_line_formula_quantity/tests/test_mrp_bom_line.py index 8455701ab53..0b1c0625362 100644 --- a/mrp_bom_line_formula_quantity/tests/test_mrp_bom_line.py +++ b/mrp_bom_line_formula_quantity/tests/test_mrp_bom_line.py @@ -8,16 +8,41 @@ class TestMRPBoMLine(TestMrpCommon): - def test_formula_validation(self): - """The formula of a BoM line is checked for not permitted operations.""" + def test_formula_syntax_validation(self): + """The formula of a BoM line is checked for invalid Python syntax.""" # Arrange bom = self.bom_1.copy() bom_line = first(bom.bom_line_ids) - # Act + # Act & Assert for an invalid syntax with self.assertRaises(ValidationError) as ve: - bom_line["quantity_formula"] = "import *" - exc_message = ve.exception.args[0] + bom_line.quantity_formula = "import *" + self.assertIn("invalid syntax", ve.exception.args[0]) - # Assert - self.assertIn("invalid syntax", exc_message) + def test_formula_runtime_validation(self): + """The formula of a BoM line is checked for runtime errors.""" + # Arrange + bom = self.bom_1.copy() + bom_line = first(bom.bom_line_ids) + + # Act & Assert for an invalid function call (NameError) + with self.assertRaises(ValidationError) as ve: + bom_line.quantity_formula = ( + "quantity = math.nonexistent_function(product_uom_qty)" + ) + self.assertIn("runtime error", ve.exception.args[0]) + self.assertIn( + "object has no attribute 'nonexistent_function'", ve.exception.args[0] + ) + + def test_formula_valid(self): + """The formula of a BoM line is correctly validated for a valid function.""" + # Arrange + bom = self.bom_1.copy() + bom_line = first(bom.bom_line_ids) + + # Act & Assert for a valid formula with math.ceil() + try: + bom_line.quantity_formula = "quantity = math.ceil(product_uom_qty)" + except ValidationError: + self.fail("Valid formula should not raise ValidationError")