diff --git a/mis_builder/README.rst b/mis_builder/README.rst
index 48aacc608..f02f7e712 100644
--- a/mis_builder/README.rst
+++ b/mis_builder/README.rst
@@ -1,7 +1,3 @@
-.. image:: https://odoo-community.org/readme-banner-image
- :target: https://odoo-community.org/get-involved?utm_source=readme
- :alt: Odoo Community Association
-
===========
MIS Builder
===========
@@ -17,7 +13,7 @@ MIS Builder
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
:target: https://odoo-community.org/page/development-status
:alt: Production/Stable
-.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
: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
diff --git a/mis_builder/__manifest__.py b/mis_builder/__manifest__.py
index 1cf14dc59..ece29c3e9 100644
--- a/mis_builder/__manifest__.py
+++ b/mis_builder/__manifest__.py
@@ -29,6 +29,9 @@
"report/mis_report_instance_qweb.xml",
"report/mis_report_instance_xlsx.xml",
],
+ "demo": [
+ "demo/mis_multicompany_demo.xml",
+ ],
"assets": {
"web.assets_backend": [
"mis_builder/static/src/components/mis_report_widget.esm.js",
diff --git a/mis_builder/demo/mis_multicompany_demo.xml b/mis_builder/demo/mis_multicompany_demo.xml
new file mode 100644
index 000000000..44170bd9f
--- /dev/null
+++ b/mis_builder/demo/mis_multicompany_demo.xml
@@ -0,0 +1,147 @@
+
+
+
+
+
+ Other Company
+
+
+
+
+
+
+
+
+
+ 600000
+ Expenses
+ expense
+
+
+
+
+
+ 611000
+ Purchase of Equipments
+ expense
+
+
+
+
+
+ 101401
+ Bank
+ asset_current
+
+
+
+
+
+ Miscellaneous
+ general
+ MISC
+
+
+
+
+
+
+
+ 2026-03-10
+
+
+
+
+
+
+
+ 2026-02-20
+
+
+
+
+
+ Multi-Company Expenses
+
+
+
+
+ exp
+ Expenses
+ True
+ 10
+
+
+
+
+ balp[600%]
+
+
+
+
+ equip
+ Equipment
+ True
+ 20
+
+
+
+
+ balp[611%]
+
+
+
+
+ total
+ Total
+ 100
+ AccountingTotal
+
+
+
+
+ exp + equip
+
+
+
+
+ Multi-Company Expenses Report
+
+ True
+ True
+
+
+
+
+
+
+ Year to Date
+ relative
+ actuals
+ y
+ 0
+ 1
+ 10
+
+
+
+
+
diff --git a/mis_builder/models/kpimatrix.py b/mis_builder/models/kpimatrix.py
index 63c706d7e..77d760ff8 100644
--- a/mis_builder/models/kpimatrix.py
+++ b/mis_builder/models/kpimatrix.py
@@ -144,6 +144,7 @@ def __init__(
env,
companies=None,
account_model="account.account",
+ companies_as_columns=False,
):
# cache language id for faster rendering
lang_model = env["res.lang"]
@@ -151,6 +152,7 @@ def __init__(
self._style_model = env["mis.report.style"]
self._account_model = env[account_model]
self._companies = companies
+ self._companies_as_columns = companies_as_columns
# data structures
# { kpi: KpiMatrixRow }
self._kpi_rows = OrderedDict()
@@ -489,7 +491,11 @@ def _get_account_name(self, account):
# (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:
+ if (
+ is_multi_company
+ and len(account_companies) == 1
+ and not self._companies_as_columns
+ ):
# 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
@@ -498,6 +504,8 @@ def _get_account_name(self, account):
# 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.
+ # When companies are shown as separate columns, each column is
+ # already a single company, so the per-row suffix is redundant.
account_name = f"{account_name} [{account_companies.display_name}]"
return account_name
diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py
index e5f0cad42..6c2ff49c6 100644
--- a/mis_builder/models/mis_report.py
+++ b/mis_builder/models/mis_report.py
@@ -539,9 +539,14 @@ def copy(self, default=None):
# TODO: kpi name cannot be start with query name
- def prepare_kpi_matrix(self, companies=None):
+ def prepare_kpi_matrix(self, companies=None, companies_as_columns=False):
self.ensure_one()
- kpi_matrix = KpiMatrix(self.env, companies, self.account_model)
+ kpi_matrix = KpiMatrix(
+ self.env,
+ companies,
+ self.account_model,
+ companies_as_columns=companies_as_columns,
+ )
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 070ffe1a5..a85a465cf 100644
--- a/mis_builder/models/mis_report_instance.py
+++ b/mis_builder/models/mis_report_instance.py
@@ -230,6 +230,12 @@ def _compute_dates(self):
default=1,
)
subkpi_ids = fields.Many2many("mis.report.subkpi", string="Sub KPI Filter")
+ company_id = fields.Many2one(
+ comodel_name="res.company",
+ string="Company",
+ help="Filter this column to a single company. "
+ "Leave empty to include all allowed companies.",
+ )
source = fields.Selection(
[
@@ -393,6 +399,8 @@ def _get_additional_move_line_filter(self):
return []
# First get the report-level filter domain.
domain = self.report_instance_id._get_filter_domain(self.source_aml_model_name)
+ if self.company_id:
+ domain.append(("company_id", "=", self.company_id.id))
if self.analytic_domain:
# Then extend it with the column-level analytic domain.
domain.extend(ast.literal_eval(self.analytic_domain))
@@ -411,9 +419,18 @@ def _get_additional_query_filter(self, query):
self.ensure_one()
domain = []
if company_field := query.sudo().company_field_id:
- query_company_ids = self.report_instance_id.query_company_ids.ids
- assert query_company_ids
- domain = [(company_field.name, "in", query_company_ids)]
+ if self.company_id:
+ domain = [(company_field.name, "=", self.company_id.id)]
+ else:
+ query_company_ids = self.report_instance_id.query_company_ids.ids
+ if not query_company_ids:
+ raise UserError(
+ self.env._(
+ "No companies are configured for report '%s'.",
+ self.report_instance_id.name,
+ )
+ )
+ domain = [(company_field.name, "in", query_company_ids)]
return domain
@api.constrains("mode", "source")
@@ -771,6 +788,49 @@ def get_views(self, views, options=None):
result = super().get_views(views, options)
return result
+ def action_generate_company_columns(self):
+ """Generate one period/column per company in company_ids + Total."""
+ self.ensure_one()
+ if not self.multi_company or len(self.company_ids) < 2:
+ raise UserError(
+ self.env._(
+ "Please enable Multi Company and select at least 2 companies "
+ "before generating company columns."
+ )
+ )
+ self.period_ids.unlink()
+ seq = 10
+ period_ids = []
+ for company in self.company_ids:
+ period = self.env["mis.report.instance.period"].create(
+ {
+ "report_instance_id": self.id,
+ "name": company.name,
+ "mode": MODE_REL,
+ "source": SRC_ACTUALS,
+ "type": "y",
+ "offset": 0,
+ "duration": 1,
+ "sequence": seq,
+ "company_id": company.id,
+ }
+ )
+ period_ids.append(period.id)
+ seq += 10
+ self.env["mis.report.instance.period"].create(
+ {
+ "report_instance_id": self.id,
+ "name": self.env._("Total"),
+ "mode": MODE_NONE,
+ "source": SRC_SUMCOL,
+ "sequence": seq,
+ "source_sumcol_ids": [
+ (0, 0, {"period_to_sum_id": pid, "sign": "+"}) for pid in period_ids
+ ],
+ }
+ )
+ self.comparison_mode = True
+
def preview(self):
self.ensure_one()
view_id = self.env.ref("mis_builder.mis_report_instance_result_view_form")
@@ -877,7 +937,15 @@ def _compute_matrix(self):
"""
self.ensure_one()
aep = self.report_id._prepare_aep(self.query_company_ids, self.currency_id)
- kpi_matrix = self.report_id.prepare_kpi_matrix(self.query_company_ids)
+ multi_company = self.multi_company and len(self.query_company_ids) > 1
+ companies_as_columns = multi_company and all(
+ p.company_id
+ for p in self.period_ids
+ if p.source in (SRC_ACTUALS, SRC_ACTUALS_ALT)
+ )
+ kpi_matrix = self.report_id.prepare_kpi_matrix(
+ self.query_company_ids, companies_as_columns
+ )
for period in self.period_ids:
description = None
if period.mode == MODE_NONE:
@@ -1023,3 +1091,82 @@ def _compute_user_can_edit_annotation(self):
self.user_can_edit_annotation = self.env.user.has_group(
"mis_builder.group_edit_annotation"
)
+
+ @api.model
+ def _post_demo_moves(self):
+ """Post demo journal entries for the multi-company demo report."""
+ # Post Other Company moves created via XML demo data
+ for xmlid in [
+ "mis_builder.move_other_consulting",
+ "mis_builder.move_other_furniture",
+ ]:
+ move = self.env.ref(xmlid, raise_if_not_found=False)
+ if move and move.state == "draft":
+ try:
+ move.with_company(move.company_id).action_post()
+ except Exception:
+ _logger.warning("Could not post demo move %s", xmlid)
+ # Create and post YourCompany demo moves so all columns show values
+ main_company = self.env.ref("base.main_company")
+ expense_account = self.env["account.account"].search(
+ [
+ ("company_ids", "in", [main_company.id]),
+ ("code", "=like", "6%"),
+ ("account_type", "=", "expense"),
+ ],
+ limit=1,
+ )
+ equip_account = self.env["account.account"].search(
+ [
+ ("company_ids", "in", [main_company.id]),
+ ("code", "=like", "61%"),
+ ("account_type", "=", "expense"),
+ ],
+ limit=1,
+ )
+ bank_account = self.env["account.account"].search(
+ [
+ ("company_ids", "in", [main_company.id]),
+ ("account_type", "in", ["asset_cash", "asset_current"]),
+ ],
+ limit=1,
+ )
+ journal = self.env["account.journal"].search(
+ [("type", "=", "general"), ("company_id", "=", main_company.id)],
+ limit=1,
+ )
+ if not (expense_account and bank_account and journal):
+ _logger.warning("Could not find accounts for YourCompany demo moves")
+ return
+ Move = self.env["account.move"].with_company(main_company)
+ for name, amount, account in [
+ ("Main Office Expenses", 18500, expense_account),
+ ("Main Office Equipment", 3800, equip_account or expense_account),
+ ]:
+ move = Move.create(
+ {
+ "company_id": main_company.id,
+ "journal_id": journal.id,
+ "date": "2026-03-15",
+ "line_ids": [
+ (
+ 0,
+ 0,
+ {"account_id": account.id, "debit": amount, "name": name},
+ ),
+ (
+ 0,
+ 0,
+ {
+ "account_id": bank_account.id,
+ "credit": amount,
+ "name": name,
+ },
+ ),
+ ],
+ }
+ )
+ try:
+ move.with_company(main_company).action_post()
+ except Exception:
+ _logger.warning("Could not post YourCompany demo move: %s", name)
diff --git a/mis_builder/readme/newsfragments/784.feature b/mis_builder/readme/newsfragments/784.feature
new file mode 100644
index 000000000..fbee018e1
--- /dev/null
+++ b/mis_builder/readme/newsfragments/784.feature
@@ -0,0 +1,3 @@
+Add ``company_id`` field to report instance periods to filter each column
+to a single company, and a "Generate Company Columns" button that
+auto-creates one column per company plus a Total.
diff --git a/mis_builder/static/description/index.html b/mis_builder/static/description/index.html
index 11ee4dac8..49b622e61 100644
--- a/mis_builder/static/description/index.html
+++ b/mis_builder/static/description/index.html
@@ -3,7 +3,7 @@
-README.rst
+MIS Builder
-
+
+
MIS Builder
-
-
-
-
-
-
+
Your preferred way to install addons will work with MIS Builder.
An easy way to install it with all its dependencies is using pip:
-
+
To configure this module, you need to:
- Go to Accounting > Configuration > MIS Reporting > MIS Report
@@ -485,7 +480,7 @@
-
+
A typical extension is to provide a mechanism to filter reports on
analytic dimensions or operational units. To implement this, you can
override _get_additional_move_line_filter and _get_additional_filter
@@ -495,7 +490,7 @@
different columns to show different analytic accounts.
-
+
-
+
-
+
- Fix computation of currency conversion rates
(#737)
@@ -515,9 +510,9 @@
-
+
-
+
- Introduction of annotations on report cells. Added notes will be
pinted when exporting to PDF and Excel.
@@ -526,9 +521,9 @@
-
+
-
+
- Add support for branch companies.
(#648)
@@ -536,7 +531,7 @@
-
+
Bugfixes
- Restore compatibility with python 3.9
@@ -544,7 +539,7 @@
-
+
Bugfixes
- Resolve a permission issue when creating report periods with a user
@@ -553,7 +548,7 @@
-
+
Features
- Improve UX by adding the option to edit the pivot date directly on the
@@ -561,7 +556,7 @@
-
+
Bugfixes
- Support users without timezone.
@@ -609,7 +604,7 @@
-
+
Bugfixes
- Allow deleting a report that has subreports.
@@ -617,7 +612,7 @@
-
+
Bugfixes
- Fix access right issue when clicking the “Save” button on a MIS Report
@@ -626,7 +621,7 @@
-
+
Features
- Remove various field size limits.
@@ -658,7 +653,7 @@
-
+
Bugfixes
- When on a MIS Report Instance, if you wanted to generate a new line of
@@ -671,7 +666,7 @@
-
+
Bugfixes
- Fix drilldown action name when the account model has been customized.
@@ -679,7 +674,7 @@
-
+
Bugfixes
- While duplicating a MIS report instance, comparison columns are
@@ -689,7 +684,7 @@
-
+
Features
- The drilldown action name displayed on the breadcrumb has been
@@ -702,7 +697,7 @@
-
+
Bugfixes
- Having a “Compare columns” added on a KPI with an associated style
@@ -717,7 +712,7 @@
-
+
Bugfixes
- The “Settings” button is now displayed for users with the “Show full
@@ -726,7 +721,7 @@
-
+
Bugfixes
- Fix TypeError: 'module' object is not iterable when using budgets
@@ -734,7 +729,7 @@
-
+
Features
- Add column-level filters on analytic account and analytic tags. These
@@ -753,11 +748,11 @@
-
+
Migration to odoo 13.0.
-
+
Features
- The account_id field of the model selected in ‘Move lines source’
@@ -797,7 +792,7 @@
-
+
Features
- New year-to-date mode for defining periods.
@@ -822,7 +817,7 @@
-
+
Features
Dynamic analytic filters in report preview are not yet available in 11,
this requires an update to the JS widget that proved difficult to
@@ -873,7 +868,7 @@
analytic_account_id field.
-
+
- [FIX] Fix bug in company_default_get call returning id instead of
recordset (#103)
@@ -883,7 +878,7 @@
-
+
- [FIX] Missing comparison operator for AccountingNone leading to errors
in pbal computations
@@ -891,7 +886,7 @@
-
+
- [FIX] make subkpi ordering deterministic
(#71)
@@ -906,12 +901,12 @@
-
+
Migration to Odoo 11. No new feature.
(#67)
-
+
New features:
- [ADD] month and year relative periods, easier to use than date ranges
@@ -949,7 +944,7 @@
-
+
Bug fix:
- [FIX] issue with initial balance rounding.
@@ -957,7 +952,7 @@
-
+
Bug fix:
- [FIX] fix error saving KPI on newly created reports.
@@ -965,7 +960,7 @@
-
+
New features:
- [ADD] Alternative move line source per report column. This makes mis
@@ -1010,7 +1005,7 @@
-
+
- [IMP] more robust behaviour in presence of missing expressions
- [FIX] indent style
@@ -1023,7 +1018,7 @@
-
+
- [IMP] Add refresh button in mis report preview.
- [IMP] Widget code changes to allow to add fields in the widget more
@@ -1031,7 +1026,7 @@
-
+
- [IMP] remove unused argument in declare_and_compute_period() for a
cleaner API. This is a breaking API changing merged in urgency before
@@ -1039,7 +1034,7 @@
-
+
Part of the work for this release has been done at the Sorrento sprint
April 26-29, 2016. The rest (ie a major refactoring) has been done in
the weeks after.
@@ -1088,7 +1083,7 @@
-
+
Pre-history. Or rather, you need to look at the git log.
-
+
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
@@ -1126,15 +1121,15 @@
Do not contact contributors directly about support or help with technical issues.
-