From 83d0cd5a473fe7eed2479c60bca49a1ffcfc4a1f Mon Sep 17 00:00:00 2001
From: Asta
Date: Wed, 19 Nov 2025 16:28:57 +0200
Subject: [PATCH 01/10] [MIG] mis_builder: Migration to 19.0
---
.pre-commit-config.yaml | 1 -
mis_builder/README.rst | 10 +--
mis_builder/__manifest__.py | 6 +-
.../migrations/18.0.1.3.0/post-migration.py | 40 -----------
mis_builder/models/aep.py | 16 +++--
mis_builder/models/mis_kpi_data.py | 4 +-
mis_builder/models/mis_report.py | 40 +++++------
mis_builder/models/mis_report_instance.py | 49 +++++++-------
.../models/mis_report_instance_annotation.py | 11 ++-
mis_builder/models/mis_report_style.py | 7 +-
mis_builder/models/mis_report_subreport.py | 23 +++----
mis_builder/models/mis_safe_eval.py | 13 ++--
.../models/prorata_read_group_mixin.py | 8 +--
mis_builder/security/res_groups.xml | 4 +-
mis_builder/static/description/index.html | 6 +-
mis_builder/tests/test_aep.py | 67 ++++++++++---------
mis_builder/tests/test_kpi_data.py | 12 ++--
mis_builder/tests/test_mis_report_instance.py | 5 +-
mis_builder/tests/test_pro_rata_read_group.py | 38 +++++------
mis_builder/tests/test_render.py | 12 ++--
20 files changed, 168 insertions(+), 204 deletions(-)
delete mode 100644 mis_builder/migrations/18.0.1.3.0/post-migration.py
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 699237bad..b78c71d93 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,6 @@
exclude: |
(?x)
# NOT INSTALLABLE ADDONS
- ^mis_builder/|
^mis_builder_budget/|
^mis_builder_demo/|
# END NOT INSTALLABLE ADDONS
diff --git a/mis_builder/README.rst b/mis_builder/README.rst
index 41e162b45..1b9e91296 100644
--- a/mis_builder/README.rst
+++ b/mis_builder/README.rst
@@ -21,13 +21,13 @@ MIS Builder
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmis--builder-lightgray.png?logo=github
- :target: https://github.com/OCA/mis-builder/tree/18.0/mis_builder
+ :target: https://github.com/OCA/mis-builder/tree/19.0/mis_builder
:alt: OCA/mis-builder
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
- :target: https://translation.odoo-community.org/projects/mis-builder-18-0/mis-builder-18-0-mis_builder
+ :target: https://translation.odoo-community.org/projects/mis-builder-19-0/mis-builder-19-0-mis_builder
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
- :target: https://runboat.odoo-community.org/builds?repo=OCA/mis-builder&target_branch=18.0
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/mis-builder&target_branch=19.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
@@ -723,7 +723,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues `_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
-`feedback `_.
+`feedback `_.
Do not contact contributors directly about support or help with technical issues.
@@ -790,6 +790,6 @@ Current `maintainer `__:
|maintainer-sbidoul|
-This module is part of the `OCA/mis-builder `_ project on GitHub.
+This module is part of the `OCA/mis-builder `_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/mis_builder/__manifest__.py b/mis_builder/__manifest__.py
index c4ec8425d..0c86f180e 100644
--- a/mis_builder/__manifest__.py
+++ b/mis_builder/__manifest__.py
@@ -3,12 +3,12 @@
{
"name": "MIS Builder",
- "version": "18.0.1.8.0",
+ "version": "19.0.1.0.0",
"category": "Reporting",
"summary": """
Build 'Management Information System' Reports and Dashboards
""",
- "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)",
+ "author": "ACSONE SA/NV, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/mis-builder",
"depends": [
"account",
@@ -41,7 +41,7 @@
],
},
"qweb": ["static/src/xml/mis_report_widget.xml"],
- "installable": False,
+ "installable": True,
"application": True,
"license": "AGPL-3",
"development_status": "Production/Stable",
diff --git a/mis_builder/migrations/18.0.1.3.0/post-migration.py b/mis_builder/migrations/18.0.1.3.0/post-migration.py
deleted file mode 100644
index 0aca6e58d..000000000
--- a/mis_builder/migrations/18.0.1.3.0/post-migration.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Copyright 2025 ForgeFlow S.L.
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-
-from openupgradelib import openupgrade
-
-from odoo.tools.safe_eval import safe_eval
-
-
-@openupgrade.migrate()
-def migrate(cr, version):
- """Update the value of the analytic_domain field."""
- # Workaround to execute the migration script without errors
- # see https://github.com/odoo/odoo/blob/2a839ef1ed09c36f27ce7536ca3052d9f65ceed9/odoo/modules/migration.py#L252-L256
- env = cr
- for record in env["mis.report.instance.period"].search(
- [("analytic_domain", "!=", False)]
- ):
- new_domain = _update_domain(record)
- record.write({"analytic_domain": new_domain})
-
- for record in env["mis.report.instance"].search([("analytic_domain", "!=", False)]):
- new_domain = _update_domain(record)
- record.write({"analytic_domain": new_domain})
-
-
-def _update_domain(record):
- # analytic_distribution_search has been removed in v18 and it was set on purpose
- # on mis_builder migration scripts in 16.0.
- domain = safe_eval(record.analytic_domain)
- new_domain = []
- for clause in domain:
- if (
- isinstance(clause, list | tuple)
- and clause[0] == "analytic_distribution_search"
- ):
- operator = clause[1]
- value = clause[2]
- clause = ("distribution_analytic_account_ids", operator, value)
- new_domain.append(clause)
- return new_domain
diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py
index bb722b583..94bc33859 100644
--- a/mis_builder/models/aep.py
+++ b/mis_builder/models/aep.py
@@ -7,7 +7,7 @@
from odoo import fields
from odoo.exceptions import UserError
-from odoo.models import expression
+from odoo.fields import Domain
from odoo.tools.float_utils import float_is_zero
from odoo.tools.safe_eval import datetime, dateutil, safe_eval, time
@@ -184,7 +184,7 @@ def _account_codes_to_domain(self, account_codes):
elems.append([("code", "=like", account_code)])
else:
elems.append([("code", "=", account_code)])
- return tuple(expression.OR(elems))
+ return tuple(Domain.OR(elems))
def _parse_match_object(self, mo):
"""Split a match object corresponding to an accounting variable
@@ -285,7 +285,7 @@ def done_parsing(self):
# separately.
account_ids = []
for company in self.companies:
- acc_domain_with_company = expression.AND(
+ acc_domain_with_company = Domain.AND(
[acc_domain, [("company_ids", "=", company.id)]]
)
account_ids += (
@@ -328,7 +328,7 @@ def get_aml_domain_for_expr(self, expr, date_from, date_to, account_id=None):
account_ids = set()
account_ids.update(self._account_ids_by_acc_domain[acc_domain])
if not account_id:
- aml_domain.append(("account_id", "in", tuple(account_ids)))
+ aml_domain.append(("account_id", "in", list(account_ids)))
else:
# filter on account_id
if account_id in account_ids:
@@ -341,7 +341,7 @@ def get_aml_domain_for_expr(self, expr, date_from, date_to, account_id=None):
aml_domain.append(("debit", "<>", 0.0))
elif fld_name:
aml_domain.append((fld_name, "!=", False))
- aml_domains.append(expression.normalize_domain(aml_domain))
+ aml_domains.append(aml_domain)
if mode not in date_domain_by_mode:
date_domain_by_mode[mode] = self.get_aml_domain_for_dates(
date_from, date_to, mode
@@ -349,7 +349,9 @@ def get_aml_domain_for_expr(self, expr, date_from, date_to, account_id=None):
assert aml_domains
# TODO we could do this for more precision:
# AND(OR(aml_domains[mode]), date_domain[mode]) for each mode
- return expression.OR(aml_domains) + expression.OR(date_domain_by_mode.values())
+ return Domain.AND(
+ [Domain.OR(aml_domains), Domain.OR(list(date_domain_by_mode.values()))]
+ )
def get_aml_domain_for_dates(self, date_from, date_to, mode):
if mode == self.MODE_VARIATION:
@@ -384,7 +386,7 @@ def get_aml_domain_for_dates(self, date_from, date_to, mode):
("date", "<", fields.Date.to_string(fy_date_from)),
("account_id.include_initial_balance", "=", False),
]
- return expression.normalize_domain(domain)
+ return Domain(domain)
def _get_company_rates(self, date):
# get exchange rates for each company with its rouding
diff --git a/mis_builder/models/mis_kpi_data.py b/mis_builder/models/mis_kpi_data.py
index 0b1143e3b..5e8fbd97b 100644
--- a/mis_builder/models/mis_kpi_data.py
+++ b/mis_builder/models/mis_kpi_data.py
@@ -5,7 +5,7 @@
from odoo import api, fields, models
from odoo.exceptions import UserError
-from odoo.osv import expression
+from odoo.fields import Domain
ACC_SUM = "sum"
ACC_AVG = "avg"
@@ -79,7 +79,7 @@ def _query_kpi_data(self, date_from, date_to, base_domain):
dt_to = fields.Date.from_string(date_to)
# all data items within or overlapping [date_from, date_to]
date_domain = [("date_from", "<=", date_to), ("date_to", ">=", date_from)]
- domain = expression.AND([date_domain, base_domain])
+ domain = Domain(date_domain) & Domain(base_domain)
res = defaultdict(float)
res_avg = defaultdict(list)
for item in self.search(domain):
diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py
index c4442f239..5405cc325 100644
--- a/mis_builder/models/mis_report.py
+++ b/mis_builder/models/mis_report.py
@@ -13,6 +13,7 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
+from odoo.fields import Domain
from odoo.tools.safe_eval import (
datetime as safe_datetime,
)
@@ -292,13 +293,10 @@ class MisReportKpiExpression(models.Model):
# TODO FIXME set readonly=True when onchange('subkpi_ids') below works
subkpi_id = fields.Many2one("mis.report.subkpi", readonly=False, ondelete="cascade")
- _sql_constraints = [
- (
- "subkpi_kpi_unique",
- "unique(subkpi_id, kpi_id)",
- "Sub KPI must be used once and only once for each KPI",
- )
- ]
+ _subkpi_kpi_unique = models.Constraint(
+ "unique(subkpi_id, kpi_id)",
+ "Sub KPI must be used once and only once for each KPI",
+ )
@api.depends(
"kpi_id.description",
@@ -324,22 +322,18 @@ def _compute_display_name(self):
def _search_display_name(self, operator, value):
if "." in value:
kpi_name, subkpi_name = value.split(".", 1)
- name_search_domain = [
- "|",
- "|",
- "&",
- ("kpi_id.name", "=", kpi_name),
- ("subkpi_id.name", operator, subkpi_name),
- ("kpi_id.description", operator, value),
- ("subkpi_id.description", operator, value),
- ]
+ name_search_domain = (
+ (
+ Domain("kpi_id.name", "=", kpi_name)
+ & Domain("subkpi_id.name", operator, subkpi_name)
+ )
+ | Domain("kpi_id.description", operator, value)
+ | Domain("subkpi_id.description", operator, value)
+ )
else:
- name_search_domain = [
- "|",
- ("kpi_id.name", operator, value),
- ("kpi_id.description", operator, value),
- ]
-
+ name_search_domain = Domain("kpi_id.name", operator, value) | Domain(
+ "kpi_id.description", operator, value
+ )
return name_search_domain
@@ -619,7 +613,7 @@ def _fetch_queries(self, date_from, date_to, get_additional_query_filter=None):
v = data[0][field_name]
except KeyError:
_logger.error(
- "field %s not found in read_group " "for %s; not summable?",
+ "field %s not found in read_group for %s; not summable?",
field_name,
model._name,
)
diff --git a/mis_builder/models/mis_report_instance.py b/mis_builder/models/mis_report_instance.py
index 9acf6fe63..b56a97be9 100644
--- a/mis_builder/models/mis_report_instance.py
+++ b/mis_builder/models/mis_report_instance.py
@@ -10,6 +10,7 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
+from odoo.fields import Domain
from .aep import AccountingExpressionProcessor as AEP
from .expression_evaluator import ExpressionEvaluator
@@ -289,19 +290,20 @@ def _compute_dates(self):
_order = "sequence, id"
- _sql_constraints = [
- ("duration", "CHECK (duration>0)", "Wrong duration, it must be positive!"),
- (
- "normalize_factor",
- "CHECK (normalize_factor>0)",
- "Wrong normalize factor, it must be positive!",
- ),
- (
- "name_unique",
- "unique(name, report_instance_id)",
- "Period name should be unique by report",
- ),
- ]
+ _duration = models.Constraint(
+ "CHECK(duration > 0)",
+ "Wrong duration, it must be positive!",
+ )
+
+ _normalize_factor = models.Constraint(
+ "CHECK(normalize_factor > 0)",
+ "Wrong normalize factor, it must be positive!",
+ )
+
+ _name_unique = models.Constraint(
+ "unique(name, report_instance_id)",
+ "Period name should be unique by report",
+ )
@api.depends("source", "report_instance_id.report_id.move_lines_source")
def _compute_source_aml_model_id(self):
@@ -423,8 +425,7 @@ def _check_mode_source(self):
if rec.mode == MODE_NONE:
raise DateFilterRequired(
self.env._(
- "A date filter is mandatory for this source "
- "in column %s.",
+ "A date filter is mandatory for this source in column %s.",
rec.name,
)
)
@@ -432,8 +433,7 @@ def _check_mode_source(self):
if rec.mode != MODE_NONE:
raise DateFilterForbidden(
self.env._(
- "No date filter is allowed for this source "
- "in column %s.",
+ "No date filter is allowed for this source in column %s.",
rec.name,
)
)
@@ -460,8 +460,7 @@ def _check_source_cmpcol(self):
):
raise ValidationError(
self.env._(
- "Columns to compare must belong to the same report "
- "in %s",
+ "Columns to compare must belong to the same report in %s",
rec.name,
)
)
@@ -499,7 +498,7 @@ def _compute_pivot_date(self):
sequence = fields.Integer(default=10)
description = fields.Char(related="report_id.description")
date = fields.Date(
- string="Base date", help="Report base date " "(leave empty to use current date)"
+ string="Base date", help="Report base date (leave empty to use current date)"
)
pivot_date = fields.Date(compute="_compute_pivot_date")
report_id = fields.Many2one("mis.report", required=True, string="Report")
@@ -765,9 +764,7 @@ def get_views(self, views, options=None):
context.get("from_dashboard")
and context.get("active_model") == "mis.report.instance"
):
- view_id = self.env.ref(
- "mis_builder." "mis_report_instance_result_view_form"
- )
+ view_id = self.env.ref("mis_builder.mis_report_instance_result_view_form")
mis_report_form_view = view_id and [view_id.id, "form"]
for view in views:
if view and view[1] == "form":
@@ -778,7 +775,7 @@ def get_views(self, views, options=None):
def preview(self):
self.ensure_one()
- view_id = self.env.ref("mis_builder." "mis_report_instance_result_view_form")
+ view_id = self.env.ref("mis_builder.mis_report_instance_result_view_form")
return {
"type": "ir.actions.act_window",
"res_model": "mis.report.instance",
@@ -980,7 +977,9 @@ def drilldown(self, arg):
period.date_to,
account_id,
)
- domain.extend(period._get_additional_move_line_filter())
+ additional_domain = period._get_additional_move_line_filter()
+ if additional_domain:
+ domain = Domain.AND([domain, additional_domain])
views = self._get_drilldown_model_views(period.source_aml_model_name)
return {
"name": self._get_drilldown_action_name(arg),
diff --git a/mis_builder/models/mis_report_instance_annotation.py b/mis_builder/models/mis_report_instance_annotation.py
index 46dd0c620..1f33cc174 100644
--- a/mis_builder/models/mis_report_instance_annotation.py
+++ b/mis_builder/models/mis_report_instance_annotation.py
@@ -63,11 +63,10 @@ def _get_first_matching_annotation(self, cell_id, instance_id):
.browse(instance_id)
._get_annotation_context()
)
- annotation = fields.first(
- annotations.filtered(
- lambda rec: rec.annotation_context == annotation_context
- )
+ annotation_rec = annotations.filtered(
+ lambda rec: rec.annotation_context == annotation_context
)
+ annotation = next(iter(annotation_rec), annotation_rec)
return annotation
@api.model
@@ -78,7 +77,7 @@ def set_annotation(self, cell_id, instance_id, note):
.user_can_edit_annotation
):
raise AccessError(
- self.env._("You do not have the rights to edit" " annotations")
+ self.env._("You do not have the rights to edit annotations")
)
annotation = self._get_first_matching_annotation(cell_id, instance_id)
@@ -109,7 +108,7 @@ def remove_annotation(self, cell_id, instance_id):
.user_can_edit_annotation
):
raise AccessError(
- self.env._("You do not have the" " rights to edit annotations")
+ self.env._("You do not have the rights to edit annotations")
)
annotation = self._get_first_matching_annotation(cell_id, instance_id)
diff --git a/mis_builder/models/mis_report_style.py b/mis_builder/models/mis_report_style.py
index 287984d0b..0b2a574ea 100644
--- a/mis_builder/models/mis_report_style.py
+++ b/mis_builder/models/mis_report_style.py
@@ -133,9 +133,10 @@ def check_positive_val(self):
hide_always_inherit = fields.Boolean(default=True)
hide_always = fields.Boolean(default=False)
- _sql_constraints = [
- ("style_name_uniq", "unique(name)", "Style name should be unique")
- ]
+ _style_name_uniq = models.Constraint(
+ "unique(name)",
+ "Style name should be unique",
+ )
description = fields.Html(
compute="_compute_description",
diff --git a/mis_builder/models/mis_report_subreport.py b/mis_builder/models/mis_report_subreport.py
index a1941c435..8c88f9aac 100644
--- a/mis_builder/models/mis_report_subreport.py
+++ b/mis_builder/models/mis_report_subreport.py
@@ -31,19 +31,16 @@ class MisReportSubReport(models.Model):
ondelete="restrict",
)
- _sql_constraints = [
- (
- "name_unique",
- "unique(name, report_id)",
- "Subreport name should be unique by report",
- ),
- (
- "subreport_unique",
- "unique(subreport_id, report_id)",
- "Should not include the same report more than once as sub report "
- "of a given report",
- ),
- ]
+ _name_unique = models.Constraint(
+ "unique(name, report_id)",
+ "Subreport name should be unique by report",
+ )
+
+ _subreport_unique = models.Constraint(
+ "unique(subreport_id, report_id)",
+ "Should not include the same report more than once as sub report "
+ "of a given report",
+ )
@api.constrains("name")
def _check_name(self):
diff --git a/mis_builder/models/mis_safe_eval.py b/mis_builder/models/mis_safe_eval.py
index 42fa31834..fad438de4 100644
--- a/mis_builder/models/mis_safe_eval.py
+++ b/mis_builder/models/mis_safe_eval.py
@@ -3,7 +3,12 @@
import traceback
-from odoo.tools.safe_eval import _BUILTINS, _SAFE_OPCODES, test_expr
+from odoo.tools.safe_eval import (
+ _BUILTINS,
+ _SAFE_OPCODES,
+ assert_valid_codeobj,
+ compile_codeobj,
+)
from .data_error import DataError, NameDataError
@@ -19,14 +24,14 @@ def mis_safe_eval(expr, locals_dict):
present in local_dict.
"""
try:
- c = test_expr(expr, _SAFE_OPCODES, mode="eval")
+ c = compile_codeobj(expr, mode="eval")
+ assert_valid_codeobj(_SAFE_OPCODES, c, expr)
globals_dict = {"__builtins__": _BUILTINS}
- # pylint: disable=eval-used,eval-referenced
+ # pylint: disable=W0123
val = eval(c, globals_dict, locals_dict)
except NameError:
val = NameDataError("#NAME", traceback.format_exc())
except ZeroDivisionError:
- # pylint: disable=redefined-variable-type
val = DataError("#DIV/0", traceback.format_exc())
except Exception:
val = DataError("#ERR", traceback.format_exc())
diff --git a/mis_builder/models/prorata_read_group_mixin.py b/mis_builder/models/prorata_read_group_mixin.py
index fa3f156f9..9abb395f1 100644
--- a/mis_builder/models/prorata_read_group_mixin.py
+++ b/mis_builder/models/prorata_read_group_mixin.py
@@ -6,7 +6,7 @@
from odoo import api, fields, models
from odoo.exceptions import UserError
-from odoo.fields import Date
+from odoo.fields import Date, Domain
from .mis_kpi_data import intersect_days
from .simple_array import SimpleArray
@@ -28,9 +28,9 @@ class ProRataReadGroupMixin(models.AbstractModel):
def _search_date(self, operator, value):
if operator in (">=", ">"):
- return [("date_to", operator, value)]
+ return Domain("date_to", operator, value)
elif operator in ("<=", "<"):
- return [("date_from", operator, value)]
+ return Domain("date_from", operator, value)
raise UserError(
self.env._("Unsupported operator %s for searching on date", operator)
)
@@ -73,7 +73,7 @@ def _read_group(
"""
date_from = None
date_to = None
- assert isinstance(domain, list)
+ assert isinstance(domain, list | Domain)
for domain_item in domain:
if isinstance(domain_item, list | tuple):
field, op, value = domain_item
diff --git a/mis_builder/security/res_groups.xml b/mis_builder/security/res_groups.xml
index bc6317785..4e1b5fae4 100644
--- a/mis_builder/security/res_groups.xml
+++ b/mis_builder/security/res_groups.xml
@@ -10,8 +10,8 @@
eval="[Command.link(ref('mis_builder.group_read_annotation'))]"
/>
diff --git a/mis_builder/static/description/index.html b/mis_builder/static/description/index.html
index c5eb0ceee..d400c1e66 100644
--- a/mis_builder/static/description/index.html
+++ b/mis_builder/static/description/index.html
@@ -374,7 +374,7 @@ MIS Builder
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:348d965e2a09fab00a015432f994ba12048c05a1387271683d815753eae84af5
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
-

+

This module allows you to build Management Information Systems
dashboards. Such style of reports presents KPI in rows and time periods
in columns. Reports mainly fetch data from account moves, but can also
@@ -1122,7 +1122,7 @@
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
-feedback.
+feedback.
Do not contact contributors directly about support or help with technical issues.
diff --git a/mis_builder/tests/test_aep.py b/mis_builder/tests/test_aep.py
index b5f5f8d9c..6f04fbd82 100644
--- a/mis_builder/tests/test_aep.py
+++ b/mis_builder/tests/test_aep.py
@@ -7,6 +7,7 @@
import odoo.tests.common as common
from odoo import Command, fields
from odoo.exceptions import UserError
+from odoo.fields import Domain
from odoo.tools.safe_eval import safe_eval
from ..models import aep
@@ -124,11 +125,11 @@ def setUp(self):
self.aep.parse_expr("bale[700%]")
self.aep.parse_expr("balp[700I%]")
self.aep.parse_expr("fldp.quantity[700%]")
- self.aep.parse_expr("balp[]" "[('account_id.code', '=', '400AR')]")
+ self.aep.parse_expr("balp[][('account_id.code', '=', '400AR')]")
self.aep.parse_expr(
- "balp[]" "[('account_id.account_type', '=', " " 'asset_receivable')]"
+ "balp[][('account_id.account_type', '=', 'asset_receivable')]"
)
- self.aep.parse_expr("balp[('account_type', '=', " " 'asset_receivable')]")
+ self.aep.parse_expr("balp[('account_type', '=', 'asset_receivable')]")
self.aep.parse_expr(
"balp['&', "
" ('account_type', '=', "
@@ -221,12 +222,12 @@ def test_aep_basic(self):
self.assertEqual(self._eval("balp[][('account_id.code', '=', '400AR')]"), 100)
self.assertEqual(
self._eval(
- "balp[]" "[('account_id.account_type', '=', " " 'asset_receivable')]"
+ "balp[][('account_id.account_type', '=', 'asset_receivable')]"
),
100,
)
self.assertEqual(
- self._eval("balp[('account_type', '=', " " 'asset_receivable')]"),
+ self._eval("balp[('account_type', '=', 'asset_receivable')]"),
100,
)
self.assertEqual(
@@ -402,36 +403,40 @@ def test_get_aml_domain_for_expr(self):
expr = "balp[700IN]"
domain = self.aep.get_aml_domain_for_expr(expr, "2017-01-01", "2017-03-31")
self.assertEqual(
- domain,
- [
- ("account_id", "in", (self.account_in.id,)),
- "&",
- ("date", ">=", "2017-01-01"),
- ("date", "<=", "2017-03-31"),
- ],
+ Domain(domain),
+ Domain(
+ [
+ ("account_id", "in", [self.account_in.id]),
+ "&",
+ ("date", ">=", "2017-01-01"),
+ ("date", "<=", "2017-03-31"),
+ ]
+ ),
)
expr = "debi[700IN] - crdi[400AR]"
domain = self.aep.get_aml_domain_for_expr(expr, "2017-02-01", "2017-03-31")
self.assertEqual(
- domain,
- [
- "|",
- # debi[700IN]
- "&",
- ("account_id", "in", (self.account_in.id,)),
- ("debit", "<>", 0.0),
- # crdi[400AR]
- "&",
- ("account_id", "in", (self.account_ar.id,)),
- ("credit", "<>", 0.0),
- "&",
- # for P&L accounts, only after fy start
- "|",
- ("date", ">=", "2017-01-01"),
- ("account_id.include_initial_balance", "=", True),
- # everything must be before from_date for initial balance
- ("date", "<", "2017-02-01"),
- ],
+ Domain(domain),
+ Domain(
+ [
+ "|",
+ # debi[700IN]
+ "&",
+ ("account_id", "in", [self.account_in.id]),
+ ("debit", "<>", 0.0),
+ # crdi[400AR]
+ "&",
+ ("account_id", "in", [self.account_ar.id]),
+ ("credit", "<>", 0.0),
+ "&",
+ # for P&L accounts, only after fy start
+ "|",
+ ("date", ">=", "2017-01-01"),
+ ("account_id.include_initial_balance", "=", True),
+ # everything must be before from_date for initial balance
+ ("date", "<", "2017-02-01"),
+ ]
+ ),
)
def test_is_domain(self):
diff --git a/mis_builder/tests/test_kpi_data.py b/mis_builder/tests/test_kpi_data.py
index 5988585c5..2ba67c805 100644
--- a/mis_builder/tests/test_kpi_data.py
+++ b/mis_builder/tests/test_kpi_data.py
@@ -1,8 +1,7 @@
# Copyright 2017 ACSONE SA/NV ()
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
-from odoo_test_helper import FakeModelLoader
-
+from odoo.orm.model_classes import add_to_registry
from odoo.tests.common import TransactionCase
from ..models.mis_kpi_data import ACC_AVG, ACC_SUM
@@ -13,12 +12,13 @@ class TestKpiData(TransactionCase):
def setUpClass(cls):
super().setUpClass()
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
from .fake_models import MisKpiDataTestItem
- cls.loader.update_registry((MisKpiDataTestItem,))
- cls.addClassCleanup(cls.loader.restore_registry)
+ add_to_registry(cls.registry, MisKpiDataTestItem)
+ model_name = "mis.kpi.data.test.item"
+ cls.registry._setup_models__(cls.env.cr, [model_name])
+ cls.registry.init_models(cls.env.cr, [model_name], {"models_to_check": True})
+ cls.addClassCleanup(cls.registry.__delitem__, model_name)
report = cls.env["mis.report"].create(dict(name="test report"))
cls.kpi1 = cls.env["mis.report.kpi"].create(
diff --git a/mis_builder/tests/test_mis_report_instance.py b/mis_builder/tests/test_mis_report_instance.py
index ba8d83aed..afb47872c 100644
--- a/mis_builder/tests/test_mis_report_instance.py
+++ b/mis_builder/tests/test_mis_report_instance.py
@@ -2,6 +2,7 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import odoo.tests.common as common
+from odoo.fields import Domain
from odoo.tools import test_reports
from ..models.accounting_none import AccountingNone
@@ -417,7 +418,9 @@ def test_drilldown(self):
)
.ids
)
- self.assertTrue(("account_id", "in", tuple(account_ids)) in action["domain"])
+ domain_list = list(Domain(action["domain"]))
+ self.assertIn(("account_id", "in", account_ids), domain_list)
+
self.assertEqual(action["res_model"], "account.move.line")
def test_drilldown_action_name_with_account(self):
diff --git a/mis_builder/tests/test_pro_rata_read_group.py b/mis_builder/tests/test_pro_rata_read_group.py
index 0b38464d7..0cde3155a 100644
--- a/mis_builder/tests/test_pro_rata_read_group.py
+++ b/mis_builder/tests/test_pro_rata_read_group.py
@@ -1,8 +1,7 @@
# Copyright 2025 ACSONE SA/NV ()
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
-from odoo_test_helper import FakeModelLoader
-
+from odoo.orm.model_classes import add_to_registry
from odoo.tests import TransactionCase
@@ -10,12 +9,13 @@ class TestProrataReadGroup(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
- cls.loader = FakeModelLoader(cls.env, cls.__module__)
- cls.loader.backup_registry()
from .fake_models import ProrataReadGroupThing
- cls.loader.update_registry((ProrataReadGroupThing,))
- cls.addClassCleanup(cls.loader.restore_registry)
+ add_to_registry(cls.registry, ProrataReadGroupThing)
+ model_name = "prorata.read.group.thing"
+ cls.registry._setup_models__(cls.env.cr, [model_name])
+ cls.registry.init_models(cls.env.cr, [model_name], {"models_to_check": True})
+ cls.addClassCleanup(cls.registry.__delitem__, model_name)
cls.thing_model = cls.env["prorata.read.group.thing"]
cls.thing_model.create(
@@ -46,31 +46,29 @@ def setUpClass(cls):
}
)
- def test_prorata_read_group(self):
- """Test a pro-rata read_group with a date period."""
- data = self.thing_model.read_group(
+ def test_prorata_formatted_read_group(self):
+ """Test a pro-rata formatted_read_group with a date period."""
+ data = self.thing_model.formatted_read_group(
[("date", ">=", "2024-01-11"), ("date", "<=", "2024-01-20")],
- fields=["debit", "credit", "account_code", "company_id"],
+ aggregates=["debit:sum", "credit:sum"],
groupby=["account_code", "company_id"],
- lazy=False,
)[0]
- self.assertEqual(data["debit"], 111)
- self.assertEqual(data["credit"], 0)
+ self.assertEqual(data["debit:sum"], 111)
+ self.assertEqual(data["credit:sum"], 0)
self.assertEqual(data["account_code"], "A1")
self.assertEqual(
data["company_id"], (self.env.company.id, self.env.company.name)
)
- def test_read_group(self):
- """Test a regular read_group without date filtering still works."""
- data = self.thing_model.read_group(
+ def test_formatted_read_group(self):
+ """Test a regular formatted_read_group without date filtering still works."""
+ data = self.thing_model.formatted_read_group(
domain=[],
- fields=["debit", "credit", "account_code", "company_id"],
groupby=["account_code", "company_id"],
- lazy=False,
+ aggregates=["debit:sum", "credit:sum"],
)[0]
- self.assertEqual(data["debit"], 218)
- self.assertEqual(data["credit"], 0)
+ self.assertEqual(data["debit:sum"], 218)
+ self.assertEqual(data["credit:sum"], 0)
self.assertEqual(data["account_code"], "A1")
self.assertEqual(
data["company_id"], (self.env.company.id, self.env.company.name)
diff --git a/mis_builder/tests/test_render.py b/mis_builder/tests/test_render.py
index 536d205e8..fc8756f78 100644
--- a/mis_builder/tests/test_render.py
+++ b/mis_builder/tests/test_render.py
@@ -322,23 +322,25 @@ def test_xslx(self):
)
def test_description(self):
- self.assertEqual(self.style.description.unescape(), "")
+ self.assertEqual(self.style.description.unescape(), "")
self.style.dp_inherit = False
self.style.dp = 4
- self.assertEqual(self.style.description.unescape(), "Rounding : 4")
+ self.assertEqual(self.style.description.unescape(), "Rounding : 4
")
self.style.dp_inherit = True
self.style.color_inherit = False
self.style.color = "red"
self.assertEqual(
self.style.description.unescape(),
- 'Text color : ',
+ ' border: 1px black solid; border-radius: 5px;">',
)
self.style.color_inherit = True
self.style.prefix_inherit = False
self.style.prefix = "$"
- self.assertEqual(self.style.description.unescape(), "Prefix : '$'")
+ self.assertEqual(
+ self.style.description.unescape(), "Prefix : '$'
"
+ )
From 5a10462d00e163a04a5784c8e793893e151b4ccf Mon Sep 17 00:00:00 2001
From: Asta
Date: Fri, 21 Nov 2025 15:32:00 +0200
Subject: [PATCH 02/10] [IMP] mis_builder: pre-commit fixes.
---
mis_builder/models/mis_report.py | 2 +-
mis_builder/models/mis_report_style.py | 14 +++++---------
mis_builder/tests/common.py | 6 +++---
mis_builder/tests/test_period_dates.py | 6 +++---
mis_builder/wizard/mis_builder_dashboard.py | 2 +-
5 files changed, 13 insertions(+), 17 deletions(-)
diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py
index 5405cc325..763592670 100644
--- a/mis_builder/models/mis_report.py
+++ b/mis_builder/models/mis_report.py
@@ -447,7 +447,7 @@ def _default_move_lines_source(self):
("field_id.name", "=", "date"),
("field_id.name", "=", "company_id"),
],
- default=_default_move_lines_source,
+ default=lambda self: self._default_move_lines_source(),
required=True,
ondelete="cascade",
help="A 'move line like' model, ie having at least debit, credit, "
diff --git a/mis_builder/models/mis_report_style.py b/mis_builder/models/mis_report_style.py
index 0b2a574ea..a0cb21c7c 100644
--- a/mis_builder/models/mis_report_style.py
+++ b/mis_builder/models/mis_report_style.py
@@ -3,7 +3,6 @@
# Copyright 2020 CorporateHub (https://corporatehub.eu)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
-import sys
from odoo import api, fields, models
from odoo.exceptions import ValidationError
@@ -12,15 +11,13 @@
from .accounting_none import AccountingNone
from .data_error import DataError
-if sys.version_info.major >= 3:
- unicode = str
-
class PropertyDict(dict):
def __getattr__(self, name):
return self.get(name)
- def copy(self): # pylint: disable=copy-wo-api-one,method-required-super
+ # pylint: disable=method-required-super
+ def copy(self):
return PropertyDict(self)
@@ -224,7 +221,8 @@ def render_num(
if value is None or value is AccountingNone:
return ""
value = float_round(value / float(divider or 1), dp or 0) or 0
- r = lang.format("%%%s.%df" % (sign, dp or 0), value, grouping=True)
+ format_str = f"%{sign}.{dp or 0}f"
+ r = lang.format(format_str, value, grouping=True)
r = r.replace("-", "\N{NON-BREAKING HYPHEN}")
if prefix:
r = prefix + "\N{NO-BREAK SPACE}" + r
@@ -240,7 +238,7 @@ def render_pct(self, lang, value, dp=1, sign="-"):
def render_str(self, lang, value):
if value is None or value is AccountingNone:
return ""
- return unicode(value)
+ return str(value)
@api.model
def compare_and_render(
@@ -287,10 +285,8 @@ def compare_and_render(
delta = AccountingNone
elif var_type == TYPE_NUM:
if value and average_value:
- # pylint: disable=redefined-variable-type
value = value / float(average_value)
if base_value and average_base_value:
- # pylint: disable=redefined-variable-type
base_value = base_value / float(average_base_value)
if compare_method == CMP_DIFF:
delta = value - base_value
diff --git a/mis_builder/tests/common.py b/mis_builder/tests/common.py
index 5725cfd6c..466abbe92 100644
--- a/mis_builder/tests/common.py
+++ b/mis_builder/tests/common.py
@@ -26,9 +26,9 @@ def assert_matrix(matrix, expected):
if row is not None and expected_row is None:
raise AssertionError("too many rows")
for j, cell, expected_val in _zip(row.iter_cells(), expected_row):
- assert (
- (cell and cell.val) == expected_val
- ), f"{cell and cell.val} != {expected_val} in row {i} col {j}"
+ assert (cell and cell.val) == expected_val, (
+ f"{cell and cell.val} != {expected_val} in row {i} col {j}"
+ )
@tagged("doctest")
diff --git a/mis_builder/tests/test_period_dates.py b/mis_builder/tests/test_period_dates.py
index 1bd8312f1..a170dbe7c 100644
--- a/mis_builder/tests/test_period_dates.py
+++ b/mis_builder/tests/test_period_dates.py
@@ -124,9 +124,9 @@ def test_rel_date_range(self):
self.env["date.range"].create(
dict(
type_id=date_range_type.id,
- name="%d" % year,
- date_start="%d-01-01" % year,
- date_end="%d-12-31" % year,
+ name=f"{year}",
+ date_start=f"{year}-01-01",
+ date_end=f"{year}-12-31",
company_id=date_range_type.company_id.id,
)
)
diff --git a/mis_builder/wizard/mis_builder_dashboard.py b/mis_builder/wizard/mis_builder_dashboard.py
index 2eaffcff7..1b8413b8b 100644
--- a/mis_builder/wizard/mis_builder_dashboard.py
+++ b/mis_builder/wizard/mis_builder_dashboard.py
@@ -17,7 +17,7 @@ class AddMisReportInstanceDashboard(models.TransientModel):
"ir.actions.act_window",
string="Dashboard",
required=True,
- domain="[('res_model', '=', " "'board.board')]",
+ domain="[('res_model', '=', 'board.board')]",
)
@api.model
From 7d45545fb95bba933f6cfe88dd254d61ed519f65 Mon Sep 17 00:00:00 2001
From: oca-ci
Date: Wed, 27 May 2026 11:18:37 +0000
Subject: [PATCH 03/10] [UPD] Update mis_builder.pot
---
mis_builder/i18n/mis_builder.pot | 30 +++++++++++++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
diff --git a/mis_builder/i18n/mis_builder.pot b/mis_builder/i18n/mis_builder.pot
index 23a282281..52b2d9de9 100644
--- a/mis_builder/i18n/mis_builder.pot
+++ b/mis_builder/i18n/mis_builder.pot
@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
-"Project-Id-Version: Odoo Server 18.0\n"
+"Project-Id-Version: Odoo Server 19.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -565,6 +565,8 @@ msgstr ""
#. module: mis_builder
#: model:ir.model.fields,field_description:mis_builder.field_add_mis_report_instance_dashboard_wizard__display_name
+#: model:ir.model.fields,field_description:mis_builder.field_ir_actions_report__display_name
+#: model:ir.model.fields,field_description:mis_builder.field_mis_kpi_data__display_name
#: model:ir.model.fields,field_description:mis_builder.field_mis_report__display_name
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_instance__display_name
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_instance_annotation__display_name
@@ -576,6 +578,8 @@ msgstr ""
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_style__display_name
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_subkpi__display_name
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_subreport__display_name
+#: model:ir.model.fields,field_description:mis_builder.field_prorata_read_group_mixin__display_name
+#: model:ir.model.fields,field_description:mis_builder.field_report_mis_builder_mis_report_instance_xlsx__display_name
msgid "Display Name"
msgstr ""
@@ -808,6 +812,8 @@ msgstr ""
#. module: mis_builder
#: model:ir.model.fields,field_description:mis_builder.field_add_mis_report_instance_dashboard_wizard__id
+#: model:ir.model.fields,field_description:mis_builder.field_ir_actions_report__id
+#: model:ir.model.fields,field_description:mis_builder.field_mis_kpi_data__id
#: model:ir.model.fields,field_description:mis_builder.field_mis_report__id
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_instance__id
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_instance_annotation__id
@@ -819,6 +825,8 @@ msgstr ""
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_style__id
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_subkpi__id
#: model:ir.model.fields,field_description:mis_builder.field_mis_report_subreport__id
+#: model:ir.model.fields,field_description:mis_builder.field_prorata_read_group_mixin__id
+#: model:ir.model.fields,field_description:mis_builder.field_report_mis_builder_mis_report_instance_xlsx__id
msgid "ID"
msgstr ""
@@ -967,6 +975,11 @@ msgstr ""
msgid "Layout"
msgstr ""
+#. module: mis_builder
+#: model:ir.model.fields.selection,name:mis_builder.selection__mis_report_style__divider__1e6
+msgid "M"
+msgstr ""
+
#. module: mis_builder
#: model:ir.model,name:mis_builder.model_report_mis_builder_mis_report_instance_xlsx
msgid "MIS Builder XLSX report"
@@ -1753,11 +1766,21 @@ msgstr ""
msgid "from %(date_from)s to %(date_to)s"
msgstr ""
+#. module: mis_builder
+#: model:ir.model.fields.selection,name:mis_builder.selection__mis_report_style__divider__1e3
+msgid "k"
+msgstr ""
+
#. module: mis_builder
#: model:ir.model.fields.selection,name:mis_builder.selection__mis_report_style__font_size__large
msgid "large"
msgstr ""
+#. module: mis_builder
+#: model:ir.model.fields.selection,name:mis_builder.selection__mis_report_style__divider__1e-3
+msgid "m"
+msgstr ""
+
#. module: mis_builder
#: model:ir.model.fields.selection,name:mis_builder.selection__mis_report_style__font_size__medium
msgid "medium"
@@ -1811,3 +1834,8 @@ msgstr ""
#: model:ir.model.fields.selection,name:mis_builder.selection__mis_report_style__font_size__xx-small
msgid "xx-small"
msgstr ""
+
+#. module: mis_builder
+#: model:ir.model.fields.selection,name:mis_builder.selection__mis_report_style__divider__1e-6
+msgid "ยต"
+msgstr ""
From 07a096c608d90e8ed51f0979c9db5a49a24cfc60 Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Wed, 27 May 2026 11:21:02 +0000
Subject: [PATCH 04/10] [BOT] post-merge updates
---
README.md | 8 +++++++-
mis_builder/README.rst | 2 +-
mis_builder/static/description/index.html | 2 +-
setup/_metapackage/pyproject.toml | 8 +++-----
4 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 4f0e570c2..5e7bd2d4b 100644
--- a/README.md
+++ b/README.md
@@ -47,11 +47,17 @@ Here are some presentations:
[//]: # (addons)
+Available addons
+----------------
+addon | version | maintainers | summary
+--- | --- | --- | ---
+[mis_builder](mis_builder/) | 19.0.1.0.0 |
| Build 'Management Information System' Reports and Dashboards
+
+
Unported addons
---------------
addon | version | maintainers | summary
--- | --- | --- | ---
-[mis_builder](mis_builder/) | 18.0.1.8.0 (unported) |
| Build 'Management Information System' Reports and Dashboards
[mis_builder_budget](mis_builder_budget/) | 18.0.2.0.0 (unported) |
| Create budgets for MIS reports
[mis_builder_demo](mis_builder_demo/) | 18.0.1.0.0 (unported) |
| Demo addon for MIS Builder
diff --git a/mis_builder/README.rst b/mis_builder/README.rst
index 1b9e91296..1de2cbb33 100644
--- a/mis_builder/README.rst
+++ b/mis_builder/README.rst
@@ -11,7 +11,7 @@ MIS Builder
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:348d965e2a09fab00a015432f994ba12048c05a1387271683d815753eae84af5
+ !! source digest: sha256:5c7e5233335bac8f1c0229d67fe87c2df572abdcca496f76fe444dc71a5221df
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
diff --git a/mis_builder/static/description/index.html b/mis_builder/static/description/index.html
index d400c1e66..078f0831d 100644
--- a/mis_builder/static/description/index.html
+++ b/mis_builder/static/description/index.html
@@ -372,7 +372,7 @@ MIS Builder
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:348d965e2a09fab00a015432f994ba12048c05a1387271683d815753eae84af5
+!! source digest: sha256:5c7e5233335bac8f1c0229d67fe87c2df572abdcca496f76fe444dc71a5221df
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

This module allows you to build Management Information Systems
diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml
index 13e5e8b67..33c415233 100644
--- a/setup/_metapackage/pyproject.toml
+++ b/setup/_metapackage/pyproject.toml
@@ -1,13 +1,11 @@
[project]
name = "odoo-addons-oca-mis-builder"
-version = "18.0.20250603.1"
+version = "19.0.20260527.0"
dependencies = [
- "odoo-addon-mis_builder==18.0.*",
- "odoo-addon-mis_builder_budget==18.0.*",
- "odoo-addon-mis_builder_demo==18.0.*",
+ "odoo-addon-mis_builder==19.0.*",
]
classifiers=[
"Programming Language :: Python",
"Framework :: Odoo",
- "Framework :: Odoo :: 18.0",
+ "Framework :: Odoo :: 19.0",
]
From d272162930ce120731a931b6629807089e2f6bfc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Wed, 27 May 2026 13:24:31 +0200
Subject: [PATCH 05/10] [IMP] mis_builder: minor code cosmetics
---
mis_builder/tests/test_aep.py | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/mis_builder/tests/test_aep.py b/mis_builder/tests/test_aep.py
index 6f04fbd82..5733fa6c5 100644
--- a/mis_builder/tests/test_aep.py
+++ b/mis_builder/tests/test_aep.py
@@ -127,9 +127,9 @@ def setUp(self):
self.aep.parse_expr("fldp.quantity[700%]")
self.aep.parse_expr("balp[][('account_id.code', '=', '400AR')]")
self.aep.parse_expr(
- "balp[][('account_id.account_type', '=', 'asset_receivable')]"
+ "balp[][('account_id.account_type', '=', 'asset_receivable')]"
)
- self.aep.parse_expr("balp[('account_type', '=', 'asset_receivable')]")
+ self.aep.parse_expr("balp[('account_type', '=', 'asset_receivable')]")
self.aep.parse_expr(
"balp['&', "
" ('account_type', '=', "
@@ -221,13 +221,11 @@ def test_aep_basic(self):
self.assertEqual(self._eval("balp[400AR]"), 100)
self.assertEqual(self._eval("balp[][('account_id.code', '=', '400AR')]"), 100)
self.assertEqual(
- self._eval(
- "balp[][('account_id.account_type', '=', 'asset_receivable')]"
- ),
+ self._eval("balp[][('account_id.account_type', '=', 'asset_receivable')]"),
100,
)
self.assertEqual(
- self._eval("balp[('account_type', '=', 'asset_receivable')]"),
+ self._eval("balp[('account_type', '=', 'asset_receivable')]"),
100,
)
self.assertEqual(
From e1cad4d3b85014925ef99763825bf6ce21ce3d7e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Wed, 13 May 2026 11:55:23 -0400
Subject: [PATCH 06/10] [FIX] mis_builder: fix multi-company account display
Fixes multi-company MIS report issues in Odoo 18:
1. `account.account.company_id` was replaced by `company_ids` (Many2many)
in Odoo 18. `_get_account_name` crashed on multi-company reports.
2. `account.code` is now company-dependent (stored in `code_store` jsonb).
When viewing a report from a different company context, codes
displayed as "False". Fix uses `account.display_name`.
3. Threads `query_companies` through `KpiMatrix` and `prepare_kpi_matrix()`
for proper multi-company context resolution.
Co-Authored-By: Don Kendall
---
mis_builder/models/kpimatrix.py | 41 ++++++++++--
mis_builder/models/mis_report.py | 4 +-
mis_builder/models/mis_report_instance.py | 3 +-
mis_builder/tests/test_mis_report_instance.py | 62 +++++++++++++++++++
4 files changed, 100 insertions(+), 10 deletions(-)
diff --git a/mis_builder/models/kpimatrix.py b/mis_builder/models/kpimatrix.py
index dc23b12a7..63c706d7e 100644
--- a/mis_builder/models/kpimatrix.py
+++ b/mis_builder/models/kpimatrix.py
@@ -139,12 +139,18 @@ def __init__(
class KpiMatrix:
- def __init__(self, env, multi_company=False, account_model="account.account"):
+ def __init__(
+ self,
+ env,
+ companies=None,
+ account_model="account.account",
+ ):
# cache language id for faster rendering
lang_model = env["res.lang"]
self.lang = lang_model._lang_get(env.user.lang)
self._style_model = env["mis.report.style"]
self._account_model = env[account_model]
+ self._companies = companies
# data structures
# { kpi: KpiMatrixRow }
self._kpi_rows = OrderedDict()
@@ -158,7 +164,6 @@ def __init__(self, env, multi_company=False, account_model="account.account"):
self._sum_todo = {}
# { account_id: account_name }
self._account_names = {}
- self._multi_company = multi_company
def declare_kpi(self, kpi):
"""Declare a new kpi (row) in the matrix.
@@ -467,10 +472,34 @@ def _load_account_names(self):
self._account_names = {a.id: self._get_account_name(a) for a in accounts}
def _get_account_name(self, account):
- result = f"{account.code} {account.name}"
- if self._multi_company:
- result = f"{result} [{account.company_id.name}]"
- return result
+ # display_name is account code + account name. Note the account may have
+ # no code for the user current active company, in which case only the
+ # name is displayed. It is consistent with other places where accounts
+ # are displayed in Odoo.
+ account_companies = (
+ account.company_ids & self._companies
+ if self._companies
+ else account.company_ids
+ )
+ if len(account_companies) == 1:
+ # When there is no ambiguity on the company, use it to compute the label
+ account_name = account.with_company(account_companies).display_name
+ else:
+ # Otherwise use the default Odoo behaviour to get the account label
+ # (this may return a name without code)
+ account_name = account.display_name
+ is_multi_company = self._companies and len(self._companies) > 1
+ if is_multi_company and len(account_companies) == 1:
+ # In a multi-company report, if the account is bound to one
+ # company, it makes sense to show the company name. If the account
+ # is bound to multiple companies it does not make sense, because we
+ # don't know to which companies this detail line effectively
+ # contributes, so the list of companies in it would not add useful
+ # information. To be able to accurately display the company on
+ # detail lines when the account is bound to multiple companies,
+ # we'll need a generalized kpi details expansion.
+ account_name = f"{account_name} [{account_companies.display_name}]"
+ return account_name
def get_account_name(self, account_id):
if account_id not in self._account_names:
diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py
index 763592670..6bfa454d3 100644
--- a/mis_builder/models/mis_report.py
+++ b/mis_builder/models/mis_report.py
@@ -533,9 +533,9 @@ def copy(self, default=None):
# TODO: kpi name cannot be start with query name
- def prepare_kpi_matrix(self, multi_company=False):
+ def prepare_kpi_matrix(self, companies=None):
self.ensure_one()
- kpi_matrix = KpiMatrix(self.env, multi_company, self.account_model)
+ kpi_matrix = KpiMatrix(self.env, companies, self.account_model)
for kpi in self.kpi_ids:
kpi_matrix.declare_kpi(kpi)
return kpi_matrix
diff --git a/mis_builder/models/mis_report_instance.py b/mis_builder/models/mis_report_instance.py
index b56a97be9..16b91350d 100644
--- a/mis_builder/models/mis_report_instance.py
+++ b/mis_builder/models/mis_report_instance.py
@@ -879,8 +879,7 @@ def _compute_matrix(self):
"""
self.ensure_one()
aep = self.report_id._prepare_aep(self.query_company_ids, self.currency_id)
- multi_company = self.multi_company and len(self.query_company_ids) > 1
- kpi_matrix = self.report_id.prepare_kpi_matrix(multi_company)
+ kpi_matrix = self.report_id.prepare_kpi_matrix(self.query_company_ids)
for period in self.period_ids:
description = None
if period.mode == MODE_NONE:
diff --git a/mis_builder/tests/test_mis_report_instance.py b/mis_builder/tests/test_mis_report_instance.py
index afb47872c..f8fd78bc2 100644
--- a/mis_builder/tests/test_mis_report_instance.py
+++ b/mis_builder/tests/test_mis_report_instance.py
@@ -493,6 +493,68 @@ def test_drilldown_views(self):
[[False, "list"], [False, "form"], [False, "pivot"], [False, "graph"]],
)
+ def test_multicompany_account_code_display(self):
+ """Account codes should display correctly in multi-company reports.
+
+ In Odoo 18, account.code is company-dependent. When a report belongs
+ to a different company than the user's current company, account codes
+ must still display correctly in auto-expanded rows.
+ """
+ company2 = self.env["res.company"].create({"name": "Test Co 2"})
+ account = (
+ self.env["account.account"]
+ .with_company(company2)
+ .create(
+ {
+ "name": "Test Account",
+ "code": "999001",
+ "account_type": "expense",
+ "company_ids": [(6, 0, [company2.id])],
+ }
+ )
+ )
+ # Verify code is visible from company2 but not from main company
+ self.assertEqual(account.with_company(company2).code, "999001")
+ self.assertFalse(account.with_company(self.env.ref("base.main_company")).code)
+ # Create report + instance for company2
+ report = self.env["mis.report"].create({"name": "MC Test Report"})
+ self.env["mis.report.kpi"].create(
+ {
+ "report_id": report.id,
+ "name": "exp",
+ "description": "Test Expense",
+ "auto_expand_accounts": True,
+ "sequence": 1,
+ "expression_ids": [(0, 0, {"name": "balp[999%]"})],
+ }
+ )
+ instance = self.env["mis.report.instance"].create(
+ {
+ "name": "MC Test Instance",
+ "report_id": report.id,
+ "company_id": company2.id,
+ "period_ids": [
+ (
+ 0,
+ 0,
+ {
+ "name": "2024",
+ "mode": "fix",
+ "manual_date_from": "2024-01-01",
+ "manual_date_to": "2024-12-31",
+ },
+ ),
+ ],
+ }
+ )
+ matrix = instance.compute()
+ body = matrix.get("body", [])
+ has_false = any("False" in (r.get("label") or "") for r in body)
+ self.assertFalse(
+ has_false,
+ "Account codes should not show as 'False' in multi-company reports",
+ )
+
def test_qweb(self):
self.report_instance.print_pdf() # get action
test_reports.try_report(
From 5ec876efa23b2270476980a7cbd2bac445e8be10 Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Thu, 28 May 2026 08:50:29 +0000
Subject: [PATCH 07/10] [BOT] post-merge updates
---
README.md | 2 +-
mis_builder/README.rst | 2 +-
mis_builder/__manifest__.py | 2 +-
mis_builder/static/description/index.html | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 5e7bd2d4b..4f45b00d3 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ Available addons
----------------
addon | version | maintainers | summary
--- | --- | --- | ---
-[mis_builder](mis_builder/) | 19.0.1.0.0 |
| Build 'Management Information System' Reports and Dashboards
+[mis_builder](mis_builder/) | 19.0.1.0.1 |
| Build 'Management Information System' Reports and Dashboards
Unported addons
diff --git a/mis_builder/README.rst b/mis_builder/README.rst
index 1de2cbb33..1ddb3a090 100644
--- a/mis_builder/README.rst
+++ b/mis_builder/README.rst
@@ -11,7 +11,7 @@ MIS Builder
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:5c7e5233335bac8f1c0229d67fe87c2df572abdcca496f76fe444dc71a5221df
+ !! source digest: sha256:0c70f530a354972bed3c22179096002dbe4293a292c63db276d8b9c13ec4e6db
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
diff --git a/mis_builder/__manifest__.py b/mis_builder/__manifest__.py
index 0c86f180e..f59046298 100644
--- a/mis_builder/__manifest__.py
+++ b/mis_builder/__manifest__.py
@@ -3,7 +3,7 @@
{
"name": "MIS Builder",
- "version": "19.0.1.0.0",
+ "version": "19.0.1.0.1",
"category": "Reporting",
"summary": """
Build 'Management Information System' Reports and Dashboards
diff --git a/mis_builder/static/description/index.html b/mis_builder/static/description/index.html
index 078f0831d..bfbf7d2a0 100644
--- a/mis_builder/static/description/index.html
+++ b/mis_builder/static/description/index.html
@@ -372,7 +372,7 @@ MIS Builder
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:5c7e5233335bac8f1c0229d67fe87c2df572abdcca496f76fe444dc71a5221df
+!! source digest: sha256:0c70f530a354972bed3c22179096002dbe4293a292c63db276d8b9c13ec4e6db
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

This module allows you to build Management Information Systems
From 4a57158802311c031ff161c0a8c9308aed35f6ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Bidoul?=
Date: Thu, 28 May 2026 12:34:47 +0200
Subject: [PATCH 08/10] Update dotfiles
---
.copier-answers.yml | 3 +--
.github/workflows/pre-commit.yml | 2 ++
.github/workflows/test.yml | 7 +++++++
.pre-commit-config.yaml | 7 ++++++-
.pylintrc | 9 ---------
CONTRIBUTING.md | 10 ----------
README.md | 3 +++
checklog-odoo.cfg | 2 ++
8 files changed, 21 insertions(+), 22 deletions(-)
delete mode 100644 CONTRIBUTING.md
diff --git a/.copier-answers.yml b/.copier-answers.yml
index 67d921a88..0cbae9717 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,8 +1,7 @@
# Do NOT update manually; changes here will be overwritten by Copier
-_commit: v1.35
+_commit: v1.40
_src_path: https://github.com/OCA/oca-addons-repo-template
additional_ruff_rules: []
-ci: GitHub
convert_readme_fragments_to_markdown: true
enable_checklog_odoo: true
generate_requirements_txt: true
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index d61380e4f..59515b28b 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -17,6 +17,8 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.11"
+ cache: 'pip'
+ cache-dependency-path: '.pre-commit-config.yaml'
- name: Get python version
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
- uses: actions/cache@v4
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6101a2b02..2bdca7401 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -65,6 +65,13 @@ jobs:
run: oca_init_test_database
- name: Run tests
run: oca_run_tests
+ - name: Upload screenshots from JS tests
+ uses: actions/upload-artifact@v4
+ if: ${{ failure() }}
+ with:
+ name: Screenshots of failed JS tests - ${{ matrix.name }}${{ join(matrix.include) }}
+ path: /tmp/odoo_tests/${{ env.PGDATABASE }}
+ if-no-files-found: ignore
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b78c71d93..d1486eda3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -40,6 +40,11 @@ repos:
entry: found a en.po file
language: fail
files: '[a-zA-Z0-9_]*/i18n/en\.po$'
+ - id: obsolete dotfiles
+ name: obsolete dotfiles
+ entry: found obsolete files; remove them
+ files: '^(\.travis\.yml|\.t2d\.yml|CONTRIBUTING\.md|\.prettierrc\.yml|\.eslintrc\.yml)$'
+ language: fail
- repo: https://github.com/sbidoul/whool
rev: v1.3
hooks:
@@ -62,7 +67,7 @@ repos:
- --convert-fragments-to-markdown
- id: oca-gen-external-dependencies
- repo: https://github.com/OCA/odoo-pre-commit-hooks
- rev: v0.1.6
+ rev: v0.1.7
hooks:
- id: oca-checks-odoo-module
- id: oca-checks-po
diff --git a/.pylintrc b/.pylintrc
index f3d017a8f..a7aec2a05 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -92,20 +92,11 @@ enable=anomalous-backslash-in-string,
no-write-in-compute,
# messages that do not cause the lint step to fail
consider-merging-classes-inherited,
- create-user-wo-reset-password,
- dangerous-filter-wo-user,
deprecated-module,
- file-not-used,
invalid-commit,
- missing-manifest-dependency,
- missing-newline-extrafiles,
missing-readme,
- no-utf8-coding-comment,
odoo-addons-relative-import,
- old-api7-method-defined,
redefined-builtin,
- too-complex,
- unnecessary-utf8-coding-comment,
manifest-external-assets
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 9ac71fee4..000000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# OCA Guidelines
-
-Please follow the official guide from the
-[OCA Guidelines page](https://odoo-community.org/page/contributing).
-
-## Project Specific Guidelines
-
-
-
-This project does not have specific coding guidelines.
diff --git a/README.md b/README.md
index 4f45b00d3..822fb0be1 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,9 @@
# MIS Builder
+[](https://odoo-community.org/get-involved?utm_source=repo-readme)
+
+# MIS Builder
[](https://runboat.odoo-community.org/builds?repo=OCA/mis-builder&target_branch=19.0)
[](https://github.com/OCA/mis-builder/actions/workflows/pre-commit.yml?query=branch%3A19.0)
[](https://github.com/OCA/mis-builder/actions/workflows/test.yml?query=branch%3A19.0)
diff --git a/checklog-odoo.cfg b/checklog-odoo.cfg
index 0b55b7bf6..58d43aa66 100644
--- a/checklog-odoo.cfg
+++ b/checklog-odoo.cfg
@@ -1,3 +1,5 @@
[checklog-odoo]
ignore=
WARNING.* 0 failed, 0 error\(s\).*
+ WARNING .* Killing chrome descendants-or-self .*
+ WARNING.* Missing widget: res_partner_many2one for field of type many2one.*
From d2e7eef94398fc639d98af4746a4287be543a3ca Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 28 May 2026 10:36:55 +0000
Subject: [PATCH 09/10] Update dependency
https://github.com/OCA/oca-addons-repo-template to v1.42
---
.copier-answers.yml | 2 +-
.github/workflows/pre-commit.yml | 3 ++-
.pre-commit-config.yaml | 2 +-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/.copier-answers.yml b/.copier-answers.yml
index 0cbae9717..c5a420752 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,5 +1,5 @@
# Do NOT update manually; changes here will be overwritten by Copier
-_commit: v1.40
+_commit: v1.42
_src_path: https://github.com/OCA/oca-addons-repo-template
additional_ruff_rules: []
convert_readme_fragments_to_markdown: true
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 59515b28b..e6a9e52be 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -1,3 +1,4 @@
+
name: pre-commit
on:
@@ -16,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
cache: 'pip'
cache-dependency-path: '.pre-commit-config.yaml'
- name: Get python version
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d1486eda3..b594477e9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -23,7 +23,7 @@ exclude: |
# You don't usually want a bot to modify your legal texts
(LICENSE.*|COPYING.*)
default_language_version:
- python: python3
+ python: python3.12
node: "22.9.0"
repos:
- repo: local
From 66cd9dc20fc557495e7280cab9855068014b264b Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 10:36:47 +0000
Subject: [PATCH 10/10] Update dependency
https://github.com/OCA/oca-addons-repo-template to v1.43
---
.copier-answers.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.copier-answers.yml b/.copier-answers.yml
index c5a420752..80c42d779 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,5 +1,5 @@
# Do NOT update manually; changes here will be overwritten by Copier
-_commit: v1.42
+_commit: v1.43
_src_path: https://github.com/OCA/oca-addons-repo-template
additional_ruff_rules: []
convert_readme_fragments_to_markdown: true
@@ -16,6 +16,7 @@ odoo_test_flavor: Both
odoo_version: 19.0
org_name: Odoo Community Association (OCA)
org_slug: OCA
+postgres_image: ''
rebel_module_groups: []
repo_description: "Management Information System reports for Odoo: easily build super\
\ fast,\nbeautiful, custom reports such as P&L, Balance Sheets and more.\n\nThis\