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..aeaca3d2c
--- /dev/null
+++ b/mis_builder/models/account_account.py
@@ -0,0 +1,10 @@
+# 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
+
+class AccountAccount(models.Model):
+ _name = 'account.account'
+ _inherit = ['account.account']
\ No newline at end of file
diff --git a/mis_builder/models/account_analytic_account.py b/mis_builder/models/account_analytic_account.py
new file mode 100644
index 000000000..ad9825586
--- /dev/null
+++ b/mis_builder/models/account_analytic_account.py
@@ -0,0 +1,10 @@
+# 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
+
+class AccountAnalyticAccount(models.Model):
+ _name = 'account.analytic.account'
+ _inherit = ['account.analytic.account']
diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py
index 5cc79ff2c..40de3cfd4 100644
--- a/mis_builder/models/aep.py
+++ b/mis_builder/models/aep.py
@@ -11,7 +11,7 @@
from odoo.tools.safe_eval import datetime, dateutil, safe_eval, time
from .accounting_none import AccountingNone
-
+import pprint
try:
import itertools.izip as zip
except ImportError:
@@ -306,6 +306,8 @@ def do_queries(
date_to,
additional_move_line_filter=None,
aml_model=None,
+ auto_expand_col_name = None,
+ rdi_transformer = lambda i: (i[0], str(i[1])) if i else ('other', _('Other'))
):
"""Query sums of debit and credit for all accounts and domains
used in expressions.
@@ -323,7 +325,7 @@ def do_queries(
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))
@@ -336,13 +338,21 @@ 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"],
+ 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
@@ -352,19 +362,33 @@ def do_queries(
):
# in initial mode, ignore accounts with 0 balance
continue
- self._data[key][acc["account_id"][0]] = (debit * rate, credit * rate)
+ rdi_id = rdi_transformer(acc[auto_expand_col_name])
+ 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)
- )
- self._data[key][account_id] = (di + dv, ci + cv)
+ 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)
+ pprint.pprint(rdis)
+ pprint.pprint(rdi)
+ pprint.pprint(initial_data)
+ pprint.pprint(variation_data)
+
+ account_ids = set(initial_data[rdi].keys()) | set(variation_data[rdi].keys())
+ 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.
@@ -377,23 +401,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 (
@@ -424,25 +450,21 @@ def f(mo):
return "(AccountingNone)"
# here we know account_id is involved in acc_domain
account_ids_data = self._data[key]
- debit, credit = account_ids_data.get(
- account_id, (AccountingNone, AccountingNone)
- )
- if field == "bal":
- v = debit - credit
- elif field == "pbal":
- if debit >= credit:
- v = debit - credit
- else:
- v = AccountingNone
- elif field == "nbal":
- if debit < credit:
- v = debit - credit
- else:
- v = AccountingNone
- elif field == "deb":
- v = debit
- elif field == "crd":
- v = credit
+ for rdi in rdi_ids_data:
+ account_ids_data = self._data[key][rdi]
+ 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 (
@@ -466,6 +488,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)
diff --git a/mis_builder/models/expression_evaluator.py b/mis_builder/models/expression_evaluator.py
index 94ff403cb..20b1c0109 100644
--- a/mis_builder/models/expression_evaluator.py
+++ b/mis_builder/models/expression_evaluator.py
@@ -25,13 +25,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
@@ -55,6 +56,7 @@ def eval_expressions(self, expressions, locals_dict):
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
@@ -71,3 +73,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
\ No newline at end of file
diff --git a/mis_builder/models/kpimatrix.py b/mis_builder/models/kpimatrix.py
index 8b89b28fe..e63b30667 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,26 @@
_logger = logging.getLogger(__name__)
+class RowDetailIdentifier():
+ def __init__(self, id, label):
+ self.id = id
+ self.label = label
+
+ 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.label
+
+# def __hash__(self):
+# pass
+
+ def __lt__(self, other): # TODO assess utility
+ return self.get_label() < other.get_label()
+
class KpiMatrixRow(object):
# TODO: ultimately, the kpi matrix will become ignorant of KPI's and
@@ -28,13 +48,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]
)
@@ -45,17 +65,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.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 +237,26 @@ 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
+ 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()]
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 +432,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 +449,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 9e49c9a3e..63905ddf9 100644
--- a/mis_builder/models/mis_report.py
+++ b/mis_builder/models/mis_report.py
@@ -25,7 +25,7 @@
from .aep import AccountingExpressionProcessor as AEP
from .aggregate import _avg, _max, _min, _sum
from .expression_evaluator import ExpressionEvaluator
-from .kpimatrix import KpiMatrix
+from .kpimatrix import KpiMatrix, RowDetailIdentifier
from .mis_kpi_data import ACC_AVG, ACC_NONE, ACC_SUM
from .mis_report_style import CMP_DIFF, CMP_NONE, CMP_PCT, TYPE_NUM, TYPE_PCT, TYPE_STR
from .mis_safe_eval import DataError
@@ -92,12 +92,14 @@ class MisReportKpi(models.Model):
copy=True,
string="Expressions",
)
- auto_expand_accounts = fields.Boolean(string="Display details by account")
+
+ auto_expand_accounts = fields.Boolean(string="Display details")
auto_expand_accounts_style_id = fields.Many2one(
- string="Style for account detail rows",
+ 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
)
@@ -438,6 +440,7 @@ def _default_move_lines_source(self):
return self.env["ir.model"].search([("model", "=", "account.move.line")])
name = fields.Char(required=True, string="Name", translate=True)
+
description = fields.Char(required=False, string="Description", translate=True)
style_id = fields.Many2one(string="Style", comodel_name="mis.report.style")
query_ids = fields.One2many(
@@ -476,6 +479,20 @@ def _default_move_lines_source(self):
compute="_compute_account_model", string="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 can hide null "
+ "lines in the style configuration."
+ )
+
@api.depends("kpi_ids", "subreport_ids")
def _compute_all_kpi_ids(self):
for rec in self:
@@ -563,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)
aep.done_parsing()
return aep
@@ -663,7 +679,8 @@ def _declare_and_compute_col( # noqa: C901 (TODO simplify this fnction)
col_description,
subkpis_filter,
locals_dict,
- no_auto_expand_accounts=False,
+ no_auto_expand_accounts
+ =False,
):
"""This is the main computation loop.
@@ -759,21 +776,24 @@ def _declare_and_compute_col( # noqa: C901 (TODO simplify this fnction)
):
continue
+ rdis = expression_evaluator.eval_expressions_by_row_detail(
+ expressions, locals_dict #, self.auto_expand_col_name
+ )
for (
- account_id,
+ rdi,
vals,
drilldown_args,
_name_error,
- ) in expression_evaluator.eval_expressions_by_account(
- expressions, locals_dict
- ):
+ ) 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
- kpi_matrix.set_values_detail_account(
- kpi, col_key, account_id, vals, drilldown_args
+ if not self._should_display_auto_expand(kpi, rdi, vals):
+ continue
+ kpi_matrix.set_values_detail(
+ kpi, col_key, RowDetailIdentifier(rdi[0], rdi[1]), vals, drilldown_args,
)
if len(recompute_queue) == 0:
@@ -891,7 +911,7 @@ def _declare_and_compute_period(
)
# use AEP to do the accounting queries
- expression_evaluator.aep_do_queries()
+ rdis = expression_evaluator.aep_do_queries(self.auto_expand_col_name)
self._declare_and_compute_col(
expression_evaluator,
@@ -1012,3 +1032,9 @@ def _evaluate(
no_auto_expand_accounts=True,
)
return locals_dict
+
+ def _should_display_auto_expand(self, kpi, rdi, vals):
+ if self.auto_expand_col_name == "account_id"
+ and self.rdi.id not in kpi
+
+ return True
diff --git a/mis_builder/views/mis_report.xml b/mis_builder/views/mis_report.xml
index 466f006ed..602e1c199 100644
--- a/mis_builder/views/mis_report.xml
+++ b/mis_builder/views/mis_report.xml
@@ -20,6 +20,7 @@
+