diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index eee3b052..1e62138d 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -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?