Skip to content

Commit 4e087c1

Browse files
committed
[IMP] mis_builder: Generalized auto detail expansion mechanism
This commit introduce a mechanism to expand automatically a kpi by any field.
1 parent 8c836f4 commit 4e087c1

8 files changed

Lines changed: 332 additions & 103 deletions

File tree

mis_builder/models/aep.py

Lines changed: 124 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
_DOMAIN_START_RE = re.compile(r"\(|(['\"])[!&|]\1")
1616

17+
UNCLASSIFIED_ROW_DETAIL = "other"
18+
1719

1820
def _is_domain(s):
1921
"""Test if a string looks like an Odoo domain"""
@@ -300,16 +302,14 @@ def do_queries(
300302
date_to,
301303
additional_move_line_filter=None,
302304
aml_model=None,
305+
auto_expand_col_name=None,
303306
):
304307
"""Query sums of debit and credit for all accounts and domains
305308
used in expressions.
306309
307310
This method must be executed after done_parsing().
308311
"""
309-
if not aml_model:
310-
aml_model = self.env["account.move.line"]
311-
else:
312-
aml_model = self.env[aml_model]
312+
aml_model = self.env[aml_model or "account.move.line"]
313313
aml_model = aml_model.with_context(active_test=False)
314314
company_rates = self._get_company_rates(date_to)
315315
# {(domain, mode): {account_id: (debit, credit)}}
@@ -330,13 +330,16 @@ def do_queries(
330330
domain.append(("account_id", "in", self._map_account_ids[key]))
331331
if additional_move_line_filter:
332332
domain.extend(additional_move_line_filter)
333+
334+
get_fields = ["debit", "credit", "account_id", "company_id"]
335+
group_by_fields = ["account_id", "company_id"]
336+
if auto_expand_col_name:
337+
get_fields = [auto_expand_col_name] + get_fields
338+
group_by_fields = [auto_expand_col_name] + group_by_fields
339+
333340
# fetch sum of debit/credit, grouped by account_id
334-
accs = aml_model.read_group(
335-
domain,
336-
["debit", "credit", "account_id", "company_id"],
337-
["account_id", "company_id"],
338-
lazy=False,
339-
)
341+
accs = aml_model.read_group(domain, get_fields, group_by_fields, lazy=False)
342+
340343
for acc in accs:
341344
rate, dp = company_rates[acc["company_id"][0]]
342345
debit = acc["debit"] or 0.0
@@ -346,19 +349,45 @@ def do_queries(
346349
):
347350
# in initial mode, ignore accounts with 0 balance
348351
continue
349-
self._data[key][acc["account_id"][0]] = (debit * rate, credit * rate)
352+
if (
353+
auto_expand_col_name
354+
and auto_expand_col_name in acc
355+
and acc[auto_expand_col_name]
356+
):
357+
rdi_id = acc[auto_expand_col_name][0]
358+
else:
359+
rdi_id = UNCLASSIFIED_ROW_DETAIL
360+
if not self._data[key].get(rdi_id, False):
361+
self._data[key][rdi_id] = defaultdict(dict)
362+
self._data[key][rdi_id][acc["account_id"][0]] = (
363+
debit * rate,
364+
credit * rate,
365+
)
350366
# compute ending balances by summing initial and variation
351367
for key in ends:
352368
domain, mode = key
353369
initial_data = self._data[(domain, self.MODE_INITIAL)]
354370
variation_data = self._data[(domain, self.MODE_VARIATION)]
355-
account_ids = set(initial_data.keys()) | set(variation_data.keys())
356-
for account_id in account_ids:
357-
di, ci = initial_data.get(account_id, (AccountingNone, AccountingNone))
358-
dv, cv = variation_data.get(
359-
account_id, (AccountingNone, AccountingNone)
371+
rdis = set(initial_data.keys()) | set(variation_data.keys())
372+
for rdi in rdis:
373+
if not initial_data.get(rdi, False):
374+
initial_data[rdi] = defaultdict(dict)
375+
if not variation_data.get(rdi, False):
376+
variation_data[rdi] = defaultdict(dict)
377+
if not self._data[key].get(rdi, False):
378+
self._data[key][rdi] = defaultdict(dict)
379+
380+
account_ids = set(initial_data[rdi].keys()) | set(
381+
variation_data[rdi].keys()
360382
)
361-
self._data[key][account_id] = (di + dv, ci + cv)
383+
for account_id in account_ids:
384+
di, ci = initial_data[rdi].get(
385+
account_id, (AccountingNone, AccountingNone)
386+
)
387+
dv, cv = variation_data[rdi].get(
388+
account_id, (AccountingNone, AccountingNone)
389+
)
390+
self._data[key][rdi][account_id] = (di + dv, ci + cv)
362391

363392
def replace_expr(self, expr):
364393
"""Replace accounting variables in an expression by their amount.
@@ -371,23 +400,25 @@ def replace_expr(self, expr):
371400
def f(mo):
372401
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
373402
key = (ml_domain, mode)
374-
account_ids_data = self._data[key]
403+
rdi_ids_data = self._data[key]
375404
v = AccountingNone
376405
account_ids = self._account_ids_by_acc_domain[acc_domain]
377-
for account_id in account_ids:
378-
debit, credit = account_ids_data.get(
379-
account_id, (AccountingNone, AccountingNone)
380-
)
381-
if field == "bal":
382-
v += debit - credit
383-
elif field == "pbal" and debit >= credit:
384-
v += debit - credit
385-
elif field == "nbal" and debit < credit:
386-
v += debit - credit
387-
elif field == "deb":
388-
v += debit
389-
elif field == "crd":
390-
v += credit
406+
for rdi in rdi_ids_data:
407+
account_ids_data = self._data[key][rdi]
408+
for account_id in account_ids:
409+
debit, credit = account_ids_data.get(
410+
account_id, (AccountingNone, AccountingNone)
411+
)
412+
if field == "bal":
413+
v += debit - credit
414+
elif field == "pbal" and debit >= credit:
415+
v += debit - credit
416+
elif field == "nbal" and debit < credit:
417+
v += debit - credit
418+
elif field == "deb":
419+
v += debit
420+
elif field == "crd":
421+
v += credit
391422
# in initial balance mode, assume 0 is None
392423
# as it does not make sense to distinguish 0 from "no data"
393424
if (
@@ -401,11 +432,11 @@ def f(mo):
401432
return self._ACC_RE.sub(f, expr)
402433

403434
def replace_exprs_by_account_id(self, exprs):
404-
"""Replace accounting variables in a list of expression
405-
by their amount, iterating by accounts involved in the expression.
435+
"""This method is depreciated and replaced by replace_exprs_by_row_detail.
406436
437+
Replace accounting variables in a list of expression
438+
by their amount, iterating by accounts involved in the expression.
407439
yields account_id, replaced_expr
408-
409440
This method must be executed after do_queries().
410441
"""
411442

@@ -417,7 +448,7 @@ def f(mo):
417448
if account_id not in self._account_ids_by_acc_domain[acc_domain]:
418449
return "(AccountingNone)"
419450
# here we know account_id is involved in acc_domain
420-
account_ids_data = self._data[key]
451+
account_ids_data = self._data[key][UNCLASSIFIED_ROW_DETAIL]
421452
debit, credit = account_ids_data.get(
422453
account_id, (AccountingNone, AccountingNone)
423454
)
@@ -452,14 +483,66 @@ def f(mo):
452483
for mo in self._ACC_RE.finditer(expr):
453484
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
454485
key = (ml_domain, mode)
455-
account_ids_data = self._data[key]
486+
account_ids_data = self._data[key][UNCLASSIFIED_ROW_DETAIL]
456487
for account_id in self._account_ids_by_acc_domain[acc_domain]:
457488
if account_id in account_ids_data:
458489
account_ids.add(account_id)
459490

460491
for account_id in account_ids:
461492
yield account_id, [self._ACC_RE.sub(f, expr) for expr in exprs]
462493

494+
def replace_exprs_by_row_detail(self, exprs):
495+
"""Replace accounting variables in a list of expression
496+
by their amount, iterating by accounts involved in the expression.
497+
498+
yields account_id, replaced_expr
499+
500+
This method must be executed after do_queries().
501+
"""
502+
503+
def f(mo):
504+
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
505+
key = (ml_domain, mode)
506+
v = AccountingNone
507+
account_ids_data = self._data[key][rdi_id]
508+
account_ids = self._account_ids_by_acc_domain[acc_domain]
509+
510+
for account_id in account_ids:
511+
debit, credit = account_ids_data.get(
512+
account_id, (AccountingNone, AccountingNone)
513+
)
514+
if field == "bal":
515+
v += debit - credit
516+
elif field == "pbal" and debit >= credit:
517+
v += debit - credit
518+
elif field == "nbal" and debit < credit:
519+
v += debit - credit
520+
elif field == "deb":
521+
v += debit
522+
elif field == "crd":
523+
v += credit
524+
# in initial balance mode, assume 0 is None
525+
# as it does not make sense to distinguish 0 from "no data"
526+
if (
527+
v is not AccountingNone
528+
and mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED)
529+
and float_is_zero(v, precision_digits=self.dp)
530+
):
531+
v = AccountingNone
532+
return "(" + repr(v) + ")"
533+
534+
rdi_ids = set()
535+
for expr in exprs:
536+
for mo in self._ACC_RE.finditer(expr):
537+
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
538+
key = (ml_domain, mode)
539+
rdis_data = self._data[key]
540+
for rdi_id in rdis_data.keys():
541+
rdi_ids.add(rdi_id)
542+
543+
for rdi_id in rdi_ids:
544+
yield rdi_id, [self._ACC_RE.sub(f, expr) for expr in exprs]
545+
463546
@classmethod
464547
def _get_balances(cls, mode, companies, date_from, date_to):
465548
expr = "deb{mode}[], crd{mode}[]".format(mode=mode)
@@ -470,7 +553,10 @@ def _get_balances(cls, mode, companies, date_from, date_to):
470553
aep.parse_expr(expr)
471554
aep.done_parsing()
472555
aep.do_queries(date_from, date_to)
473-
return aep._data[((), mode)]
556+
557+
return aep._data[((), mode)].get(UNCLASSIFIED_ROW_DETAIL, {})
558+
# to keep compatibility, we give the UNCLASSIFIED_ROW_DETAIL
559+
# (expecting that auto_expand_col_names=None was given to do_queries )
474560

475561
@classmethod
476562
def get_balances_initial(cls, companies, date):

mis_builder/models/expression_evaluator.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ def __init__(
2020
self.aml_model = aml_model
2121
self._aep_queries_done = False
2222

23-
def aep_do_queries(self):
23+
def aep_do_queries(self, auto_expand_col_name=None):
2424
if self.aep and not self._aep_queries_done:
2525
self.aep.do_queries(
2626
self.date_from,
2727
self.date_to,
2828
self.additional_move_line_filter,
2929
self.aml_model,
30+
auto_expand_col_name,
3031
)
3132
self._aep_queries_done = True
3233

@@ -50,6 +51,7 @@ def eval_expressions(self, expressions, locals_dict):
5051
drilldown_args.append(None)
5152
return vals, drilldown_args, name_error
5253

54+
# we keep it for backward compatibility
5355
def eval_expressions_by_account(self, expressions, locals_dict):
5456
if not self.aep:
5557
return
@@ -66,3 +68,20 @@ def eval_expressions_by_account(self, expressions, locals_dict):
6668
else:
6769
drilldown_args.append(None)
6870
yield account_id, vals, drilldown_args, name_error
71+
72+
def eval_expressions_by_row_detail(self, expressions, locals_dict):
73+
if not self.aep:
74+
return
75+
exprs = [e and e.name or "AccountingNone" for e in expressions]
76+
for rdi_id, replaced_exprs in self.aep.replace_exprs_by_row_detail(exprs):
77+
vals = []
78+
drilldown_args = []
79+
name_error = False
80+
for expr, replaced_expr in zip(exprs, replaced_exprs):
81+
val = mis_safe_eval(replaced_expr, locals_dict)
82+
vals.append(val)
83+
if replaced_expr != expr:
84+
drilldown_args.append({"expr": expr, "row_detail": rdi_id})
85+
else:
86+
drilldown_args.append(None)
87+
yield rdi_id, vals, drilldown_args, name_error

0 commit comments

Comments
 (0)