From 90702af67af96d305b00eb11c5bf0bab2a42d6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=A9o=20Goddet?= Date: Tue, 21 Dec 2021 22:40:13 +0100 Subject: [PATCH] generalize auto expand --- mis_builder/models/__init__.py | 2 + mis_builder/models/account_account.py | 14 ++++ .../models/account_analytic_account.py | 14 ++++ mis_builder/models/aep.py | 34 ++++++--- mis_builder/models/expression_evaluator.py | 7 +- mis_builder/models/kpimatrix.py | 73 ++++++++++++++----- mis_builder/models/mis_report.py | 57 ++++++++------- mis_builder/views/mis_report.xml | 6 +- 8 files changed, 144 insertions(+), 63 deletions(-) create mode 100644 mis_builder/models/account_account.py create mode 100644 mis_builder/models/account_analytic_account.py diff --git a/mis_builder/models/__init__.py b/mis_builder/models/__init__.py index af7bfa7e5..610fea94d 100644 --- a/mis_builder/models/__init__.py +++ b/mis_builder/models/__init__.py @@ -8,3 +8,5 @@ from . import aep from . import mis_kpi_data from . import prorata_read_group_mixin +from . import account_account +from . import account_analytic_account diff --git a/mis_builder/models/account_account.py b/mis_builder/models/account_account.py new file mode 100644 index 000000000..c5af009b5 --- /dev/null +++ b/mis_builder/models/account_account.py @@ -0,0 +1,14 @@ +# Copyright 2017 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.osv import expression +from .kpimatrix import RowDetailIdentifierInterface + +class AccountAccount(models.Model): + _name = 'account.account' + _inherit = ['account.account', 'row.detail.identifier.mixin'] + + def get_domain(self): + return [('account_id' , '=', self.id)] diff --git a/mis_builder/models/account_analytic_account.py b/mis_builder/models/account_analytic_account.py new file mode 100644 index 000000000..8fd19db00 --- /dev/null +++ b/mis_builder/models/account_analytic_account.py @@ -0,0 +1,14 @@ +# Copyright 2017 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.osv import expression +from .kpimatrix import RowDetailIdentifierInterface + +class AccountAnalyticAccount(models.Model): + _name = 'account.analytic.account' + _inherit = ['account.analytic.account', 'row.detail.identifier.mixin'] + + def get_domain(self): + return [('analytic_account_id' , '=', self.id)] diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 468cceb26..9fa76f144 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -170,7 +170,7 @@ def _parse_match_object(self, mo): ml_domain = tuple() return field, mode, acc_domain, ml_domain - def parse_expr(self, expr): + def parse_expr(self, expr, row_detail_identifiers = False): """Parse an expression, extracting accounting variables. Move line domains and account selectors are extracted and @@ -178,15 +178,21 @@ def parse_expr(self, expr): we know which account domains to query for each move line domain and mode. """ + if not row_detail_identifiers: + row_detail_identifiers = [None] + for mo in self._ACC_RE.finditer(expr): _, mode, acc_domain, ml_domain = self._parse_match_object(mo) if mode == self.MODE_END and self.smart_end: modes = (self.MODE_INITIAL, self.MODE_VARIATION, self.MODE_END) else: modes = (mode,) - for mode in modes: - key = (ml_domain, mode) - self._map_account_ids[key].add(acc_domain) + for rdi in row_detail_identifiers: + for mode in modes: + key = (ml_domain, rdi, mode) + if rdi: + print(rdi.get_label()) + self._map_account_ids[key].add(acc_domain) def done_parsing(self): """ Replace account domains by account ids in map """ @@ -328,19 +334,23 @@ def do_queries( domain_by_mode = {} ends = [] for key in self._map_account_ids: - domain, mode = key + (domain, row_detail_identifier, mode) = key if mode == self.MODE_END and self.smart_end: # postpone computation of ending balance - ends.append((domain, mode)) + ends.append((domain, row_detail_identifier, mode)) continue if mode not in domain_by_mode: domain_by_mode[mode] = self.get_aml_domain_for_dates( date_from, date_to, mode, target_move ) - domain = list(domain) + domain_by_mode[mode] + domain = list(domain) + domain_by_mode[mode] domain.append(("account_id", "in", self._map_account_ids[key])) if additional_move_line_filter: domain.extend(additional_move_line_filter) + + if row_detail_identifier: + domain = list(domain) + row_detail_identifier.get_domain() + # fetch sum of debit/credit, grouped by account_id accs = aml_model.read_group( domain, @@ -360,9 +370,9 @@ def do_queries( self._data[key][acc["account_id"][0]] = (debit * rate, credit * rate) # compute ending balances by summing initial and variation for key in ends: - domain, mode = key - initial_data = self._data[(domain, self.MODE_INITIAL)] - variation_data = self._data[(domain, self.MODE_VARIATION)] + domain, row_detail_identifier, mode = key + initial_data = self._data[(domain, row_detail_identifier, self.MODE_INITIAL)] + variation_data = self._data[(domain, row_detail_identifier, self.MODE_VARIATION)] account_ids = set(initial_data.keys()) | set(variation_data.keys()) for account_id in account_ids: di, ci = initial_data.get(account_id, (AccountingNone, AccountingNone)) @@ -371,7 +381,7 @@ def do_queries( ) self._data[key][account_id] = (di + dv, ci + cv) - def replace_expr(self, expr): + def replace_expr(self, expr, row_detail_identifier = None): """Replace accounting variables in an expression by their amount. Returns a new expression string. @@ -381,7 +391,7 @@ def replace_expr(self, expr): def f(mo): field, mode, acc_domain, ml_domain = self._parse_match_object(mo) - key = (ml_domain, mode) + key = (ml_domain, row_detail_identifier, mode) account_ids_data = self._data[key] v = AccountingNone account_ids = self._account_ids_by_acc_domain[acc_domain] diff --git a/mis_builder/models/expression_evaluator.py b/mis_builder/models/expression_evaluator.py index a98ac4e37..8a908d81d 100644 --- a/mis_builder/models/expression_evaluator.py +++ b/mis_builder/models/expression_evaluator.py @@ -38,14 +38,14 @@ def aep_do_queries(self): ) self._aep_queries_done = True - def eval_expressions(self, expressions, locals_dict): + def eval_expressions(self, expressions, locals_dict, row_detail_identifier = None): vals = [] drilldown_args = [] name_error = False for expression in expressions: expr = expression and expression.name or "AccountingNone" if self.aep: - replaced_expr = self.aep.replace_expr(expr) + replaced_expr = self.aep.replace_expr(expr, row_detail_identifier) else: replaced_expr = expr val = mis_safe_eval(replaced_expr, locals_dict) @@ -53,11 +53,12 @@ def eval_expressions(self, expressions, locals_dict): if isinstance(val, NameDataError): name_error = True if replaced_expr != expr: - drilldown_args.append({"expr": expr}) + drilldown_args.append({"expr": expr, "row_detail_identifier": row_detail_identifier.get_domain()} if row_detail_identifier else {"expr": expr}) else: drilldown_args.append(None) return vals, drilldown_args, name_error + # TODO maybe depreciated def eval_expressions_by_account(self, expressions, locals_dict): if not self.aep: return diff --git a/mis_builder/models/kpimatrix.py b/mis_builder/models/kpimatrix.py index 8b89b28fe..457bc990a 100644 --- a/mis_builder/models/kpimatrix.py +++ b/mis_builder/models/kpimatrix.py @@ -4,7 +4,7 @@ import logging from collections import OrderedDict, defaultdict -from odoo import _ +from odoo import _, models from odoo.exceptions import UserError from .accounting_none import AccountingNone @@ -21,6 +21,35 @@ _logger = logging.getLogger(__name__) +class RowDetailIdentifierInterface(models.Model): + _name = "row.detail.identifier.mixin" + + def get_id(self): + """ return a python compatible row id """ + from .mis_report import _python_var + return _python_var(self.get_label()) + + def get_label(self): + """ return the row label """ + return self.name_get()[0][1] + + def iter_parents(self): # TODO assess utility# TODO assess + """ yield all parent RowDetailIdentifierInterface + (eg if self references an account, yield parent groups from bottom to top) + This is important so detail rows can be added in any order and the KpiMatrix + can create placeholders for parent rows. + """ + pass + + def get_domain(self): + return [('account_id', '=', self.id)] + +# def __hash__(self): +# pass + + def __lt__(self): # TODO assess utility + pass + class KpiMatrixRow(object): # TODO: ultimately, the kpi matrix will become ignorant of KPI's and @@ -28,34 +57,34 @@ class KpiMatrixRow(object): # It is already ignorant of period and only knowns about columns. # This will require a correct abstraction for expanding row details. - def __init__(self, matrix, kpi, account_id=None, parent_row=None): + def __init__(self, matrix, kpi, row_detail_identifier=None, parent_row=None): self._matrix = matrix self.kpi = kpi - self.account_id = account_id + self.row_detail_identifier = row_detail_identifier self.description = "" self.parent_row = parent_row - if not self.account_id: + if not self.row_detail_identifier: self.style_props = self._matrix._style_model.merge( [self.kpi.report_id.style_id, self.kpi.style_id] ) else: self.style_props = self._matrix._style_model.merge( - [self.kpi.report_id.style_id, self.kpi.auto_expand_accounts_style_id] + [self.kpi.report_id.style_id, self.kpi.row_details_style_id] ) @property def label(self): - if not self.account_id: + if not self.row_detail_identifier: return self.kpi.description else: - return self._matrix.get_account_name(self.account_id) + return self.row_detail_identifier.get_label() @property def row_id(self): - if not self.account_id: + if not self.row_detail_identifier: return self.kpi.name else: - return "{}:{}".format(self.kpi.name, self.account_id) + return "{}:{}".format(self.kpi.name, self.row_detail_identifier.get_id()) def iter_cell_tuples(self, cols=None): if cols is None: @@ -217,26 +246,34 @@ def set_values(self, kpi, col_key, vals, drilldown_args, tooltips=True): Invoke this after declaring the kpi and the column. """ - self.set_values_detail_account( + self.set_values_detail( kpi, col_key, None, vals, drilldown_args, tooltips ) def set_values_detail_account( self, kpi, col_key, account_id, vals, drilldown_args, tooltips=True + ): + """Compatibility function for old account_id drilldown api + """ + # TODO : if needed : put the account_id in a RowDetailIdentifier class and call set_values_detail + pass + + def set_values_detail( + self, kpi, col_key, row_detail_identifier, vals, drilldown_args, tooltips=True ): """Set values for a kpi and a column and a detail account. Invoke this after declaring the kpi and the column. """ - if not account_id: + if not row_detail_identifier: row = self._kpi_rows[kpi] else: kpi_row = self._kpi_rows[kpi] - if account_id in self._detail_rows[kpi]: - row = self._detail_rows[kpi][account_id] + if row_detail_identifier.get_id() in self._detail_rows[kpi]: + row = self._detail_rows[kpi][row_detail_identifier.get_id()] # TODO need to check if need a change else: - row = KpiMatrixRow(self, kpi, account_id, parent_row=kpi_row) - self._detail_rows[kpi][account_id] = row + row = KpiMatrixRow(self, kpi, row_detail_identifier, parent_row=kpi_row) + self._detail_rows[kpi][row_detail_identifier.get_id()] = row col = self._cols[col_key] cell_tuple = [] assert len(vals) == col.colspan @@ -412,7 +449,7 @@ def compute_sums(self): for row in self.iter_rows(): acc = SimpleArray([AccountingNone] * (len(common_subkpis) or 1)) if row.kpi.accumulation_method == ACC_SUM and not ( - row.account_id and not sum_accdet + row.row_detail_identifier and not sum_accdet ): for sign, col_to_sum in col_to_sum_keys: cell_tuple = self._cols[col_to_sum].get_cell_tuple_for_row(row) @@ -429,10 +466,10 @@ def compute_sums(self): acc += SimpleArray(vals) else: acc -= SimpleArray(vals) - self.set_values_detail_account( + self.set_values_detail( row.kpi, sumcol_key, - row.account_id, + row.row_detail_identifier, acc, [None] * (len(common_subkpis) or 1), tooltips=False, diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 75b644652..59d871c7e 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -27,6 +27,9 @@ _logger = logging.getLogger(__name__) +DETAILS_NONE = "none" +DETAILS_ACCOUNTS = "accounts" +DETAILS_ANALYTIC_ACCOUNTS = "analytic_accounts" class SubKPITupleLengthError(UserError): pass @@ -86,12 +89,25 @@ class MisReportKpi(models.Model): copy=True, string="Expressions", ) - auto_expand_accounts = fields.Boolean(string="Display details by account") - auto_expand_accounts_style_id = fields.Many2one( + + #auto_expand_accounts = fields.Boolean(string="Display details by account") + row_details_style_id = fields.Many2one( string="Style for account detail rows", comodel_name="mis.report.style", required=False, ) + + row_details_selector = fields.Selection( + [ + (DETAILS_NONE, _("None")), + (DETAILS_ACCOUNTS, _("Accounts")), + (DETAILS_ANALYTIC_ACCOUNTS, _("Analytic Accounts")) + ], + required=True, + string="Auto Expand Details", + default=DETAILS_NONE, + ) + style_id = fields.Many2one( string="Style", comodel_name="mis.report.style", required=False ) @@ -247,6 +263,13 @@ def _get_expressions(self, subkpis): else: return [None] + def _get_row_detail_identifiers(self): + if self.row_details_selector == DETAILS_ACCOUNTS: + return self.env["account.account"].search([]) + elif self.row_details_selector == DETAILS_ANALYTIC_ACCOUNTS: + return self.env["account.analytic.account"].search([]) + return False + class MisReportSubkpi(models.Model): _name = "mis.report.subkpi" @@ -557,8 +580,7 @@ def _prepare_aep(self, companies, currency=None): aep = AEP(companies, currency, self.account_model) for kpi in self.all_kpi_ids: for expression in kpi.expression_ids: - if expression.name: - aep.parse_expr(expression.name) + aep.parse_expr(expression.name, kpi._get_row_detail_identifiers()) aep.done_parsing() return aep @@ -745,30 +767,11 @@ def _declare_and_compute_col( # noqa: C901 (TODO simplify this fnction) drilldown_args = [None] * col.colspan kpi_matrix.set_values(kpi, col_key, vals, drilldown_args) + + for rdi in kpi._get_row_detail_identifiers() or []: + (vals, drilldown_args, name_error) = expression_evaluator.eval_expressions(expressions, locals_dict, rdi) + kpi_matrix.set_values_detail(kpi, col_key, rdi, vals, drilldown_args) - if ( - name_error - or no_auto_expand_accounts - or not kpi.auto_expand_accounts - ): - continue - - for ( - account_id, - vals, - drilldown_args, - _name_error, - ) in expression_evaluator.eval_expressions_by_account( - expressions, locals_dict - ): - for drilldown_arg in drilldown_args: - if not drilldown_arg: - continue - drilldown_arg["period_id"] = col_key - drilldown_arg["kpi_id"] = kpi.id - kpi_matrix.set_values_detail_account( - kpi, col_key, account_id, vals, drilldown_args - ) if len(recompute_queue) == 0: # nothing to recompute, we are done diff --git a/mis_builder/views/mis_report.xml b/mis_builder/views/mis_report.xml index 466f006ed..451862764 100644 --- a/mis_builder/views/mis_report.xml +++ b/mis_builder/views/mis_report.xml @@ -152,10 +152,10 @@ /> - +