Merge pull request #179 from acsone/8.0-mis-compute-refactor-2-sbi
[IMP] mis_builder: refactor the compute methodpull/174/head
commit
c1dbd0af6f
|
@ -292,6 +292,207 @@ class MisReport(models.Model):
|
||||||
|
|
||||||
# TODO: kpi name cannot be start with query name
|
# TODO: kpi name cannot be start with query name
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _prepare_aep(self, root_account):
|
||||||
|
self.ensure_one()
|
||||||
|
aep = AEP(self.env)
|
||||||
|
for kpi in self.kpi_ids:
|
||||||
|
aep.parse_expr(kpi.expression)
|
||||||
|
aep.done_parsing(root_account)
|
||||||
|
return aep
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _fetch_queries(self, date_from, date_to,
|
||||||
|
get_additional_query_filter=None):
|
||||||
|
self.ensure_one()
|
||||||
|
res = {}
|
||||||
|
for query in self.query_ids:
|
||||||
|
model = self.env[query.model_id.model]
|
||||||
|
eval_context = {
|
||||||
|
'env': self.env,
|
||||||
|
'time': time,
|
||||||
|
'datetime': datetime,
|
||||||
|
'dateutil': dateutil,
|
||||||
|
# deprecated
|
||||||
|
'uid': self.env.uid,
|
||||||
|
'context': self.env.context,
|
||||||
|
}
|
||||||
|
domain = query.domain and \
|
||||||
|
safe_eval(query.domain, eval_context) or []
|
||||||
|
if get_additional_query_filter:
|
||||||
|
domain.extend(get_additional_query_filter(query))
|
||||||
|
if query.date_field.ttype == 'date':
|
||||||
|
domain.extend([(query.date_field.name, '>=', date_from),
|
||||||
|
(query.date_field.name, '<=', date_to)])
|
||||||
|
else:
|
||||||
|
datetime_from = _utc_midnight(
|
||||||
|
date_from, self._context.get('tz', 'UTC'))
|
||||||
|
datetime_to = _utc_midnight(
|
||||||
|
date_to, self._context.get('tz', 'UTC'), add_day=1)
|
||||||
|
domain.extend([(query.date_field.name, '>=', datetime_from),
|
||||||
|
(query.date_field.name, '<', datetime_to)])
|
||||||
|
field_names = [f.name for f in query.field_ids]
|
||||||
|
if not query.aggregate:
|
||||||
|
data = model.search_read(domain, field_names)
|
||||||
|
res[query.name] = [AutoStruct(**d) for d in data]
|
||||||
|
elif query.aggregate == 'sum':
|
||||||
|
data = model.read_group(
|
||||||
|
domain, field_names, [])
|
||||||
|
s = AutoStruct(count=data[0]['__count'])
|
||||||
|
for field_name in field_names:
|
||||||
|
v = data[0][field_name]
|
||||||
|
setattr(s, field_name, v)
|
||||||
|
res[query.name] = s
|
||||||
|
else:
|
||||||
|
data = model.search_read(domain, field_names)
|
||||||
|
s = AutoStruct(count=len(data))
|
||||||
|
if query.aggregate == 'min':
|
||||||
|
agg = _min
|
||||||
|
elif query.aggregate == 'max':
|
||||||
|
agg = _max
|
||||||
|
elif query.aggregate == 'avg':
|
||||||
|
agg = _avg
|
||||||
|
for field_name in field_names:
|
||||||
|
setattr(s, field_name,
|
||||||
|
agg([d[field_name] for d in data]))
|
||||||
|
res[query.name] = s
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _compute(self, lang_id, aep,
|
||||||
|
date_from, date_to,
|
||||||
|
period_from, period_to,
|
||||||
|
target_move,
|
||||||
|
get_additional_move_line_filter=None,
|
||||||
|
get_additional_query_filter=None,
|
||||||
|
period_id=None):
|
||||||
|
""" Evaluate a report for a given period.
|
||||||
|
|
||||||
|
It returns a dictionary keyed on kpi.name with the following values:
|
||||||
|
* val: the evaluated kpi, or None if there is no data or an error
|
||||||
|
* val_r: the rendered kpi as a string, or #ERR, #DIV
|
||||||
|
* val_c: a comment (explaining the error, typically)
|
||||||
|
* style: the css style of the kpi
|
||||||
|
(may change in the future!)
|
||||||
|
* prefix: a prefix to display in front of the rendered value
|
||||||
|
* suffix: a prefix to display after rendered value
|
||||||
|
* dp: the decimal precision of the kpi
|
||||||
|
* is_percentage: true if the kpi is of percentage type
|
||||||
|
(may change in the future!)
|
||||||
|
* expr: the kpi expression
|
||||||
|
* drilldown: true if the drilldown method of
|
||||||
|
mis.report.instance.period is going to do something
|
||||||
|
useful in this kpi
|
||||||
|
|
||||||
|
:param lang_id: id of a res.lang object
|
||||||
|
:param aep: an AccountingExpressionProcessor instance created
|
||||||
|
using _prepare_aep()
|
||||||
|
:param date_from, date_to: the starting and ending date
|
||||||
|
:param period_from, period_to: the starting and ending accounting
|
||||||
|
period (optional, if present must
|
||||||
|
match date_from, date_to)
|
||||||
|
:param target_move: all|posted
|
||||||
|
:param get_additional_move_line_filter: a bound method that takes
|
||||||
|
no arguments and returns
|
||||||
|
a domain compatible with
|
||||||
|
account.move.line
|
||||||
|
:param get_additional_query_filter: a bound method that takes a single
|
||||||
|
query argument and returns a
|
||||||
|
domain compatible with the query
|
||||||
|
underlying model
|
||||||
|
:param period_id: an optional opaque value that is returned as
|
||||||
|
query_id field in the result (may change in the
|
||||||
|
future!)
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
localdict = {
|
||||||
|
'registry': self.pool,
|
||||||
|
'sum': _sum,
|
||||||
|
'min': _min,
|
||||||
|
'max': _max,
|
||||||
|
'len': len,
|
||||||
|
'avg': _avg,
|
||||||
|
'AccountingNone': AccountingNone,
|
||||||
|
}
|
||||||
|
|
||||||
|
localdict.update(self._fetch_queries(
|
||||||
|
date_from, date_to, get_additional_query_filter))
|
||||||
|
|
||||||
|
additional_move_line_filter = None
|
||||||
|
if get_additional_move_line_filter:
|
||||||
|
additional_move_line_filter = get_additional_move_line_filter()
|
||||||
|
aep.do_queries(date_from, date_to,
|
||||||
|
period_from, period_to,
|
||||||
|
target_move,
|
||||||
|
additional_move_line_filter)
|
||||||
|
|
||||||
|
compute_queue = self.kpi_ids
|
||||||
|
recompute_queue = []
|
||||||
|
while True:
|
||||||
|
for kpi in compute_queue:
|
||||||
|
try:
|
||||||
|
kpi_val_comment = kpi.name + " = " + kpi.expression
|
||||||
|
kpi_eval_expression = aep.replace_expr(kpi.expression)
|
||||||
|
kpi_val = safe_eval(kpi_eval_expression, localdict)
|
||||||
|
localdict[kpi.name] = kpi_val
|
||||||
|
except ZeroDivisionError:
|
||||||
|
kpi_val = None
|
||||||
|
kpi_val_rendered = '#DIV/0'
|
||||||
|
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),)
|
||||||
|
except (NameError, ValueError):
|
||||||
|
recompute_queue.append(kpi)
|
||||||
|
kpi_val = None
|
||||||
|
kpi_val_rendered = '#ERR'
|
||||||
|
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),)
|
||||||
|
except:
|
||||||
|
kpi_val = None
|
||||||
|
kpi_val_rendered = '#ERR'
|
||||||
|
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),)
|
||||||
|
else:
|
||||||
|
kpi_val_rendered = kpi.render(lang_id, kpi_val)
|
||||||
|
|
||||||
|
try:
|
||||||
|
kpi_style = None
|
||||||
|
if kpi.css_style:
|
||||||
|
kpi_style = safe_eval(kpi.css_style, localdict)
|
||||||
|
except:
|
||||||
|
_logger.warning("error evaluating css stype expression %s",
|
||||||
|
kpi.css_style, exc_info=True)
|
||||||
|
kpi_style = None
|
||||||
|
|
||||||
|
drilldown = (kpi_val is not None and
|
||||||
|
AEP.has_account_var(kpi.expression))
|
||||||
|
|
||||||
|
res[kpi.name] = {
|
||||||
|
'val': None if kpi_val is AccountingNone else kpi_val,
|
||||||
|
'val_r': kpi_val_rendered,
|
||||||
|
'val_c': kpi_val_comment,
|
||||||
|
'style': kpi_style,
|
||||||
|
'prefix': kpi.prefix,
|
||||||
|
'suffix': kpi.suffix,
|
||||||
|
'dp': kpi.dp,
|
||||||
|
'is_percentage': kpi.type == 'pct',
|
||||||
|
'period_id': period_id,
|
||||||
|
'expr': kpi.expression,
|
||||||
|
'drilldown': drilldown,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recompute_queue) == 0:
|
||||||
|
# nothing to recompute, we are done
|
||||||
|
break
|
||||||
|
if len(recompute_queue) == len(compute_queue):
|
||||||
|
# could not compute anything in this iteration
|
||||||
|
# (ie real Value errors or cyclic dependency)
|
||||||
|
# so we stop trying
|
||||||
|
break
|
||||||
|
# try again
|
||||||
|
compute_queue = recompute_queue
|
||||||
|
recompute_queue = []
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
class MisReportInstancePeriod(models.Model):
|
class MisReportInstancePeriod(models.Model):
|
||||||
""" A MIS report instance has the logic to compute
|
""" A MIS report instance has the logic to compute
|
||||||
|
@ -432,7 +633,7 @@ class MisReportInstancePeriod(models.Model):
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def drilldown(self, expr):
|
def drilldown(self, expr):
|
||||||
assert len(self) == 1
|
self.ensure_one()
|
||||||
if AEP.has_account_var(expr):
|
if AEP.has_account_var(expr):
|
||||||
aep = AEP(self.env)
|
aep = AEP(self.env)
|
||||||
aep.parse_expr(expr)
|
aep.parse_expr(expr)
|
||||||
|
@ -456,144 +657,18 @@ class MisReportInstancePeriod(models.Model):
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _fetch_queries(self):
|
@api.multi
|
||||||
assert len(self) == 1
|
|
||||||
res = {}
|
|
||||||
for query in self.report_instance_id.report_id.query_ids:
|
|
||||||
model = self.env[query.model_id.model]
|
|
||||||
eval_context = {
|
|
||||||
'env': self.env,
|
|
||||||
'time': time,
|
|
||||||
'datetime': datetime,
|
|
||||||
'dateutil': dateutil,
|
|
||||||
# deprecated
|
|
||||||
'uid': self.env.uid,
|
|
||||||
'context': self.env.context,
|
|
||||||
}
|
|
||||||
domain = query.domain and \
|
|
||||||
safe_eval(query.domain, eval_context) or []
|
|
||||||
domain.extend(self._get_additional_query_filter(query))
|
|
||||||
if query.date_field.ttype == 'date':
|
|
||||||
domain.extend([(query.date_field.name, '>=', self.date_from),
|
|
||||||
(query.date_field.name, '<=', self.date_to)])
|
|
||||||
else:
|
|
||||||
datetime_from = _utc_midnight(
|
|
||||||
self.date_from, self._context.get('tz', 'UTC'))
|
|
||||||
datetime_to = _utc_midnight(
|
|
||||||
self.date_to, self._context.get('tz', 'UTC'), add_day=1)
|
|
||||||
domain.extend([(query.date_field.name, '>=', datetime_from),
|
|
||||||
(query.date_field.name, '<', datetime_to)])
|
|
||||||
field_names = [f.name for f in query.field_ids]
|
|
||||||
if not query.aggregate:
|
|
||||||
data = model.search_read(domain, field_names)
|
|
||||||
res[query.name] = [AutoStruct(**d) for d in data]
|
|
||||||
elif query.aggregate == 'sum':
|
|
||||||
data = model.read_group(
|
|
||||||
domain, field_names, [])
|
|
||||||
s = AutoStruct(count=data[0]['__count'])
|
|
||||||
for field_name in field_names:
|
|
||||||
v = data[0][field_name]
|
|
||||||
setattr(s, field_name, v)
|
|
||||||
res[query.name] = s
|
|
||||||
else:
|
|
||||||
data = model.search_read(domain, field_names)
|
|
||||||
s = AutoStruct(count=len(data))
|
|
||||||
if query.aggregate == 'min':
|
|
||||||
agg = _min
|
|
||||||
elif query.aggregate == 'max':
|
|
||||||
agg = _max
|
|
||||||
elif query.aggregate == 'avg':
|
|
||||||
agg = _avg
|
|
||||||
for field_name in field_names:
|
|
||||||
setattr(s, field_name,
|
|
||||||
agg([d[field_name] for d in data]))
|
|
||||||
res[query.name] = s
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _compute(self, lang_id, aep):
|
def _compute(self, lang_id, aep):
|
||||||
res = {}
|
self.ensure_one()
|
||||||
|
return self.report_instance_id.report_id._compute(
|
||||||
localdict = {
|
lang_id, aep,
|
||||||
'registry': self.pool,
|
self.date_from, self.date_to,
|
||||||
'sum': _sum,
|
|
||||||
'min': _min,
|
|
||||||
'max': _max,
|
|
||||||
'len': len,
|
|
||||||
'avg': _avg,
|
|
||||||
'AccountingNone': AccountingNone,
|
|
||||||
}
|
|
||||||
|
|
||||||
localdict.update(self._fetch_queries())
|
|
||||||
|
|
||||||
aep.do_queries(self.date_from, self.date_to,
|
|
||||||
self.period_from, self.period_to,
|
self.period_from, self.period_to,
|
||||||
self.report_instance_id.target_move,
|
self.report_instance_id.target_move,
|
||||||
self._get_additional_move_line_filter())
|
self._get_additional_move_line_filter,
|
||||||
|
self._get_additional_query_filter,
|
||||||
compute_queue = self.report_instance_id.report_id.kpi_ids
|
period_id=self.id,
|
||||||
recompute_queue = []
|
)
|
||||||
while True:
|
|
||||||
for kpi in compute_queue:
|
|
||||||
try:
|
|
||||||
kpi_val_comment = kpi.name + " = " + kpi.expression
|
|
||||||
kpi_eval_expression = aep.replace_expr(kpi.expression)
|
|
||||||
kpi_val = safe_eval(kpi_eval_expression, localdict)
|
|
||||||
localdict[kpi.name] = kpi_val
|
|
||||||
except ZeroDivisionError:
|
|
||||||
kpi_val = None
|
|
||||||
kpi_val_rendered = '#DIV/0'
|
|
||||||
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),)
|
|
||||||
except (NameError, ValueError):
|
|
||||||
recompute_queue.append(kpi)
|
|
||||||
kpi_val = None
|
|
||||||
kpi_val_rendered = '#ERR'
|
|
||||||
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),)
|
|
||||||
except:
|
|
||||||
kpi_val = None
|
|
||||||
kpi_val_rendered = '#ERR'
|
|
||||||
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),)
|
|
||||||
else:
|
|
||||||
kpi_val_rendered = kpi.render(lang_id, kpi_val)
|
|
||||||
|
|
||||||
try:
|
|
||||||
kpi_style = None
|
|
||||||
if kpi.css_style:
|
|
||||||
kpi_style = safe_eval(kpi.css_style, localdict)
|
|
||||||
except:
|
|
||||||
_logger.warning("error evaluating css stype expression %s",
|
|
||||||
kpi.css_style, exc_info=True)
|
|
||||||
kpi_style = None
|
|
||||||
|
|
||||||
drilldown = (kpi_val is not None and
|
|
||||||
AEP.has_account_var(kpi.expression))
|
|
||||||
|
|
||||||
res[kpi.name] = {
|
|
||||||
'val': None if kpi_val is AccountingNone else kpi_val,
|
|
||||||
'val_r': kpi_val_rendered,
|
|
||||||
'val_c': kpi_val_comment,
|
|
||||||
'style': kpi_style,
|
|
||||||
'prefix': kpi.prefix,
|
|
||||||
'suffix': kpi.suffix,
|
|
||||||
'dp': kpi.dp,
|
|
||||||
'is_percentage': kpi.type == 'pct',
|
|
||||||
'period_id': self.id,
|
|
||||||
'expr': kpi.expression,
|
|
||||||
'drilldown': drilldown,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(recompute_queue) == 0:
|
|
||||||
# nothing to recompute, we are done
|
|
||||||
break
|
|
||||||
if len(recompute_queue) == len(compute_queue):
|
|
||||||
# could not compute anything in this iteration
|
|
||||||
# (ie real Value errors or cyclic dependency)
|
|
||||||
# so we stop trying
|
|
||||||
break
|
|
||||||
# try again
|
|
||||||
compute_queue = recompute_queue
|
|
||||||
recompute_queue = []
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
class MisReportInstance(models.Model):
|
class MisReportInstance(models.Model):
|
||||||
|
@ -713,13 +788,9 @@ class MisReportInstance(models.Model):
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def compute(self):
|
def compute(self):
|
||||||
assert len(self) == 1
|
self.ensure_one()
|
||||||
|
|
||||||
# prepare AccountingExpressionProcessor
|
aep = self.report_id._prepare_aep(self.root_account)
|
||||||
aep = AEP(self.env)
|
|
||||||
for kpi in self.report_id.kpi_ids:
|
|
||||||
aep.parse_expr(kpi.expression)
|
|
||||||
aep.done_parsing(self.root_account)
|
|
||||||
|
|
||||||
# fetch user language only once
|
# fetch user language only once
|
||||||
# TODO: is this necessary?
|
# TODO: is this necessary?
|
||||||
|
|
Loading…
Reference in New Issue