From 91546b594293871ff8efccca97d44cabc4d2395d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=A9o=20Goddet?= Date: Mon, 14 Mar 2022 23:34:43 +0100 Subject: [PATCH] Generalized auto detail expansion mechanism This commit introduce a mechanism to expand automatically a kpi by any field. --- mis_builder/models/aep.py | 167 +++++++++++++----- mis_builder/models/expression_evaluator.py | 21 ++- mis_builder/models/kpimatrix.py | 70 ++++---- mis_builder/models/mis_report.py | 47 +++-- mis_builder/models/mis_report_instance.py | 4 + mis_builder/tests/test_aep.py | 108 ++++++++++- mis_builder/tests/test_mis_report_instance.py | 14 +- mis_builder/views/mis_report.xml | 1 + 8 files changed, 328 insertions(+), 104 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index d2b57bd02..cf57dd2f0 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -14,6 +14,8 @@ _DOMAIN_START_RE = re.compile(r"\(|(['\"])[!&|]\1") +UNCLASSIFIED_ROW_DETAIL = "other" + def _is_domain(s): """Test if a string looks like an Odoo domain""" @@ -299,25 +301,22 @@ def do_queries( date_from, date_to, additional_move_line_filter=None, - aml_model=None, + aml_model="account.move.line", + auto_expand_col_name=None, ): """Query sums of debit and credit for all accounts and domains used in expressions. This method must be executed after done_parsing(). """ - if not aml_model: - aml_model = self.env["account.move.line"] - else: - aml_model = self.env[aml_model] - aml_model = aml_model.with_context(active_test=False) + aml_model = self.env[aml_model].with_context(active_test=False) company_rates = self._get_company_rates(date_to) # {(domain, mode): {account_id: (debit, credit)}} self._data = defaultdict(dict) domain_by_mode = {} ends = [] for key in self._map_account_ids: - domain, mode = key + (domain, mode) = key if mode == self.MODE_END and self.smart_end: # postpone computation of ending balance ends.append((domain, mode)) @@ -330,13 +329,16 @@ def do_queries( domain.append(("account_id", "in", self._map_account_ids[key])) if additional_move_line_filter: domain.extend(additional_move_line_filter) + + get_fields = ["debit", "credit", "account_id", "company_id"] + group_by_fields = ["account_id", "company_id"] + if auto_expand_col_name: + get_fields = [auto_expand_col_name] + get_fields + group_by_fields = [auto_expand_col_name] + group_by_fields + # fetch sum of debit/credit, grouped by account_id - accs = aml_model.read_group( - domain, - ["debit", "credit", "account_id", "company_id"], - ["account_id", "company_id"], - lazy=False, - ) + accs = aml_model.read_group(domain, get_fields, group_by_fields, lazy=False) + for acc in accs: rate, dp = company_rates[acc["company_id"][0]] debit = acc["debit"] or 0.0 @@ -346,19 +348,45 @@ def do_queries( ): # in initial mode, ignore accounts with 0 balance continue - self._data[key][acc["account_id"][0]] = (debit * rate, credit * rate) + if ( + auto_expand_col_name + and auto_expand_col_name in acc + and acc[auto_expand_col_name] + ): + rdi_id = acc[auto_expand_col_name][0] + else: + rdi_id = UNCLASSIFIED_ROW_DETAIL + if not self._data[key].get(rdi_id, False): + self._data[key][rdi_id] = defaultdict(dict) + self._data[key][rdi_id][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)] - 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)) - dv, cv = variation_data.get( - account_id, (AccountingNone, AccountingNone) + rdis = set(initial_data.keys()) | set(variation_data.keys()) + for rdi in rdis: + if not initial_data.get(rdi, False): + initial_data[rdi] = defaultdict(dict) + if not variation_data.get(rdi, False): + variation_data[rdi] = defaultdict(dict) + if not self._data[key].get(rdi, False): + self._data[key][rdi] = defaultdict(dict) + + account_ids = set(initial_data[rdi].keys()) | set( + variation_data[rdi].keys() ) - self._data[key][account_id] = (di + dv, ci + cv) + for account_id in account_ids: + di, ci = initial_data[rdi].get( + account_id, (AccountingNone, AccountingNone) + ) + dv, cv = variation_data[rdi].get( + account_id, (AccountingNone, AccountingNone) + ) + self._data[key][rdi][account_id] = (di + dv, ci + cv) def replace_expr(self, expr): """Replace accounting variables in an expression by their amount. @@ -371,23 +399,25 @@ def replace_expr(self, expr): def f(mo): field, mode, acc_domain, ml_domain = self._parse_match_object(mo) key = (ml_domain, mode) - account_ids_data = self._data[key] + rdi_ids_data = self._data[key] v = AccountingNone account_ids = self._account_ids_by_acc_domain[acc_domain] - for account_id in account_ids: - debit, credit = account_ids_data.get( - account_id, (AccountingNone, AccountingNone) - ) - if field == "bal": - v += debit - credit - elif field == "pbal" and debit >= credit: - v += debit - credit - elif field == "nbal" and debit < credit: - v += debit - credit - elif field == "deb": - v += debit - elif field == "crd": - v += credit + for rdi in rdi_ids_data: + account_ids_data = self._data[key][rdi] + for account_id in account_ids: + debit, credit = account_ids_data.get( + account_id, (AccountingNone, AccountingNone) + ) + if field == "bal": + v += debit - credit + elif field == "pbal" and debit >= credit: + v += debit - credit + elif field == "nbal" and debit < credit: + v += debit - credit + elif field == "deb": + v += debit + elif field == "crd": + v += credit # in initial balance mode, assume 0 is None # as it does not make sense to distinguish 0 from "no data" if ( @@ -401,11 +431,11 @@ def f(mo): return self._ACC_RE.sub(f, expr) def replace_exprs_by_account_id(self, exprs): - """Replace accounting variables in a list of expression - by their amount, iterating by accounts involved in the expression. + """This method is depreciated and replaced by replace_exprs_by_row_detail. + Replace accounting variables in a list of expression + by their amount, iterating by accounts involved in the expression. yields account_id, replaced_expr - This method must be executed after do_queries(). """ @@ -417,7 +447,7 @@ def f(mo): if account_id not in self._account_ids_by_acc_domain[acc_domain]: return "(AccountingNone)" # here we know account_id is involved in acc_domain - account_ids_data = self._data[key] + account_ids_data = self._data[key][UNCLASSIFIED_ROW_DETAIL] debit, credit = account_ids_data.get( account_id, (AccountingNone, AccountingNone) ) @@ -452,7 +482,7 @@ def f(mo): for mo in self._ACC_RE.finditer(expr): field, mode, acc_domain, ml_domain = self._parse_match_object(mo) key = (ml_domain, mode) - account_ids_data = self._data[key] + account_ids_data = self._data[key][UNCLASSIFIED_ROW_DETAIL] for account_id in self._account_ids_by_acc_domain[acc_domain]: if account_id in account_ids_data: account_ids.add(account_id) @@ -460,6 +490,58 @@ def f(mo): for account_id in account_ids: yield account_id, [self._ACC_RE.sub(f, expr) for expr in exprs] + def replace_exprs_by_row_detail(self, exprs): + """Replace accounting variables in a list of expression + by their amount, iterating by accounts involved in the expression. + + yields account_id, replaced_expr + + This method must be executed after do_queries(). + """ + + def f(mo): + field, mode, acc_domain, ml_domain = self._parse_match_object(mo) + key = (ml_domain, mode) + v = AccountingNone + account_ids_data = self._data[key][rdi_id] + account_ids = self._account_ids_by_acc_domain[acc_domain] + + for account_id in account_ids: + debit, credit = account_ids_data.get( + account_id, (AccountingNone, AccountingNone) + ) + if field == "bal": + v += debit - credit + elif field == "pbal" and debit >= credit: + v += debit - credit + elif field == "nbal" and debit < credit: + v += debit - credit + elif field == "deb": + v += debit + elif field == "crd": + v += credit + # in initial balance mode, assume 0 is None + # as it does not make sense to distinguish 0 from "no data" + if ( + v is not AccountingNone + and mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) + and float_is_zero(v, precision_digits=self.dp) + ): + v = AccountingNone + return "(" + repr(v) + ")" + + rdi_ids = set() + for expr in exprs: + for mo in self._ACC_RE.finditer(expr): + field, mode, acc_domain, ml_domain = self._parse_match_object(mo) + key = (ml_domain, mode) + rdis_data = self._data[key] + for rdi_id in rdis_data.keys(): + rdi_ids.add(rdi_id) + + for rdi_id in rdi_ids: + yield rdi_id, [self._ACC_RE.sub(f, expr) for expr in exprs] + @classmethod def _get_balances(cls, mode, companies, date_from, date_to): expr = "deb{mode}[], crd{mode}[]".format(mode=mode) @@ -470,7 +552,10 @@ def _get_balances(cls, mode, companies, date_from, date_to): aep.parse_expr(expr) aep.done_parsing() aep.do_queries(date_from, date_to) - return aep._data[((), mode)] + + return aep._data[((), mode)].get(UNCLASSIFIED_ROW_DETAIL, {}) + # to keep compatibility, we give the UNCLASSIFIED_ROW_DETAIL + # (expecting that auto_expand_col_names=None was given to do_queries ) @classmethod def get_balances_initial(cls, companies, date): diff --git a/mis_builder/models/expression_evaluator.py b/mis_builder/models/expression_evaluator.py index 32705f55d..5faa231b3 100644 --- a/mis_builder/models/expression_evaluator.py +++ b/mis_builder/models/expression_evaluator.py @@ -20,13 +20,14 @@ def __init__( self.aml_model = aml_model self._aep_queries_done = False - def aep_do_queries(self): + def aep_do_queries(self, auto_expand_col_name=None): if self.aep and not self._aep_queries_done: self.aep.do_queries( self.date_from, self.date_to, self.additional_move_line_filter, self.aml_model, + auto_expand_col_name, ) self._aep_queries_done = True @@ -50,6 +51,7 @@ def eval_expressions(self, expressions, locals_dict): drilldown_args.append(None) return vals, drilldown_args, name_error + # we keep it for backward compatibility def eval_expressions_by_account(self, expressions, locals_dict): if not self.aep: return @@ -66,3 +68,20 @@ def eval_expressions_by_account(self, expressions, locals_dict): else: drilldown_args.append(None) yield account_id, vals, drilldown_args, name_error + + def eval_expressions_by_row_detail(self, expressions, locals_dict): + if not self.aep: + return + exprs = [e and e.name or "AccountingNone" for e in expressions] + for rdi_id, replaced_exprs in self.aep.replace_exprs_by_row_detail(exprs): + vals = [] + drilldown_args = [] + name_error = False + for expr, replaced_expr in zip(exprs, replaced_exprs): + val = mis_safe_eval(replaced_expr, locals_dict) + vals.append(val) + if replaced_expr != expr: + drilldown_args.append({"expr": expr, "row_detail": rdi_id}) + else: + drilldown_args.append(None) + yield rdi_id, vals, drilldown_args, name_error diff --git a/mis_builder/models/kpimatrix.py b/mis_builder/models/kpimatrix.py index 5d396cd83..b857e54dc 100644 --- a/mis_builder/models/kpimatrix.py +++ b/mis_builder/models/kpimatrix.py @@ -8,6 +8,7 @@ from odoo.exceptions import UserError from .accounting_none import AccountingNone +from .aep import UNCLASSIFIED_ROW_DETAIL from .mis_kpi_data import ACC_SUM from .mis_safe_eval import DataError, mis_safe_eval from .simple_array import SimpleArray @@ -22,13 +23,13 @@ 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] ) @@ -39,17 +40,17 @@ def __init__(self, matrix, kpi, account_id=None, parent_row=None): @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._matrix.get_rdi_name(self.row_detail_identifier) @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) def iter_cell_tuples(self, cols=None): if cols is None: @@ -147,12 +148,12 @@ def __init__( class KpiMatrix(object): - def __init__(self, env, multi_company=False, account_model="account.account"): + def __init__(self, env, multi_company=False, rdi_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._rdi_model = env[rdi_model] # data structures # { kpi: KpiMatrixRow } self._kpi_rows = OrderedDict() @@ -165,7 +166,7 @@ def __init__(self, env, multi_company=False, account_model="account.account"): # { col_key (left of sum): (col_key, [(sign, sum_col_key)]) self._sum_todo = {} # { account_id: account_name } - self._account_names = {} + self._rdi_names = {} self._multi_company = multi_company def declare_kpi(self, kpi): @@ -215,22 +216,23 @@ def set_values(self, kpi, col_key, vals, drilldown_args, tooltips=True): kpi, col_key, None, vals, drilldown_args, tooltips ) + # TODO this could be renamed set_values_detail def set_values_detail_account( - self, kpi, col_key, account_id, vals, drilldown_args, tooltips=True + 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 in self._detail_rows[kpi]: + row = self._detail_rows[kpi][row_detail_identifier] 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] = row col = self._cols[col_key] cell_tuple = [] assert len(vals) == col.colspan @@ -406,7 +408,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) @@ -426,7 +428,7 @@ def compute_sums(self): self.set_values_detail_account( row.kpi, sumcol_key, - row.account_id, + row.row_detail_identifier, acc, [None] * (len(common_subkpis) or 1), tooltips=False, @@ -462,23 +464,27 @@ def iter_subcols(self): for subcol in col.iter_subcols(): yield subcol - def _load_account_names(self): - account_ids = set() + def _load_rdi_names(self): + rdi_ids = set() for detail_rows in self._detail_rows.values(): - account_ids.update(detail_rows.keys()) - accounts = self._account_model.search([("id", "in", list(account_ids))]) - self._account_names = {a.id: self._get_account_name(a) for a in accounts} - - def _get_account_name(self, account): - result = u"{} {}".format(account.code, account.name) - if self._multi_company: - result = u"{} [{}]".format(result, account.company_id.name) + rdi_ids.update(detail_rows.keys()) + rdi_ids = list(rdi_ids) + if UNCLASSIFIED_ROW_DETAIL in rdi_ids: + rdi_ids.remove(UNCLASSIFIED_ROW_DETAIL) + rdis = self._rdi_model.search([("id", "in", rdi_ids)]) + self._rdi_names = {rdi.id: self._get_rdi_name(rdi) for rdi in rdis} + self._rdi_names[UNCLASSIFIED_ROW_DETAIL] = _(UNCLASSIFIED_ROW_DETAIL) + + def _get_rdi_name(self, rdi): + result = rdi.name_get()[0][1] + if self._multi_company and rdi.company_id: + result = u"{} [{}]".format(result, rdi.company_id.name) return result - def get_account_name(self, account_id): - if account_id not in self._account_names: - self._load_account_names() - return self._account_names[account_id] + def get_rdi_name(self, rdi_id): + if rdi_id not in self._rdi_names: + self._load_rdi_names() + return self._rdi_names[rdi_id] def as_dict(self): header = [{"cols": []}, {"cols": []}] diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 5e7f62f70..b1c9c5c0d 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -91,11 +91,11 @@ class MisReportKpi(models.Model): copy=True, string="Expressions", ) - auto_expand_accounts = fields.Boolean(string="Display details by account") + + # TODO : this fields should be renamed to auto_expand_details or something like this + auto_expand_accounts = fields.Boolean(string="Display details") auto_expand_accounts_style_id = fields.Many2one( - string="Style for account detail rows", - comodel_name="mis.report.style", - required=False, + string="Style for details rows", comodel_name="mis.report.style", required=False ) style_id = fields.Many2one( string="Style", comodel_name="mis.report.style", required=False @@ -465,6 +465,20 @@ def _default_move_lines_source(self): ) account_model = fields.Char(compute="_compute_account_model") + auto_expand_col_name = fields.Selection( + [ + ("account_id", _("Accounts")), + ("analytic_account_id", _("Analytic Accounts")), + ("partner_id", _("Parner")), + ], + required=True, + string="Auto Expand Details", + default="account_id", + help="Allow to drilldown kpis by the specified field, " + "it need to be activated in each kpi. You should use " + "style configuration to hide null.", + ) + @api.depends("kpi_ids", "subreport_ids") def _compute_all_kpi_ids(self): for rec in self: @@ -748,21 +762,23 @@ def _declare_and_compute_col( # noqa: C901 (TODO simplify this fnction) ): continue - for ( - account_id, - vals, - drilldown_args, - _name_error, - ) in expression_evaluator.eval_expressions_by_account( - expressions, locals_dict - ): + rdis = expression_evaluator.eval_expressions_by_row_detail( + expressions, locals_dict # , self.auto_expand_col_name + ) + for (rdi, vals, drilldown_args, _name_error) in rdis: for drilldown_arg in drilldown_args: if not drilldown_arg: continue drilldown_arg["period_id"] = col_key drilldown_arg["kpi_id"] = kpi.id + drilldown_arg[ + "auto_expand_col_name" + ] = self.auto_expand_col_name + drilldown_arg["auto_expand_id"] = rdi + if not self._should_display_auto_expand(kpi, rdi, vals): + continue kpi_matrix.set_values_detail_account( - kpi, col_key, account_id, vals, drilldown_args + kpi, col_key, rdi, vals, drilldown_args ) if len(recompute_queue) == 0: @@ -880,7 +896,7 @@ def _declare_and_compute_period( ) # use AEP to do the accounting queries - expression_evaluator.aep_do_queries() + expression_evaluator.aep_do_queries(self.auto_expand_col_name) self._declare_and_compute_col( expression_evaluator, @@ -1001,3 +1017,6 @@ def _evaluate( no_auto_expand_accounts=True, ) return locals_dict + + def _should_display_auto_expand(self, kpi, rdi, vals): + return True diff --git a/mis_builder/models/mis_report_instance.py b/mis_builder/models/mis_report_instance.py index 0c5712240..e19cf0274 100644 --- a/mis_builder/models/mis_report_instance.py +++ b/mis_builder/models/mis_report_instance.py @@ -887,6 +887,10 @@ def drilldown(self, arg): account_id, ) domain.extend(period._get_additional_move_line_filter()) + if arg.get("auto_expand_id") and arg.get("auto_expand_col_name"): + domain.extend( + [(arg.get("auto_expand_col_name"), "=", arg.get("auto_expand_id"))] + ) return { "name": self._get_drilldown_action_name(arg), "domain": domain, diff --git a/mis_builder/tests/test_aep.py b/mis_builder/tests/test_aep.py index e0eb4cefb..526941968 100644 --- a/mis_builder/tests/test_aep.py +++ b/mis_builder/tests/test_aep.py @@ -9,7 +9,11 @@ from odoo.tools.safe_eval import safe_eval from ..models.accounting_none import AccountingNone -from ..models.aep import AccountingExpressionProcessor as AEP, _is_domain +from ..models.aep import ( + UNCLASSIFIED_ROW_DETAIL, + AccountingExpressionProcessor as AEP, + _is_domain, +) class TestAEP(common.TransactionCase): @@ -17,6 +21,7 @@ def setUp(self): super().setUp() self.res_company = self.env["res.company"] self.account_model = self.env["account.account"] + self.partner_model = self.env["res.partner"] self.move_model = self.env["account.move"] self.journal_model = self.env["account.journal"] self.curr_year = datetime.date.today().year @@ -53,6 +58,13 @@ def setUp(self): "type": "sale", } ) + # create partner + self.partner_a = self.partner_model.create( + {"company_id": self.company.id, "name": "Partner A"} + ) + self.partner_b = self.partner_model.create( + {"company_id": self.company.id, "name": "Partner B"} + ) # create move in December last year self._create_move( date=datetime.date(self.prev_year, 12, 1), @@ -73,6 +85,8 @@ def setUp(self): amount=500, debit_acc=self.account_ar, credit_acc=self.account_in, + debit_partner=self.partner_a, + credit_partner=self.partner_b, ) # create the AEP, and prepare the expressions we'll need self.aep = AEP(self.company) @@ -109,17 +123,40 @@ def setUp(self): self.aep.parse_expr("bal_700IN") # deprecated self.aep.parse_expr("bals[700IN]") # deprecated - def _create_move(self, date, amount, debit_acc, credit_acc, post=True): + def _create_move( + self, + date, + amount, + debit_acc, + credit_acc, + debit_partner=None, + credit_partner=None, + post=True, + ): move = self.move_model.create( { "journal_id": self.journal.id, "date": fields.Date.to_string(date), "line_ids": [ - (0, 0, {"name": "/", "debit": amount, "account_id": debit_acc.id}), ( 0, 0, - {"name": "/", "credit": amount, "account_id": credit_acc.id}, + { + "name": "/", + "debit": amount, + "account_id": debit_acc.id, + "partner_id": debit_partner.id if debit_partner else None, + }, + ), + ( + 0, + 0, + { + "name": "/", + "credit": amount, + "account_id": credit_acc.id, + "partner_id": credit_partner.id if credit_partner else None, + }, ), ], } @@ -128,11 +165,18 @@ def _create_move(self, date, amount, debit_acc, credit_acc, post=True): move._post() return move - def _do_queries(self, date_from, date_to): - self.aep.do_queries( - date_from=fields.Date.to_string(date_from), - date_to=fields.Date.to_string(date_to), - ) + def _do_queries(self, date_from, date_to, auto_expand_col_name=None): + if auto_expand_col_name: + self.aep.do_queries( + date_from=fields.Date.to_string(date_from), + date_to=fields.Date.to_string(date_to), + auto_expand_col_name=auto_expand_col_name, + ) + else: + self.aep.do_queries( + date_from=fields.Date.to_string(date_from), + date_to=fields.Date.to_string(date_to), + ) def _eval(self, expr): eval_dict = {"AccountingNone": AccountingNone} @@ -141,8 +185,17 @@ def _eval(self, expr): def _eval_by_account_id(self, expr): res = {} eval_dict = {"AccountingNone": AccountingNone} + for account_id, replaced_exprs in self.aep.replace_exprs_by_account_id([expr]): res[account_id] = safe_eval(replaced_exprs[0], eval_dict) + + return res + + def _eval_by_rdi(self, expr): + res = {} + eval_dict = {"AccountingNone": AccountingNone} + for rdi, replaced_exprs in self.aep.replace_exprs_by_row_detail([expr]): + res[rdi] = safe_eval(replaced_exprs[0], eval_dict) return res def test_sanity_check(self): @@ -240,6 +293,43 @@ def test_aep_basic(self): # TODO allocate profits, and then... + def test_aep_with_row_details(self): + self.aep.done_parsing() + self._do_queries( + datetime.date(self.curr_year, 3, 1), + datetime.date(self.curr_year, 3, 31), + auto_expand_col_name="partner_id", + ) + initial = self._eval_by_rdi("crdi[]") + self.assertEqual( + initial, + { + self.partner_a.id: AccountingNone, + self.partner_b.id: AccountingNone, + UNCLASSIFIED_ROW_DETAIL: 300.0, + }, + ) + + variation = self._eval_by_rdi("balp[]") + self.assertEqual( + variation, + { + self.partner_a.id: 500, + self.partner_b.id: -500, + UNCLASSIFIED_ROW_DETAIL: AccountingNone, + }, + ) + + initial = self._eval_by_rdi("bale[400AR]") + self.assertEqual( + initial, + { + self.partner_a.id: 500, + self.partner_b.id: AccountingNone, + UNCLASSIFIED_ROW_DETAIL: 400.0, + }, + ) + def test_aep_by_account(self): self.aep.done_parsing() self._do_queries( diff --git a/mis_builder/tests/test_mis_report_instance.py b/mis_builder/tests/test_mis_report_instance.py index baa1c1103..d9086586d 100644 --- a/mis_builder/tests/test_mis_report_instance.py +++ b/mis_builder/tests/test_mis_report_instance.py @@ -376,22 +376,22 @@ def test_multi_company_compute(self): "company_ids": [(6, 0, self.report_instance.company_id.ids)], } ) + self.report_instance.report_id.write({"auto_expand_col_name": "account_id"}) self.report_instance.report_id.kpi_ids.write({"auto_expand_accounts": True}) matrix = self.report_instance._compute_matrix() for row in matrix.iter_rows(): - if row.account_id: - account = self.env["account.account"].browse(row.account_id) + if row.row_detail_identifier: + account = self.env["account.account"].browse(row.row_detail_identifier) self.assertEqual( row.label, - "%s %s [%s]" - % (account.code, account.name, account.company_id.name), + "%s [%s]" % (account.name_get()[0][1], account.company_id.name), ) self.report_instance.write({"multi_company": False}) matrix = self.report_instance._compute_matrix() for row in matrix.iter_rows(): - if row.account_id: - account = self.env["account.account"].browse(row.account_id) - self.assertEqual(row.label, "{} {}".format(account.code, account.name)) + if row.row_detail_identifier: + account = self.env["account.account"].browse(row.row_detail_identifier) + self.assertEqual(row.label, account.name_get()[0][1]) def test_evaluate(self): company = self.env.ref("base.main_company") diff --git a/mis_builder/views/mis_report.xml b/mis_builder/views/mis_report.xml index 0b67e914e..0b49567e3 100644 --- a/mis_builder/views/mis_report.xml +++ b/mis_builder/views/mis_report.xml @@ -20,6 +20,7 @@ +