From 42c79d3afc5af259b5b2c42099aa7a31ee0d4f95 Mon Sep 17 00:00:00 2001 From: miquelrosell99 <38435442+miquelrosell99@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:00:34 +0200 Subject: [PATCH 1/2] [IMP] kpi: expose dateutil in safe_eval context Allows KPI Python expressions to reference the dateutil module, which is essential for date arithmetic (e.g. relativedelta) in KPI formulas. --- kpi/models/kpi.py | 5 +++-- kpi/tests/test_kpi.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/kpi/models/kpi.py b/kpi/models/kpi.py index 7fb5e33f15..c91661979d 100644 --- a/kpi/models/kpi.py +++ b/kpi/models/kpi.py @@ -9,7 +9,7 @@ from odoo import api, fields, models from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT -from odoo.tools.safe_eval import safe_eval +from odoo.tools.safe_eval import dateutil, safe_eval _logger = logging.getLogger(__name__) @@ -164,7 +164,8 @@ def _get_kpi_value(self): kpi_value = res[0]["value"] elif self.kpi_type == "python": kpi_value = safe_eval( - self.kpi_code, {"self": self, "datetime": fields.datetime} + self.kpi_code, + {"self": self, "datetime": fields.datetime, "dateutil": dateutil}, ) if isinstance(kpi_value, dict): res = kpi_value diff --git a/kpi/tests/test_kpi.py b/kpi/tests/test_kpi.py index 4953efe3b2..1b86543ae4 100644 --- a/kpi/tests/test_kpi.py +++ b/kpi/tests/test_kpi.py @@ -127,3 +127,25 @@ def test_kpi_python(self): self.assertEqual(len(kpi_history), 1) self.assertEqual(kpi_history.color, "#00FF00") self.assertEqual(kpi_history.value, 1.0) + + def test_kpi_python_dateutil(self): + kpi_category = self.env["kpi.category"].create({"name": "Dynamic KPIs"}) + kpi_threshold = self.env["kpi.threshold"].create( + {"name": "KPI Threshold for dynamic KPIs"} + ) + kpi = self.env["kpi"].create( + { + "name": "KPI with dateutil", + "description": "KPI using dateutil in python code", + "category_id": kpi_category.id, + "threshold_id": kpi_threshold.id, + "periodicity": 1, + "periodicity_uom": "day", + "kpi_type": "python", + "kpi_code": "dateutil.relativedelta.relativedelta(days=1).days", + } + ) + kpi.update_kpi_value() + kpi_history = self.env["kpi.history"].search([("kpi_id", "=", kpi.id)]) + self.assertEqual(len(kpi_history), 1) + self.assertEqual(kpi_history.value, 1.0) From 12891e9f5b5cdd6873add8af53db41579cac7fd5 Mon Sep 17 00:00:00 2001 From: miquelrosell99 <38435442+miquelrosell99@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:01:27 +0200 Subject: [PATCH 2/2] [IMP] kpi: expose mean function in safe_eval context Allows KPI Python expressions to use the mean() function from the statistics module, making it easy to compute averages in KPI formulas without requiring a custom module. --- kpi/models/kpi.py | 8 +++++++- kpi/tests/test_kpi.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/kpi/models/kpi.py b/kpi/models/kpi.py index c91661979d..0d22d4b697 100644 --- a/kpi/models/kpi.py +++ b/kpi/models/kpi.py @@ -4,6 +4,7 @@ import logging import re from datetime import datetime +from statistics import mean from dateutil.relativedelta import relativedelta @@ -165,7 +166,12 @@ def _get_kpi_value(self): elif self.kpi_type == "python": kpi_value = safe_eval( self.kpi_code, - {"self": self, "datetime": fields.datetime, "dateutil": dateutil}, + { + "self": self, + "datetime": fields.datetime, + "dateutil": dateutil, + "mean": mean, + }, ) if isinstance(kpi_value, dict): res = kpi_value diff --git a/kpi/tests/test_kpi.py b/kpi/tests/test_kpi.py index 1b86543ae4..e6aefd9de9 100644 --- a/kpi/tests/test_kpi.py +++ b/kpi/tests/test_kpi.py @@ -149,3 +149,25 @@ def test_kpi_python_dateutil(self): kpi_history = self.env["kpi.history"].search([("kpi_id", "=", kpi.id)]) self.assertEqual(len(kpi_history), 1) self.assertEqual(kpi_history.value, 1.0) + + def test_kpi_python_mean(self): + kpi_category = self.env["kpi.category"].create({"name": "Dynamic KPIs"}) + kpi_threshold = self.env["kpi.threshold"].create( + {"name": "KPI Threshold for dynamic KPIs"} + ) + kpi = self.env["kpi"].create( + { + "name": "KPI with mean", + "description": "KPI using mean in python code", + "category_id": kpi_category.id, + "threshold_id": kpi_threshold.id, + "periodicity": 1, + "periodicity_uom": "day", + "kpi_type": "python", + "kpi_code": "mean([1.0, 2.0, 3.0])", + } + ) + kpi.update_kpi_value() + kpi_history = self.env["kpi.history"].search([("kpi_id", "=", kpi.id)]) + self.assertEqual(len(kpi_history), 1) + self.assertEqual(kpi_history.value, 2.0)