diff --git a/.ruff.toml b/.ruff.toml index 5e6312820..25b6afcc6 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -19,6 +19,7 @@ exclude = ["setup/*"] [lint.per-file-ignores] "__init__.py" = ["F401", "I001"] # ignore unused and unsorted imports in __init__.py "__manifest__.py" = ["B018"] # useless expression +"mis_builder/models/mis_report_style.py" = ["C901"] # ignore complexity for compare_and_render method [lint.isort] section-order = ["future", "standard-library", "third-party", "odoo", "odoo-addons", "first-party", "local-folder"] diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 36fb5929e..58d3d4f6a 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -32,7 +32,15 @@ from .expression_evaluator import ExpressionEvaluator from .kpimatrix import KpiMatrix from .mis_kpi_data import ACC_AVG, ACC_NONE, ACC_SUM -from .mis_report_style import CMP_DIFF, CMP_NONE, CMP_PCT, TYPE_NUM, TYPE_PCT, TYPE_STR +from .mis_report_style import ( + CMP_DIFF, + CMP_NONE, + CMP_PCT, + CMP_PCT_NEG, + TYPE_NUM, + TYPE_PCT, + TYPE_STR, +) from .mis_safe_eval import DataError from .simple_array import SimpleArray, named_simple_array @@ -123,6 +131,7 @@ class MisReportKpi(models.Model): [ (CMP_DIFF, "Difference"), (CMP_PCT, "Percentage"), + (CMP_PCT_NEG, "Percentage (negative growth)"), (CMP_NONE, "None"), ], required=True, diff --git a/mis_builder/models/mis_report_style.py b/mis_builder/models/mis_report_style.py index cc1b2b829..5286447bf 100644 --- a/mis_builder/models/mis_report_style.py +++ b/mis_builder/models/mis_report_style.py @@ -44,6 +44,7 @@ def copy(self): # pylint: disable=copy-wo-api-one,method-required-super CMP_DIFF = "diff" CMP_PCT = "pct" +CMP_PCT_NEG = "pct_neg" # Percentage (negative growth) CMP_NONE = "none" @@ -241,7 +242,7 @@ def render_str(self, lang, value): return unicode(value) @api.model - def compare_and_render( + def compare_and_render( # pylint: disable=too-many-locals,too-complex self, lang, style_props, @@ -304,6 +305,23 @@ def compare_and_render( delta_type = TYPE_PCT else: delta = AccountingNone + elif compare_method == CMP_PCT_NEG: + if base_value and round(base_value, style_props.dp or 0) != 0: + # Calculate the percentage change + delta = (value - base_value) / abs(base_value) + if delta and round(delta, 3) != 0: + # For negative values, invert the growth logic + if base_value < 0: + # If the new value is more negative than base, + # it's negative growth + if value < base_value: + delta = -abs(delta) # Negative growth + else: + delta = abs(delta) # Positive growth + delta_style.update(dp=1) + delta_type = TYPE_PCT + else: + delta = AccountingNone if delta is not AccountingNone: delta_r = self.render(lang, delta_style, delta_type, delta, sign="+") return delta, delta_r, delta_style, delta_type diff --git a/mis_builder/tests/test_render.py b/mis_builder/tests/test_render.py index 43f5c2368..60d1843cb 100644 --- a/mis_builder/tests/test_render.py +++ b/mis_builder/tests/test_render.py @@ -5,7 +5,14 @@ from ..models.accounting_none import AccountingNone from ..models.data_error import DataError -from ..models.mis_report_style import CMP_DIFF, CMP_PCT, TYPE_NUM, TYPE_PCT, TYPE_STR +from ..models.mis_report_style import ( + CMP_DIFF, + CMP_PCT, + CMP_PCT_NEG, + TYPE_NUM, + TYPE_PCT, + TYPE_STR, +) class TestRendering(common.TransactionCase): @@ -184,6 +191,42 @@ def test_compare_pct_result_type(self): ) self.assertEqual(result[3], TYPE_NUM) + def test_compare_num_pct_neg(self): + """Test percentage (negative growth) comparison method.""" + # Case 1: Cost increases (more negative) = negative growth + # -100 to -114.6 should give -14.6% + result = self._compare_and_render(-114.6, -100, TYPE_NUM, CMP_PCT_NEG) + self.assertEqual((-0.146, "\u201114.6\xa0%"), result) + + # Case 2: Cost decreases (less negative) = positive growth + # -100 to -80 should give +20% + result = self._compare_and_render(-80, -100, TYPE_NUM, CMP_PCT_NEG) + self.assertEqual((0.2, "+20.0\xa0%"), result) + + # Case 3: Positive values (should work same as CMP_PCT) + result = self._compare_and_render(120, 100, TYPE_NUM, CMP_PCT_NEG) + self.assertEqual((0.2, "+20.0\xa0%"), result) + + # Case 4: From positive to negative + result = self._compare_and_render(-50, 100, TYPE_NUM, CMP_PCT_NEG) + self.assertEqual((-1.5, "\u2011150.0\xa0%"), result) + + # Case 5: Edge case - zero base value + result = self._compare_and_render(50, 0, TYPE_NUM, CMP_PCT_NEG) + self.assertEqual((AccountingNone, ""), result) + + # Case 6: Edge case - both zero + result = self._compare_and_render(0, 0, TYPE_NUM, CMP_PCT_NEG) + self.assertEqual((AccountingNone, ""), result) + + # Case 7: Small change detection + result = self._compare_and_render(-100.01, -100, TYPE_NUM, CMP_PCT_NEG) + self.assertEqual((AccountingNone, ""), result) + + # Case 8: Large negative growth + result = self._compare_and_render(-200, -100, TYPE_NUM, CMP_PCT_NEG) + self.assertEqual((-1.0, "\u2011100.0\xa0%"), result) + def test_merge(self): self.style.color = "#FF0000" self.style.color_inherit = False