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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Production/Stable License: AGPL-3 OCA/mis-builder Translate me on Weblate Try me on Runboat

+

Production/Stable License: AGPL-3 OCA/mis-builder Translate me on Weblate Try me on Runboat

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 @@

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.

@@ -1179,7 +1179,7 @@

Maintainers

promote its widespread use.

Current 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/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 | sbidoul | Build 'Management Information System' Reports and Dashboards + + Unported addons --------------- addon | version | maintainers | summary --- | --- | --- | --- -[mis_builder](mis_builder/) | 18.0.1.8.0 (unported) | sbidoul | Build 'Management Information System' Reports and Dashboards [mis_builder_budget](mis_builder_budget/) | 18.0.2.0.0 (unported) | sbidoul | Create budgets for MIS reports [mis_builder_demo](mis_builder_demo/) | 18.0.1.0.0 (unported) | sbidoul | 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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 OCA/mis-builder Translate me on Weblate Try me on Runboat

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 | sbidoul | Build 'Management Information System' Reports and Dashboards +[mis_builder](mis_builder/) | 19.0.1.0.1 | sbidoul | 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 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 OCA/mis-builder Translate me on Weblate Try me on Runboat

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 +[![Support the OCA](https://odoo-community.org/readme-banner-image)](https://odoo-community.org/get-involved?utm_source=repo-readme) + +# MIS Builder [![Runboat](https://img.shields.io/badge/runboat-Try%20me-875A7B.png)](https://runboat.odoo-community.org/builds?repo=OCA/mis-builder&target_branch=19.0) [![Pre-commit Status](https://github.com/OCA/mis-builder/actions/workflows/pre-commit.yml/badge.svg?branch=19.0)](https://github.com/OCA/mis-builder/actions/workflows/pre-commit.yml?query=branch%3A19.0) [![Build Status](https://github.com/OCA/mis-builder/actions/workflows/test.yml/badge.svg?branch=19.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\