Browse Source

[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/179/head
Stéphane Bidoul 9 years ago
parent
commit
c57c25d814
  1. 358
      mis_builder/models/mis_builder.py

358
mis_builder/models/mis_builder.py

@ -284,6 +284,206 @@ 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))
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
@ -424,7 +624,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)
@ -448,144 +648,18 @@ class MisReportInstancePeriod(models.Model):
else: else:
return False 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): 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.period_from, self.period_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.period_from, self.period_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): class MisReportInstance(models.Model):
@ -698,13 +772,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 = AEP(self.env)
for kpi in self.report_id.kpi_ids:
aep.parse_expr(kpi.expression)
aep.done_parsing(self.root_account)
aep = self.report_id._prepare_aep(self.root_account)
# fetch user language only once # fetch user language only once
# TODO: is this necessary? # TODO: is this necessary?

Loading…
Cancel
Save