Skip to content

Commit 91546b5

Browse files
committed
Generalized auto detail expansion mechanism
This commit introduce a mechanism to expand automatically a kpi by any field.
1 parent 034f9d6 commit 91546b5

8 files changed

Lines changed: 328 additions & 104 deletions

File tree

mis_builder/models/aep.py

Lines changed: 126 additions & 41 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"""
@@ -299,25 +301,22 @@ def do_queries(
299301
date_from,
300302
date_to,
301303
additional_move_line_filter=None,
302-
aml_model=None,
304+
aml_model="account.move.line",
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]
313-
aml_model = aml_model.with_context(active_test=False)
312+
aml_model = self.env[aml_model].with_context(active_test=False)
314313
company_rates = self._get_company_rates(date_to)
315314
# {(domain, mode): {account_id: (debit, credit)}}
316315
self._data = defaultdict(dict)
317316
domain_by_mode = {}
318317
ends = []
319318
for key in self._map_account_ids:
320-
domain, mode = key
319+
(domain, mode) = key
321320
if mode == self.MODE_END and self.smart_end:
322321
# postpone computation of ending balance
323322
ends.append((domain, mode))
@@ -330,13 +329,16 @@ def do_queries(
330329
domain.append(("account_id", "in", self._map_account_ids[key]))
331330
if additional_move_line_filter:
332331
domain.extend(additional_move_line_filter)
332+
333+
get_fields = ["debit", "credit", "account_id", "company_id"]
334+
group_by_fields = ["account_id", "company_id"]
335+
if auto_expand_col_name:
336+
get_fields = [auto_expand_col_name] + get_fields
337+
group_by_fields = [auto_expand_col_name] + group_by_fields
338+
333339
# 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-
)
340+
accs = aml_model.read_group(domain, get_fields, group_by_fields, lazy=False)
341+
340342
for acc in accs:
341343
rate, dp = company_rates[acc["company_id"][0]]
342344
debit = acc["debit"] or 0.0
@@ -346,19 +348,45 @@ def do_queries(
346348
):
347349
# in initial mode, ignore accounts with 0 balance
348350
continue
349-
self._data[key][acc["account_id"][0]] = (debit * rate, credit * rate)
351+
if (
352+
auto_expand_col_name
353+
and auto_expand_col_name in acc
354+
and acc[auto_expand_col_name]
355+
):
356+
rdi_id = acc[auto_expand_col_name][0]
357+
else:
358+
rdi_id = UNCLASSIFIED_ROW_DETAIL
359+
if not self._data[key].get(rdi_id, False):
360+
self._data[key][rdi_id] = defaultdict(dict)
361+
self._data[key][rdi_id][acc["account_id"][0]] = (
362+
debit * rate,
363+
credit * rate,
364+
)
350365
# compute ending balances by summing initial and variation
351366
for key in ends:
352367
domain, mode = key
353368
initial_data = self._data[(domain, self.MODE_INITIAL)]
354369
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)
370+
rdis = set(initial_data.keys()) | set(variation_data.keys())
371+
for rdi in rdis:
372+
if not initial_data.get(rdi, False):
373+
initial_data[rdi] = defaultdict(dict)
374+
if not variation_data.get(rdi, False):
375+
variation_data[rdi] = defaultdict(dict)
376+
if not self._data[key].get(rdi, False):
377+
self._data[key][rdi] = defaultdict(dict)
378+
379+
account_ids = set(initial_data[rdi].keys()) | set(
380+
variation_data[rdi].keys()
360381
)
361-
self._data[key][account_id] = (di + dv, ci + cv)
382+
for account_id in account_ids:
383+
di, ci = initial_data[rdi].get(
384+
account_id, (AccountingNone, AccountingNone)
385+
)
386+
dv, cv = variation_data[rdi].get(
387+
account_id, (AccountingNone, AccountingNone)
388+
)
389+
self._data[key][rdi][account_id] = (di + dv, ci + cv)
362390

363391
def replace_expr(self, expr):
364392
"""Replace accounting variables in an expression by their amount.
@@ -371,23 +399,25 @@ def replace_expr(self, expr):
371399
def f(mo):
372400
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
373401
key = (ml_domain, mode)
374-
account_ids_data = self._data[key]
402+
rdi_ids_data = self._data[key]
375403
v = AccountingNone
376404
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
405+
for rdi in rdi_ids_data:
406+
account_ids_data = self._data[key][rdi]
407+
for account_id in account_ids:
408+
debit, credit = account_ids_data.get(
409+
account_id, (AccountingNone, AccountingNone)
410+
)
411+
if field == "bal":
412+
v += debit - credit
413+
elif field == "pbal" and debit >= credit:
414+
v += debit - credit
415+
elif field == "nbal" and debit < credit:
416+
v += debit - credit
417+
elif field == "deb":
418+
v += debit
419+
elif field == "crd":
420+
v += credit
391421
# in initial balance mode, assume 0 is None
392422
# as it does not make sense to distinguish 0 from "no data"
393423
if (
@@ -401,11 +431,11 @@ def f(mo):
401431
return self._ACC_RE.sub(f, expr)
402432

403433
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.
434+
"""This method is depreciated and replaced by replace_exprs_by_row_detail.
406435
436+
Replace accounting variables in a list of expression
437+
by their amount, iterating by accounts involved in the expression.
407438
yields account_id, replaced_expr
408-
409439
This method must be executed after do_queries().
410440
"""
411441

@@ -417,7 +447,7 @@ def f(mo):
417447
if account_id not in self._account_ids_by_acc_domain[acc_domain]:
418448
return "(AccountingNone)"
419449
# here we know account_id is involved in acc_domain
420-
account_ids_data = self._data[key]
450+
account_ids_data = self._data[key][UNCLASSIFIED_ROW_DETAIL]
421451
debit, credit = account_ids_data.get(
422452
account_id, (AccountingNone, AccountingNone)
423453
)
@@ -452,14 +482,66 @@ def f(mo):
452482
for mo in self._ACC_RE.finditer(expr):
453483
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
454484
key = (ml_domain, mode)
455-
account_ids_data = self._data[key]
485+
account_ids_data = self._data[key][UNCLASSIFIED_ROW_DETAIL]
456486
for account_id in self._account_ids_by_acc_domain[acc_domain]:
457487
if account_id in account_ids_data:
458488
account_ids.add(account_id)
459489

460490
for account_id in account_ids:
461491
yield account_id, [self._ACC_RE.sub(f, expr) for expr in exprs]
462492

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

475560
@classmethod
476561
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)