From 203891ddb55ff91ad6abd3eb5fd243e225af359b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 7 Jun 2015 13:51:24 +0200 Subject: [PATCH] [IMP] mis_builder: new api --- mis_builder/models/mis_builder.py | 788 ++++++++---------- .../report/report_mis_report_instance.py | 2 +- mis_builder/views/mis_builder.xml | 10 +- 3 files changed, 344 insertions(+), 456 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index ca688f86..3c75c980 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -23,17 +23,13 @@ ############################################################################## from datetime import datetime, timedelta -from dateutil import parser import re import traceback import pytz -from openerp import api -from openerp.api import Environment -from openerp.osv import orm, fields -from openerp import tools + +from openerp import api, fields, models, _ from openerp.tools.safe_eval import safe_eval -from openerp.tools.translate import _ from .aep import AccountingExpressionProcessor as AEP @@ -53,14 +49,13 @@ def _get_selection_label(selection, value): def _utc_midnight(d, tz_name, add_day=0): - d = datetime.strptime(d, tools.DEFAULT_SERVER_DATE_FORMAT) + d = fields.Date.from_string(d) if add_day: d = d + timedelta(days=add_day) utc_tz = pytz.timezone('UTC') context_tz = pytz.timezone(tz_name) local_timestamp = context_tz.localize(d, is_dst=False) - return datetime.strftime(local_timestamp.astimezone(utc_tz), - tools.DEFAULT_SERVER_DATETIME_FORMAT) + return fields.Datetime.to_string(local_timestamp.astimezone(utc_tz)) def _python_var(var_str): @@ -71,7 +66,7 @@ def _is_valid_python_var(name): return re.match("[_A-Za-z][_a-zA-Z0-9]*$", name) -class mis_report_kpi(orm.Model): +class MisReportKpi(models.Model): """ A KPI is an element (ie a line) of a MIS report. In addition to a name and description, it has an expression @@ -84,151 +79,138 @@ class mis_report_kpi(orm.Model): _name = 'mis.report.kpi' - _columns = { - 'name': fields.char(size=32, required=True, - string='Name'), - 'description': fields.char(required=True, - string='Description', - translate=True), - 'expression': fields.char(required=True, - string='Expression'), - 'default_css_style': fields.char( - string='Default CSS style'), - 'css_style': fields.char(string='CSS style expression'), - 'type': fields.selection([('num', _('Numeric')), - ('pct', _('Percentage')), - ('str', _('String'))], - required=True, - string='Type'), - 'divider': fields.selection([('1e-6', _('µ')), - ('1e-3', _('m')), - ('1', _('1')), - ('1e3', _('k')), - ('1e6', _('M'))], - string='Factor'), - 'dp': fields.integer(string='Rounding'), - 'suffix': fields.char(size=16, string='Suffix'), - 'compare_method': fields.selection([('diff', _('Difference')), - ('pct', _('Percentage')), - ('none', _('None'))], - required=True, - string='Comparison Method'), - 'sequence': fields.integer(string='Sequence'), - 'report_id': fields.many2one('mis.report', string='Report', - ondelete='cascade'), - } - - _defaults = { - 'type': 'num', - 'divider': '1', - 'dp': 0, - 'compare_method': 'pct', - 'sequence': 100, - } + name = fields.Char(size=32, required=True, + string='Name') + description = fields.Char(required=True, + string='Description', + translate=True) + expression = fields.Char(required=True, + string='Expression') + default_css_style = fields.Char(string='Default CSS style') + css_style = fields.Char(string='CSS style expression') + type = fields.Selection([('num', _('Numeric')), + ('pct', _('Percentage')), + ('str', _('String'))], + required=True, + string='Type', + default='num') + divider = fields.Selection([('1e-6', _('µ')), + ('1e-3', _('m')), + ('1', _('1')), + ('1e3', _('k')), + ('1e6', _('M'))], + string='Factor', + default='1') + dp = fields.Integer(string='Rounding', default=0) + suffix = fields.Char(size=16, string='Suffix') + compare_method = fields.Selection([('diff', _('Difference')), + ('pct', _('Percentage')), + ('none', _('None'))], + required=True, + string='Comparison Method', + default='pct') + sequence = fields.Integer(string='Sequence', default=100) + report_id = fields.Many2one('mis.report', + string='Report', + ondelete='cascade') _order = 'sequence, id' - def _check_name(self, cr, uid, ids, context=None): - for record_name in self.read(cr, uid, ids, ['name']): - if not _is_valid_python_var(record_name['name']): - return False - return True - - _constraints = [ - (_check_name, 'The name must be a valid python identifier', ['name']), - ] + @api.one + @api.constrains('name') + def _check_name(self): + return _is_valid_python_var(self.name) - def onchange_name(self, cr, uid, ids, name, context=None): - res = {} - if name and not _is_valid_python_var(name): - res['warning'] = { - 'title': 'Invalid name %s' % name, - 'message': 'The name must be a valid python identifier'} - return res + @api.onchange('name') + def _onchange_name(self): + if self.name and not _is_valid_python_var(self.name): + return { + 'warning': { + 'title': 'Invalid name %s' % self.name, + 'message': 'The name must be a valid python identifier' + } + } - def onchange_description(self, cr, uid, ids, description, name, - context=None): + @api.onchange('description') + def _onchange_description(self): """ construct name from description """ - res = {} - if description and not name: - res = {'value': {'name': _python_var(description)}} - return res - - def onchange_type(self, cr, uid, ids, kpi_type, context=None): - res = {} - if kpi_type == 'pct': - res['value'] = {'compare_method': 'diff'} - elif kpi_type == 'str': - res['value'] = {'compare_method': 'none', - 'divider': '', - 'dp': 0} - return res - - def _render(self, cr, uid, lang_id, kpi, value, context=None): + if self.description and not self.name: + self.name = _python_var(self.description) + + @api.onchange('type') + def _onchange_type(self): + # TODO: change compare_method, divider and dp for all 3 types + if self.type == 'pct': + self.compare_method = 'diff' + elif self.type == 'str': + self.compare_method = 'none' + self.divider = '' + self.dp = 0 + + def render(self, lang_id, value): """ render a KPI value as a unicode string, ready for display """ + assert len(self) == 1 if value is None: return '#N/A' - elif kpi.type == 'num': - return self._render_num(cr, uid, lang_id, value, kpi.divider, - kpi.dp, kpi.suffix, context=context) - elif kpi.type == 'pct': - return self._render_num(cr, uid, lang_id, value, 0.01, - kpi.dp, '%', context=context) + elif self.type == 'num': + return self._render_num(lang_id, value, self.divider, + self.dp, self.suffix) + elif self.type == 'pct': + return self._render_num(lang_id, value, 0.01, + self.dp, '%') else: return unicode(value) - def _render_comparison(self, cr, uid, lang_id, kpi, value, base_value, - average_value, average_base_value, context=None): + def render_comparison(self, lang_id, value, base_value, + average_value, average_base_value): """ render the comparison of two KPI values, ready for display """ + assert len(self) == 1 if value is None or base_value is None: return '' - if kpi.type == 'pct': + if self.type == 'pct': return self._render_num( - cr, uid, lang_id, + lang_id, value - base_value, - 0.01, kpi.dp, _('pp'), sign='+', - context=context) - elif kpi.type == 'num': + 0.01, self.dp, _('pp'), sign='+') + elif self.type == 'num': if average_value: value = value / float(average_value) if average_base_value: base_value = base_value / float(average_base_value) - if kpi.compare_method == 'diff': + if self.compare_method == 'diff': return self._render_num( - cr, uid, lang_id, + lang_id, value - base_value, - kpi.divider, kpi.dp, kpi.suffix, sign='+', - context=context) - elif kpi.compare_method == 'pct': - if round(base_value, kpi.dp) != 0: + self.divider, self.dp, self.suffix, sign='+') + elif self.compare_method == 'pct': + if round(base_value, self.dp) != 0: return self._render_num( - cr, uid, lang_id, + lang_id, (value - base_value) / abs(base_value), - 0.01, kpi.dp, '%', sign='+', - context=context) + 0.01, self.dp, '%', sign='+') return '' - def _render_num(self, cr, uid, lang_id, value, divider, - dp, suffix, sign='-', context=None): + def _render_num(self, lang_id, value, divider, + dp, suffix, sign='-'): divider_label = _get_selection_label( self._columns['divider'].selection, divider) if divider_label == '1': divider_label = '' # format number following user language value = round(value / float(divider or 1), dp) or 0 - value = self.pool['res.lang'].format( - cr, uid, lang_id, + value = self.env.registry['res.lang'].format( + self.env.cr, self.env.uid, [lang_id], '%%%s.%df' % (sign, dp), value, grouping=True, - context=context) + context=self.env.context) value = u'%s\N{NO-BREAK SPACE}%s%s' % \ (value, divider_label, suffix or '') value = value.replace('-', u'\N{NON-BREAKING HYPHEN}') return value -class mis_report_query(orm.Model): +class MisReportQuery(models.Model): """ A query to fetch arbitrary data for a MIS report. A query works on a model and has a domain and list of fields to fetch. @@ -237,66 +219,42 @@ class mis_report_query(orm.Model): _name = 'mis.report.query' - def _get_field_names(self, cr, uid, ids, name, args, context=None): - res = {} - for query in self.browse(cr, uid, ids, context=context): - field_names = [] - for field in query.field_ids: - field_names.append(field.name) - res[query.id] = ', '.join(field_names) - return res - - def onchange_field_ids(self, cr, uid, ids, field_ids, context=None): - # compute field_names - field_names = [] - for field in self.pool.get('ir.model.fields').read( - cr, uid, - field_ids[0][2], - ['name'], - context=context): - field_names.append(field['name']) - return {'value': {'field_names': ', '.join(field_names)}} - - _columns = { - 'name': fields.char(size=32, required=True, - string='Name'), - 'model_id': fields.many2one('ir.model', required=True, - string='Model'), - 'field_ids': fields.many2many('ir.model.fields', required=True, - string='Fields to fetch'), - 'field_names': fields.function(_get_field_names, type='char', - string='Fetched fields name', - store={'mis.report.query': - (lambda self, cr, uid, ids, c={}: - ids, ['field_ids'], 20), }), - 'aggregate': fields.selection([('sum', _('Sum')), - ('avg', _('Average')), - ('min', _('Min')), - ('max', _('Max'))], - string='Aggregate'), - 'date_field': fields.many2one('ir.model.fields', required=True, - string='Date field', - domain=[('ttype', 'in', - ('date', 'datetime'))]), - 'domain': fields.char(string='Domain'), - 'report_id': fields.many2one('mis.report', string='Report', - ondelete='cascade'), - } + @api.one + @api.depends('field_ids') + def _compute_field_names(self): + field_names = [field.name for field in self.field_ids] + self.field_names = ', '.join(field_names) + + name = fields.Char(size=32, required=True, + string='Name') + model_id = fields.Many2one('ir.model', required=True, + string='Model') + field_ids = fields.Many2many('ir.model.fields', required=True, + string='Fields to fetch') + field_names = fields.Char(compute='_compute_field_names', + string='Fetched fields name') + aggregate = fields.Selection([('sum', _('Sum')), + ('avg', _('Average')), + ('min', _('Min')), + ('max', _('Max'))], + string='Aggregate') + date_field = fields.Many2one('ir.model.fields', required=True, + string='Date field', + domain=[('ttype', 'in', + ('date', 'datetime'))]) + domain = fields.Char(string='Domain') + report_id = fields.Many2one('mis.report', string='Report', + ondelete='cascade') _order = 'name' - def _check_name(self, cr, uid, ids, context=None): - for record_name in self.read(cr, uid, ids, ['name']): - if not _is_valid_python_var(record_name['name']): - return False - return True + @api.one + @api.constrains('name') + def _check_name(self): + return _is_valid_python_var(self.name) - _constraints = [ - (_check_name, 'The name must be a valid python identifier', ['name']), - ] - -class mis_report(orm.Model): +class MisReport(models.Model): """ A MIS report template (without period information) The MIS report holds: @@ -312,21 +270,19 @@ class mis_report(orm.Model): _name = 'mis.report' - _columns = { - 'name': fields.char(required=True, - string='Name', translate=True), - 'description': fields.char(required=False, - string='Description', translate=True), - 'query_ids': fields.one2many('mis.report.query', 'report_id', - string='Queries'), - 'kpi_ids': fields.one2many('mis.report.kpi', 'report_id', - string='KPI\'s'), - } + name = fields.Char(required=True, + string='Name', translate=True) + description = fields.Char(required=False, + string='Description', translate=True) + query_ids = fields.One2many('mis.report.query', 'report_id', + string='Queries') + kpi_ids = fields.One2many('mis.report.kpi', 'report_id', + string='KPI\'s') # TODO: kpi name cannot be start with query name -class mis_report_instance_period(orm.Model): +class MisReportInstancePeriod(models.Model): """ A MIS report instance has the logic to compute a report template for a given date period. @@ -334,122 +290,93 @@ class mis_report_instance_period(orm.Model): are defined as an offset relative to a pivot date. """ - def _get_dates(self, cr, uid, ids, field_names, arg, context=None): - if isinstance(ids, (int, long)): - ids = [ids] - res = {} - for c in self.browse(cr, uid, ids, context=context): - date_from = False - date_to = False - period_ids = None - valid = False - d = parser.parse(c.report_instance_id.pivot_date) - if c.type == 'd': - date_from = d + timedelta(days=c.offset) - date_to = date_from + timedelta(days=c.duration - 1) - date_from = date_from.strftime( - tools.DEFAULT_SERVER_DATE_FORMAT) - date_to = date_to.strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - valid = True - elif c.type == 'w': - date_from = d - timedelta(d.weekday()) - date_from = date_from + timedelta(days=c.offset * 7) - date_to = date_from + timedelta(days=(7 * c.duration) - 1) - date_from = date_from.strftime( - tools.DEFAULT_SERVER_DATE_FORMAT) - date_to = date_to.strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - valid = True - elif c.type == 'fp': - period_obj = self.pool['account.period'] - current_period_ids = period_obj.search( - cr, uid, + @api.one + @api.depends('report_instance_id.pivot_date', 'type', 'offset', 'duration') + def _compute_dates(self): + self.date_from = False + self.date_to = False + self.period_from = False + self.period_to = False + self.valid = False + d = fields.Date.from_string(self.report_instance_id.pivot_date) + if self.type == 'd': + date_from = d + timedelta(days=self.offset) + date_to = date_from + timedelta(days=self.duration - 1) + self.date_from = fields.Date.to_string(date_from) + self.date_to = fields.Date.to_string(date_to) + self.valid = True + elif self.type == 'w': + date_from = d - timedelta(d.weekday()) + date_from = date_from + timedelta(days=self.offset * 7) + date_to = date_from + timedelta(days=(7 * self.duration) - 1) + self.date_from = fields.Date.to_string(date_from) + self.date_to = fields.Date.to_string(date_to) + self.valid = True + elif self.type == 'fp': + current_periods = self.env['account.period'].search( + [('special', '=', False), + ('date_start', '<=', d), + ('date_stop', '>=', d), + ('company_id', '=', + self.report_instance_id.company_id.id)]) + if current_periods: + all_periods = self.env['account.period'].search( [('special', '=', False), - ('date_start', '<=', d), - ('date_stop', '>=', d), - ('company_id', '=', c.company_id.id)], - context=context) - if current_period_ids: - all_period_ids = period_obj.search( - cr, uid, - [('special', '=', False), - ('company_id', '=', c.company_id.id)], - order='date_start', - context=context) - p = all_period_ids.index(current_period_ids[0]) + c.offset - if p >= 0 and p + c.duration <= len(all_period_ids): - period_ids = all_period_ids[p:p + c.duration] - periods = period_obj.browse(cr, uid, period_ids, - context=context) - date_from = periods[0].date_start - date_to = periods[-1].date_stop - valid = True - - res[c.id] = { - 'date_from': date_from, - 'date_to': date_to, - 'period_from': period_ids and period_ids[0] or False, - 'period_to': period_ids and period_ids[-1] or False, - 'valid': valid, - } - return res + ('company_id', '=', + self.report_instance_id.company_id.id)], + order='date_start') + all_period_ids = [p.id for p in all_periods] + p = all_period_ids.index(current_periods[0].id) + self.offset + if p >= 0 and p + self.duration <= len(all_period_ids): + periods = all_periods[p:p + self.duration] + self.date_from = periods[0].date_start + self.date_to = periods[-1].date_stop + self.period_from = periods[0] + self.period_to = periods[-1] + self.valid = True _name = 'mis.report.instance.period' - _columns = { - 'name': fields.char(size=32, required=True, - string='Description', translate=True), - 'type': fields.selection([('d', _('Day')), - ('w', _('Week')), - ('fp', _('Fiscal Period')), - # ('fy', _('Fiscal Year')) - ], - required=True, - string='Period type'), - 'offset': fields.integer(string='Offset', - help='Offset from current period'), - 'duration': fields.integer(string='Duration', - help='Number of periods'), - 'date_from': fields.function(_get_dates, - type='date', - multi="dates", - string="From"), - 'date_to': fields.function(_get_dates, - type='date', - multi="dates", - string="To"), - 'period_from': fields.function(_get_dates, - type='many2one', obj='account.period', - multi="dates", string="From period"), - 'period_to': fields.function(_get_dates, - type='many2one', obj='account.period', - multi="dates", string="To period"), - 'valid': fields.function(_get_dates, - type='boolean', - multi='dates', string='Valid'), - 'sequence': fields.integer(string='Sequence'), - 'report_instance_id': fields.many2one('mis.report.instance', - string='Report Instance', - ondelete='cascade'), - 'comparison_column_ids': fields.many2many( - 'mis.report.instance.period', - 'mis_report_instance_period_rel', - 'period_id', - 'compare_period_id', - string='Compare with'), - 'company_id': fields.related('report_instance_id', 'company_id', - type="many2one", relation="res.company", - string="Company", readonly=True), - 'normalize_factor': fields.integer( - string='Factor', - help='Factor to use to normalize the period (used in comparison'), - } - - _defaults = { - 'offset': -1, - 'duration': 1, - 'sequence': 100, - 'normalize_factor': 1, - } + name = fields.Char(size=32, required=True, + string='Description', translate=True) + type = fields.Selection([('d', _('Day')), + ('w', _('Week')), + ('fp', _('Fiscal Period')), + # ('fy', _('Fiscal Year')) + ], + required=True, + string='Period type') + offset = fields.Integer(string='Offset', + help='Offset from current period', + default=-1) + duration = fields.Integer(string='Duration', + help='Number of periods', + default=1) + date_from = fields.Date(compute='_compute_dates', string="From") + date_to = fields.Date(compute='_compute_dates', string="To") + period_from = fields.Many2one(compute='_compute_dates', + comodel_name='account.period', + string="From period") + period_to = fields.Many2one(compute='_compute_dates', + comodel_name='account.period', + string="To period") + valid = fields.Boolean(compute='_compute_dates', + type='boolean', + string='Valid') + sequence = fields.Integer(string='Sequence', default=100) + report_instance_id = fields.Many2one('mis.report.instance', + string='Report Instance', + ondelete='cascade') + comparison_column_ids = fields.Many2many( + comodel_name='mis.report.instance.period', + relation='mis_report_instance_period_rel', + column1='period_id', + column2='compare_period_id', + string='Compare with') + normalize_factor = fields.Integer( + string='Factor', + help='Factor to use to normalize the period (used in comparison', + default=1) _order = 'sequence, id' @@ -462,21 +389,20 @@ class mis_report_instance_period(orm.Model): 'Period name should be unique by report'), ] - def drilldown(self, cr, uid, _id, expr, context=None): - if context is None: - context = {} - this = self.browse(cr, uid, _id, context=context)[0] + @api.multi + def drilldown(self, expr): + assert len(self) == 1 if AEP.has_account_var(expr): - env = Environment(cr, uid, context) - aep = AEP(env) + aep = AEP(self.env) aep.parse_expr(expr) - aep.done_parsing(this.report_instance_id.root_account) + aep.done_parsing(self.report_instance_id.root_account) domain = aep.get_aml_domain_for_expr( - expr, this.date_from, this.date_to, - this.period_from, this.period_to, - this.report_instance_id.target_move) + expr, + self.date_from, self.date_to, + self.period_from, self.period_to, + self.report_instance_id.target_move) return { - 'name': expr + ' - ' + this.name, + 'name': expr + ' - ' + self.name, 'domain': domain, 'type': 'ir.actions.act_window', 'res_model': 'account.move.line', @@ -488,44 +414,42 @@ class mis_report_instance_period(orm.Model): else: return False - def _fetch_queries(self, cr, uid, c, context): + def _fetch_queries(self): + assert len(self) == 1 res = {} - report = c.report_instance_id.report_id - for query in report.query_ids: - obj = self.pool[query.model_id.model] + for query in self.report_instance_id.report_id.query_ids: + model = self.env[query.model_id.model] domain = query.domain and safe_eval(query.domain) or [] if query.date_field.ttype == 'date': - domain.extend([(query.date_field.name, '>=', c.date_from), - (query.date_field.name, '<=', c.date_to)]) + domain.extend([(query.date_field.name, '>=', self.date_from), + (query.date_field.name, '<=', self.date_to)]) else: datetime_from = _utc_midnight( - c.date_from, context.get('tz', 'UTC')) + self.date_from, self._context.get('tz', 'UTC')) datetime_to = _utc_midnight( - c.date_to, context.get('tz', 'UTC'), add_day=1) + 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)]) - if obj._columns.get('company_id'): + # TODO: we probably don't need company_id here + if model._columns.get('company_id'): domain.extend(['|', ('company_id', '=', False), - ('company_id', '=', c.company_id.id)]) + ('company_id', '=', + self.report_instance_id.company_id.id)]) field_names = [f.name for f in query.field_ids] if not query.aggregate: - obj_ids = obj.search(cr, uid, domain, context=context) - obj_datas = obj.read( - cr, uid, obj_ids, field_names, context=context) - res[query.name] = [AutoStruct(**d) for d in obj_datas] + data = model.search_read(domain, field_names) + res[query.name] = [AutoStruct(**d) for d in data] elif query.aggregate == 'sum': - obj_datas = obj.read_group( - cr, uid, domain, field_names, [], context=context) - s = AutoStruct(count=obj_datas[0]['__count']) + data = model.read_group( + domain, field_names, []) + s = AutoStruct(count=data[0]['__count']) for field_name in field_names: - v = obj_datas[0][field_name] + v = data[0][field_name] setattr(s, field_name, v) res[query.name] = s else: - obj_ids = obj.search(cr, uid, domain, context=context) - obj_datas = obj.read( - cr, uid, obj_ids, field_names, context=context) - s = AutoStruct(count=len(obj_datas)) + data = model.search_read(domain, field_names) + s = AutoStruct(count=len(data)) if query.aggregate == 'min': agg = min elif query.aggregate == 'max': @@ -534,16 +458,11 @@ class mis_report_instance_period(orm.Model): agg = lambda l: sum(l) / float(len(l)) for field_name in field_names: setattr(s, field_name, - agg([d[field_name] for d in obj_datas])) + agg([d[field_name] for d in data])) res[query.name] = s return res - def _compute(self, cr, uid, lang_id, c, aep, context=None): - if context is None: - context = {} - - kpi_obj = self.pool['mis.report.kpi'] - + def _compute(self, lang_id, aep): res = {} localdict = { @@ -554,14 +473,14 @@ class mis_report_instance_period(orm.Model): 'len': len, 'avg': lambda l: sum(l) / float(len(l)), } - localdict.update(self._fetch_queries(cr, uid, c, - context=context)) - aep.do_queries(c.date_from, c.date_to, - c.period_from, c.period_to, - c.report_instance_id.target_move) + 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) - compute_queue = c.report_instance_id.report_id.kpi_ids + compute_queue = self.report_instance_id.report_id.kpi_ids recompute_queue = [] while True: for kpi in compute_queue: @@ -583,8 +502,7 @@ class mis_report_instance_period(orm.Model): kpi_val_rendered = '#ERR' kpi_val_comment += '\n\n%s' % (traceback.format_exc(),) else: - kpi_val_rendered = kpi_obj._render( - cr, uid, lang_id, kpi, kpi_val, context=context) + kpi_val_rendered = kpi.render(lang_id, kpi_val) localdict[kpi.name] = kpi_val try: @@ -592,6 +510,7 @@ class mis_report_instance_period(orm.Model): if kpi.css_style: kpi_style = safe_eval(kpi.css_style, localdict) except: + # TODO: log warning kpi_style = None drilldown = (kpi_val is not None and @@ -605,7 +524,7 @@ class mis_report_instance_period(orm.Model): 'suffix': kpi.suffix, 'dp': kpi.dp, 'is_percentage': kpi.type == 'pct', - 'period_id': c.id, + 'period_id': self.id, 'expr': kpi.expression, 'drilldown': drilldown, } @@ -625,127 +544,101 @@ class mis_report_instance_period(orm.Model): return res -class mis_report_instance(orm.Model): +class MisReportInstance(models.Model): """The MIS report instance combines everything to compute a MIS report template for a set of periods.""" - def _get_pivot_date(self, cr, uid, ids, field_name, arg, context=None): - res = {} - for r in self.browse(cr, uid, ids, context=context): - if r.date: - res[r.id] = r.date - else: - res[r.id] = fields.date.context_today(self, cr, uid, - context=context) - return res - - def _get_root_account(self, cr, uid, ids, field_name, arg, context=None): - res = {} - account_obj = self.pool['account.account'] - for r in self.browse(cr, uid, ids, context=context): - account_ids = account_obj.search( - cr, uid, - [('parent_id', '=', False), - ('company_id', '=', r.company_id.id)], - context=context) - if len(account_ids) == 1: - res[r.id] = account_ids[0] - return res + @api.one + @api.depends('date') + def _compute_pivot_date(self): + if self.date: + self.pivot_date = self.date + else: + self.pivot_date = fields.Date.context_today(self) + + @api.one + @api.depends('company_id') + def _compute_root_account(self): + self.root_account = False + accounts = self.env['account.account'].search( + [('parent_id', '=', False), + ('company_id', '=', self.company_id.id)]) + if len(accounts) == 1: + self.root_account = accounts[0] _name = 'mis.report.instance' - _columns = { - 'name': fields.char(required=True, - string='Name', translate=True), - 'description': fields.char(required=False, - string='Description', translate=True), - 'date': fields.date(string='Base date', - help='Report base date ' - '(leave empty to use current date)'), - 'pivot_date': fields.function(_get_pivot_date, - type='date', - string="Pivot date"), - 'report_id': fields.many2one('mis.report', - required=True, - string='Report'), - 'period_ids': fields.one2many('mis.report.instance.period', - 'report_instance_id', - required=True, - string='Periods'), - 'target_move': fields.selection([('posted', 'All Posted Entries'), - ('all', 'All Entries'), - ], 'Target Moves', required=True), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'root_account': fields.function(_get_root_account, - type='many2one', obj='account.account', - string="Account chart"), - 'landscape_pdf': fields.boolean(string='Landscape PDF'), - } - - _defaults = { - 'target_move': 'posted', - 'company_id': lambda s, cr, uid, c: - s.pool.get('res.company')._company_default_get( - cr, uid, - 'mis.report.instance', - context=c) - } - - def _format_date(self, cr, uid, lang_id, date, context=None): + name = fields.Char(required=True, + string='Name', translate=True) + description = fields.Char(required=False, + string='Description', translate=True) + date = fields.Date(string='Base date', + help='Report base date ' + '(leave empty to use current date)') + pivot_date = fields.Date(compute='_compute_pivot_date', + string="Pivot date") + report_id = fields.Many2one('mis.report', + required=True, + string='Report') + period_ids = fields.One2many('mis.report.instance.period', + 'report_instance_id', + required=True, + string='Periods') + target_move = fields.Selection([('posted', 'All Posted Entries'), + ('all', 'All Entries')], + string='Target Moves', required=True, + default='posted') + company_id = fields.Many2one('res.company', 'Company', required=True, + default=lambda self: self.env['res.company']. + _company_default_get('mis.report.instance')) + root_account = fields.Many2one(compute='_compute_root_account', + comodel_name='account.account', + string="Account chart") + landscape_pdf = fields.Boolean(string='Landscape PDF') + + def _format_date(self, lang_id, date): # format date following user language - tformat = self.pool['res.lang'].read( - cr, uid, lang_id, ['date_format'])[0]['date_format'] - return datetime.strftime(datetime.strptime( - date, - tools.DEFAULT_SERVER_DATE_FORMAT), - tformat) - - def preview(self, cr, uid, _id, context=None): - view_id = self.pool['ir.model.data'].get_object_reference( - cr, uid, 'mis_builder', 'mis_report_instance_result_view_form')[1] + date_format = self.env['res.lang'].browse(lang_id).date_format + return datetime.strftime(fields.Date.from_string(date), date_format) + + @api.multi + def preview(self): + assert len(self) == 1 + view_id = self.env.ref('mis_builder.' + 'mis_report_instance_result_view_form') return { 'type': 'ir.actions.act_window', 'res_model': 'mis.report.instance', - 'res_id': _id[0], + 'res_id': self.id, 'view_mode': 'form', 'view_type': 'form', - 'view_id': view_id, + 'view_id': view_id.id, 'target': 'new', } - @api.cr_uid_id_context - def compute(self, cr, uid, _id, context=None): - assert isinstance(_id, (int, long)) - if context is None: - context = {} - - this = self.browse(cr, uid, _id, context=context) - - kpi_obj = self.pool['mis.report.kpi'] - report_instance_period_obj = self.pool['mis.report.instance.period'] + @api.multi + def compute(self): + assert len(self) == 1 # prepare AccountingExpressionProcessor - env = Environment(cr, uid, context) - aep = AEP(env) - for kpi in this.report_id.kpi_ids: + aep = AEP(self.env) + for kpi in self.report_id.kpi_ids: aep.parse_expr(kpi.expression) - aep.done_parsing(this.root_account) + aep.done_parsing(self.root_account) - # fetch user language only once (TODO: is it necessary?) - lang = self.pool['res.users'].read( - cr, uid, uid, ['lang'], context=context)['lang'] + # fetch user language only once + # TODO: is this necessary? + lang = self.env.user.lang if not lang: lang = 'en_US' - lang_id = self.pool['res.lang'].search( - cr, uid, [('code', '=', lang)], context=context) + lang_id = self.env['res.lang'].search([('code', '=', lang)]).id # compute kpi values for each period kpi_values_by_period_ids = {} - for period in this.period_ids: + for period in self.period_ids: if not period.valid: continue - kpi_values = report_instance_period_obj._compute( - cr, uid, lang_id, period, aep, context=context) + kpi_values = period._compute(lang_id, aep) kpi_values_by_period_ids[period.id] = kpi_values # prepare header and content @@ -756,7 +649,7 @@ class mis_report_instance(orm.Model): }) content = [] rows_by_kpi_name = {} - for kpi in this.report_id.kpi_ids: + for kpi in self.report_id.kpi_ids: rows_by_kpi_name[kpi.name] = { 'kpi_name': kpi.description, 'cols': [], @@ -765,20 +658,19 @@ class mis_report_instance(orm.Model): content.append(rows_by_kpi_name[kpi.name]) # populate header and content - for period in this.period_ids: + for period in self.period_ids: if not period.valid: continue # add the column header + # TODO: format period.date_from header[0]['cols'].append(dict( name=period.name, date=(period.duration > 1 or period.type == 'w') and _('from %s to %s' % (period.period_from and period.period_from.name - or self._format_date(cr, uid, lang_id, period.date_from, - context=context), + or self._format_date(lang_id, period.date_from), period.period_to and period.period_to.name - or self._format_date(cr, uid, lang_id, period.date_to, - context=context))) + or self._format_date(lang_id, period.date_to))) or period.period_from and period.period_from.name or period.date_from)) # add kpi values @@ -792,22 +684,20 @@ class mis_report_instance(orm.Model): kpi_values_by_period_ids.get(compare_col.id) if compare_kpi_values: # add the comparison column header + # TODO: make 'vs' translatable header[0]['cols'].append( dict(name='%s vs %s' % (period.name, compare_col.name), date='')) # add comparison values - for kpi in this.report_id.kpi_ids: - rows_by_kpi_name[kpi.name]['cols'].append( - {'val_r': kpi_obj._render_comparison( - cr, - uid, + for kpi in self.report_id.kpi_ids: + rows_by_kpi_name[kpi.name]['cols'].append({ + 'val_r': kpi.render_comparison( lang_id, - kpi, kpi_values[kpi.name]['val'], compare_kpi_values[kpi.name]['val'], period.normalize_factor, - compare_col.normalize_factor, - context=context)}) + compare_col.normalize_factor) + }) return {'header': header, 'content': content} diff --git a/mis_builder/report/report_mis_report_instance.py b/mis_builder/report/report_mis_report_instance.py index c1e8d408..72dc3b9d 100644 --- a/mis_builder/report/report_mis_report_instance.py +++ b/mis_builder/report/report_mis_report_instance.py @@ -38,7 +38,7 @@ class ReportMisReportInstance(models.AbstractModel): docs = self.env['mis.report.instance'].browse(self._ids) docs_computed = {} for doc in docs: - docs_computed[doc.id] = doc.compute()[0] + docs_computed[doc.id] = doc.compute() docargs = { 'doc_ids': self._ids, 'doc_model': 'mis.report.instance', diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index d01d39c9..3bc3da69 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -33,8 +33,7 @@ - + @@ -44,10 +43,10 @@ - - + + - + @@ -190,7 +189,6 @@ -