Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions mrp_bom_line_formula_quantity/models/mrp_bom_line.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Copyright 2024 Simone Rubino - Aion Tech
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import api, fields, models
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):
Expand All @@ -29,16 +31,46 @@ 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",
)
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,
Expand All @@ -55,6 +87,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(
Expand Down
39 changes: 32 additions & 7 deletions mrp_bom_line_formula_quantity/tests/test_mrp_bom_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")