[IMP] mis_builder: refactor the compute method
The main computation entry point is now on mis.report instead of mis.report.instance.period so it can easily be reused by other module who need to evaluate mis.report's on without creating full-fledge persistant mis.report.instance (such as a wizard that allows the user to select the period for a one-off run).pull/189/head
parent
499ce4edc6
commit
3f7306166a
|
@ -296,6 +296,201 @@ class MisReport(models.Model):
|
|||
|
||||
# TODO: kpi name cannot be start with query name
|
||||
|
||||
@api.multi
|
||||
def _prepare_aep(self):
|
||||
self.ensure_one()
|
||||
aep = AEP(self.env)
|
||||
for kpi in self.kpi_ids:
|
||||
aep.parse_expr(kpi.expression)
|
||||
aep.done_parsing()
|
||||
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,
|
||||
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 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))
|
||||
|
||||
if get_additional_move_line_filter:
|
||||
additional_move_line_filter = get_additional_move_line_filter()
|
||||
aep.do_queries(date_from, date_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):
|
||||
""" A MIS report instance has the logic to compute
|
||||
|
@ -405,7 +600,7 @@ class MisReportInstancePeriod(models.Model):
|
|||
|
||||
@api.multi
|
||||
def drilldown(self, expr):
|
||||
assert len(self) == 1
|
||||
self.ensure_one()
|
||||
if AEP.has_account_var(expr):
|
||||
aep = AEP(self.env)
|
||||
aep.parse_expr(expr)
|
||||
|
@ -428,143 +623,17 @@ class MisReportInstancePeriod(models.Model):
|
|||
else:
|
||||
return False
|
||||
|
||||
def _fetch_queries(self):
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def _compute(self, lang_id, aep):
|
||||
res = {}
|
||||
|
||||
localdict = {
|
||||
'registry': self.pool,
|
||||
'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.report_instance_id.target_move,
|
||||
self._get_additional_move_line_filter())
|
||||
|
||||
compute_queue = self.report_instance_id.report_id.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': 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
|
||||
self.ensure_one()
|
||||
return self.report_instance_id.report_id._compute(
|
||||
lang_id, aep,
|
||||
self.date_from, self.date_to,
|
||||
self.report_instance_id.target_move,
|
||||
self._get_additional_move_line_filter,
|
||||
self._get_additional_query_filter,
|
||||
period_id=self.id,
|
||||
)
|
||||
|
||||
|
||||
class MisReportInstance(models.Model):
|
||||
|
@ -675,13 +744,8 @@ class MisReportInstance(models.Model):
|
|||
|
||||
@api.multi
|
||||
def compute(self):
|
||||
assert len(self) == 1
|
||||
|
||||
# prepare AccountingExpressionProcessor
|
||||
aep = AEP(self.env)
|
||||
for kpi in self.report_id.kpi_ids:
|
||||
aep.parse_expr(kpi.expression)
|
||||
aep.done_parsing()
|
||||
self.ensure_one()
|
||||
aep = self.report_id._prepare_aep()
|
||||
|
||||
# fetch user language only once
|
||||
# TODO: is this necessary?
|
||||
|
|
Loading…
Reference in New Issue