From 495693dacc10fcde4550679c069265f512dfbb09 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 11 Jun 2015 17:31:02 +0200 Subject: [PATCH] [ADD] backport mis_builder from 8.0 --- mis_builder/__openerp__.py | 9 +- mis_builder/models/aep.py | 161 ++-- mis_builder/models/mis_builder.py | 820 +++++++++++------- mis_builder/report/__init__.py | 1 - .../report/report_mis_report_instance.py | 66 -- .../report/report_mis_report_instance.xml | 55 -- mis_builder/tests/__init__.py | 4 + mis_builder/views/mis_builder.xml | 18 - 8 files changed, 604 insertions(+), 530 deletions(-) delete mode 100644 mis_builder/report/report_mis_report_instance.py delete mode 100644 mis_builder/report/report_mis_report_instance.xml diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index 3b8c22e0..417d5033 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -29,6 +29,8 @@ 'summary': """ Build 'Management Information System' Reports and Dashboards """, + 'description': """ + """, 'author': 'ACSONE SA/NV', 'website': 'http://acsone.eu', 'depends': [ @@ -40,7 +42,6 @@ 'views/mis_builder.xml', 'security/ir.model.access.csv', 'security/mis_builder_security.xml', - 'report/report_mis_report_instance.xml', ], 'test': [ ], @@ -51,6 +52,12 @@ 'tests/mis.report.instance.period.csv', 'tests/mis.report.instance.csv', ], + 'js': [ + 'static/src/js/*.js' + ], + 'css': [ + 'static/src/css/*.css' + ], 'qweb': [ 'static/src/xml/*.xml' ], diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index fc1622bf..16e23b69 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -26,6 +26,7 @@ import re from collections import defaultdict from openerp.exceptions import Warning +from openerp import pooler from openerp.osv import expression from openerp.tools.safe_eval import safe_eval from openerp.tools.translate import _ @@ -82,18 +83,16 @@ class AccountingExpressionProcessor(object): r"(?P_[a-zA-Z0-9]+|\[.*?\])" r"(?P\[.*?\])?") - def __init__(self, env): - self.env = env + def __init__(self, cursor): + self.pool = pooler.get_pool(cursor.dbname) # before done_parsing: {(domain, mode): set(account_codes)} # after done_parsing: {(domain, mode): list(account_ids)} self._map_account_ids = defaultdict(set) self._account_ids_by_code = defaultdict(set) - def _load_account_codes(self, account_codes, root_account): - account_model = self.env['account.account'] - # TODO: account_obj is necessary because _get_children_and_consol - # does not work in new API? - account_obj = self.env.registry('account.account') + def _load_account_codes(self, cr, uid, account_codes, root_account, + context=None): + account_obj = self.pool['account.account'] exact_codes = set() like_codes = set() for account_code in account_codes: @@ -109,9 +108,13 @@ class AccountingExpressionProcessor(object): like_codes.add(account_code) else: exact_codes.add(account_code) - for account in account_model.\ - search([('code', 'in', list(exact_codes)), - ('parent_id', 'child_of', root_account.id)]): + account_ids = account_obj.search( + cr, uid, + [('code', 'in', list(exact_codes)), + ('parent_id', 'child_of', root_account.id)], + context=context) + for account in account_obj.browse( + cr, uid, account_ids, context=context): if account.code == root_account.code: code = None else: @@ -119,21 +122,26 @@ class AccountingExpressionProcessor(object): if account.type in ('view', 'consolidation'): self._account_ids_by_code[code].update( account_obj._get_children_and_consol( - self.env.cr, self.env.uid, + cr, uid, [account.id], - self.env.context)) + context=context)) else: self._account_ids_by_code[code].add(account.id) for like_code in like_codes: - for account in account_model.\ - search([('code', 'like', like_code), - ('parent_id', 'child_of', root_account.id)]): + for account_id in account_obj.\ + search(cr, uid, + [('code', 'like', like_code), + ('parent_id', 'child_of', root_account.id)], + context=context): + account = account_obj.browse(cr, uid, account_id, + context=context) if account.type in ('view', 'consolidation'): self._account_ids_by_code[like_code].update( account_obj._get_children_and_consol( + cr, uid, self.env.cr, self.env.uid, [account.id], - self.env.context)) + context=context)) else: self._account_ids_by_code[like_code].add(account.id) @@ -171,11 +179,12 @@ class AccountingExpressionProcessor(object): key = (domain, mode) self._map_account_ids[key].update(account_codes) - def done_parsing(self, root_account): + def done_parsing(self, cr, uid, root_account, context=None): """Load account codes and replace account codes by account ids in map.""" for key, account_codes in self._map_account_ids.items(): - self._load_account_codes(account_codes, root_account) + self._load_account_codes(cr, uid, account_codes, root_account, + context=context) account_ids = set() for account_code in account_codes: account_ids.update(self._account_ids_by_code[account_code]) @@ -219,78 +228,105 @@ class AccountingExpressionProcessor(object): expression.OR(date_domain_by_mode.values()) def _period_has_moves(self, period): - move_model = self.env['account.move'] + move_model = self.pool['account.move'] return bool(move_model.search([('period_id', '=', period.id)], limit=1)) - def _get_previous_opening_period(self, period, company_id): - period_model = self.env['account.period'] - periods = period_model.search( + def _get_previous_opening_period(self, cr, uid, period, company_id, + context=None): + period_model = self.pool['account.period'] + period_ids = period_model.search( + cr, uid, [('date_start', '<=', period.date_start), ('special', '=', True), ('company_id', '=', company_id)], order="date_start desc", - limit=1) + limit=1, + context=context) + periods = period_model.browse(cr, uid, period_ids, context=context) return periods and periods[0] - def _get_previous_normal_period(self, period, company_id): - period_model = self.env['account.period'] - periods = period_model.search( + def _get_previous_normal_period(self, cr, uid, period, company_id, + context=None): + period_model = self.pool['account.period'] + period_ids = period_model.search( + cr, uid, [('date_start', '<', period.date_start), ('special', '=', False), ('company_id', '=', company_id)], order="date_start desc", - limit=1) + limit=1, + context=context) + periods = period_model.browse(cr, uid, period_ids, context=context) return periods and periods[0] - def _get_first_normal_period(self, company_id): - period_model = self.env['account.period'] - periods = period_model.search( + def _get_first_normal_period(self, cr, uid, company_id, context=None): + period_model = self.pool['account.period'] + period_ids = period_model.search( + cr, uid, [('special', '=', False), ('company_id', '=', company_id)], order="date_start asc", - limit=1) + limit=1, + context=context) + periods = period_model.browse(cr, uid, period_ids, context=context) return periods and periods[0] - def _get_period_ids_between(self, period_from, period_to, company_id): - period_model = self.env['account.period'] - periods = period_model.search( + def _get_period_ids_between(self, cr, uid, period_from, period_to, + company_id, context=None): + period_model = self.pool['account.period'] + period_ids = period_model.search( + cr, uid, [('date_start', '>=', period_from.date_start), ('date_stop', '<=', period_to.date_stop), ('special', '=', False), - ('company_id', '=', company_id)]) - period_ids = [p.id for p in periods] + ('company_id', '=', company_id)], + context=context) if period_from.special: period_ids.append(period_from.id) return period_ids - def _get_period_company_ids(self, period_from, period_to): - period_model = self.env['account.period'] - periods = period_model.search( + def _get_period_company_ids(self, cr, uid, period_from, period_to, + context=None): + period_model = self.pool['account.period'] + period_ids = period_model.search( + cr, uid, [('date_start', '>=', period_from.date_start), ('date_stop', '<=', period_to.date_stop), - ('special', '=', False)]) + ('special', '=', False)], + context=context) + periods = period_model.browse(cr, uid, period_ids, context=context) return set([p.company_id.id for p in periods]) - def _get_period_ids_for_mode(self, period_from, period_to, mode): + def _get_period_ids_for_mode(self, cr, uid, period_from, period_to, mode, + context=None): assert not period_from.special assert not period_to.special assert period_from.company_id == period_to.company_id assert period_from.date_start <= period_to.date_start period_ids = [] - for company_id in self._get_period_company_ids(period_from, period_to): + for company_id in self._get_period_company_ids(cr, uid, + period_from, period_to, + context=context): if mode == MODE_VARIATION: period_ids.extend(self._get_period_ids_between( - period_from, period_to, company_id)) + cr, uid, + period_from, period_to, company_id, + context=context)) else: if mode == MODE_INITIAL: period_to = self._get_previous_normal_period( - period_from, company_id) + cr, uid, + period_from, company_id, + context=context) # look for opening period with moves opening_period = self._get_previous_opening_period( - period_from, company_id) + cr, uid, + period_from, company_id, + context=context) if opening_period and \ - self._period_has_moves(opening_period[0]): + self._period_has_moves(cr, uid, opening_period[0], + context=context): # found opening period with moves if opening_period.date_start == period_from.date_start and \ mode == MODE_INITIAL: @@ -303,19 +339,25 @@ class AccountingExpressionProcessor(object): else: # no opening period with moves, # use very first normal period - period_from = self._get_first_normal_period(company_id) + period_from = self._get_first_normal_period( + cr, uid, company_id, context=context) if period_to: period_ids.extend(self._get_period_ids_between( - period_from, period_to, company_id)) + cr, uid, + period_from, period_to, company_id, + context=context)) return period_ids - def get_aml_domain_for_dates(self, date_from, date_to, + def get_aml_domain_for_dates(self, cr, uid, date_from, date_to, period_from, period_to, mode, - target_move): + target_move, + context=None): if period_from and period_to: period_ids = self._get_period_ids_for_mode( - period_from, period_to, mode) + cr, uid, + period_from, period_to, mode, + context=context) domain = [('period_id', 'in', period_ids)] else: if mode == MODE_VARIATION: @@ -327,14 +369,14 @@ class AccountingExpressionProcessor(object): domain.append(('move_id.state', '=', 'posted')) return expression.normalize_domain(domain) - def do_queries(self, date_from, date_to, period_from, period_to, - target_move): + def do_queries(self, cr, uid, date_from, date_to, period_from, period_to, + target_move, context=None): """Query sums of debit and credit for all accounts and domains used in expressions. This method must be executed after done_parsing(). """ - aml_model = self.env['account.move.line'] + aml_model = self.pool['account.move.line'] # {(domain, mode): {account_id: (debit, credit)}} self._data = defaultdict(dict) domain_by_mode = {} @@ -342,15 +384,18 @@ class AccountingExpressionProcessor(object): domain, mode = key if mode not in domain_by_mode: domain_by_mode[mode] = \ - self.get_aml_domain_for_dates(date_from, date_to, + self.get_aml_domain_for_dates(cr, uid, + date_from, date_to, period_from, period_to, - mode, target_move) + mode, target_move, + context=context) domain = list(domain) + domain_by_mode[mode] domain.append(('account_id', 'in', self._map_account_ids[key])) # fetch sum of debit/credit, grouped by account_id - accs = aml_model.read_group(domain, + accs = aml_model.read_group(cr, uid, domain, ['debit', 'credit', 'account_id'], - ['account_id']) + ['account_id'], + context=context) for acc in accs: self._data[key][acc['account_id'][0]] = \ (acc['debit'] or 0.0, acc['credit'] or 0.0) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 76e5bf2c..a3a1f006 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -23,14 +23,17 @@ ############################################################################## from datetime import datetime, timedelta +from dateutil import parser import logging import re import traceback import pytz -from openerp import api, fields, models, _ +from openerp.osv import orm, fields +from openerp import tools from openerp.tools.safe_eval import safe_eval +from openerp.tools.translate import _ from .aep import AccountingExpressionProcessor as AEP @@ -52,11 +55,14 @@ def _get_selection_label(selection, value): def _utc_midnight(d, tz_name, add_day=0): - d = fields.Datetime.from_string(d) + timedelta(days=add_day) + d = datetime.strptime(d, tools.DEFAULT_SERVER_DATE_FORMAT) utc_tz = pytz.timezone('UTC') + if add_day: + d = d + timedelta(days=add_day) context_tz = pytz.timezone(tz_name) local_timestamp = context_tz.localize(d, is_dst=False) - return fields.Datetime.to_string(local_timestamp.astimezone(utc_tz)) + return datetime.strftime(local_timestamp.astimezone(utc_tz), + tools.DEFAULT_SERVER_DATETIME_FORMAT) def _python_var(var_str): @@ -67,7 +73,7 @@ def _is_valid_python_var(name): return re.match("[_A-Za-z][_a-zA-Z0-9]*$", name) -class MisReportKpi(models.Model): +class MisReportKpi(orm.Model): """ A KPI is an element (ie a line) of a MIS report. In addition to a name and description, it has an expression @@ -80,141 +86,156 @@ class MisReportKpi(models.Model): _name = 'mis.report.kpi' - 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') + _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'), + } + + _defaults = { + 'type': 'num', + 'divider': '1', + 'dp': 0, + 'compare_method': 'pct', + 'sequence': 100, + } _order = 'sequence, id' - @api.one - @api.constrains('name') - def _check_name(self): - return _is_valid_python_var(self.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.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' - } - } + _constraints = [ + (_check_name, 'The name must be a valid python identifier', ['name']), + ] - @api.onchange('description') - def _onchange_description(self): + 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 + + def onchange_description(self, cr, uid, ids, description, name, + context=None): """ construct name from description """ - if self.description and not self.name: - self.name = _python_var(self.description) - - @api.onchange('type') - def _onchange_type(self): - if self.type == 'num': - self.compare_method = 'pct' - self.divider = '1' - self.dp = 0 - elif self.type == 'pct': - self.compare_method = 'diff' - self.divider = '1' - self.dp = 0 - 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 + 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 == 'num': + res['value'] = { + 'compare_method': 'pct', + 'divider': '1', + 'dp': 0 + } + elif kpi_type == 'pct': + res['value'] = { + 'compare_method': 'diff', + 'divider': '1', + 'dp': 0 + } + 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 value is None: return '#N/A' - 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, '%') + if 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) else: return unicode(value) - def render_comparison(self, lang_id, value, base_value, - average_value, average_base_value): + def _render_comparison(self, cr, uid, lang_id, kpi, value, base_value, + average_value, average_base_value, context=None): """ 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 self.type == 'pct': - return self._render_num( - lang_id, - value - base_value, - 0.01, self.dp, _('pp'), sign='+') - elif self.type == 'num': + if kpi.type == 'pct': + return self._render_num(cr, uid, lang_id, value - base_value, 0.01, + kpi.dp, _('pp'), sign='+', context=context) + elif kpi.type == 'num': if average_value: value = value / float(average_value) if average_base_value: base_value = base_value / float(average_base_value) - if self.compare_method == 'diff': - return self._render_num( - lang_id, - value - base_value, - self.divider, self.dp, self.suffix, sign='+') - elif self.compare_method == 'pct': - if round(base_value, self.dp) != 0: + if kpi.compare_method == 'diff': + return self._render_num(cr, uid, 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: return self._render_num( - lang_id, + cr, uid, lang_id, (value - base_value) / abs(base_value), - 0.01, self.dp, '%', sign='+') + 0.01, kpi.dp, '%', sign='+', context=context) return '' - def _render_num(self, lang_id, value, divider, - dp, suffix, sign='-'): + def _render_num(self, cr, uid, lang_id, value, divider, + dp, suffix, sign='-', context=None): 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.env['res.lang'].browse(lang_id).format( + value = self.pool['res.lang'].format( + cr, uid, lang_id, '%%%s.%df' % (sign, dp), value, - grouping=True) + grouping=True, + context=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 MisReportQuery(models.Model): +class MisReportQuery(orm.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. @@ -223,42 +244,66 @@ class MisReportQuery(models.Model): _name = 'mis.report.query' - @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') + 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'), + } _order = 'name' - @api.one - @api.constrains('name') - def _check_name(self): - return _is_valid_python_var(self.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 + + _constraints = [ + (_check_name, 'The name must be a valid python identifier', ['name']), + ] -class MisReport(models.Model): +class MisReport(orm.Model): """ A MIS report template (without period information) The MIS report holds: @@ -274,19 +319,44 @@ class MisReport(models.Model): _name = 'mis.report' - 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') - + _columns = { + 'name': fields.char(size=32, 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 + def create(self, cr, uid, vals, context=None): + # TODO: explain this + if 'kpi_ids' in vals: + mis_report_kpi_obj = self.pool.get('mis.report.kpi') + for idx, line in enumerate(vals['kpi_ids']): + if line[0] == 0: + line[2]['sequence'] = idx + 1 + else: + mis_report_kpi_obj.write( + cr, uid, [line[1]], {'sequence': idx + 1}, + context=context) + return super(MisReport, self).create(cr, uid, vals, context=context) + + def write(self, cr, uid, ids, vals, context=None): + # TODO: explain this + res = super(MisReport, self).write( + cr, uid, ids, vals, context=context) + mis_report_kpi_obj = self.pool.get('mis.report.kpi') + for report in self.browse(cr, uid, ids, context): + for idx, kpi in enumerate(report.kpi_ids): + mis_report_kpi_obj.write( + cr, uid, [kpi.id], {'sequence': idx + 1}, context=context) + return res + -class MisReportInstancePeriod(models.Model): +class MisReportInstancePeriod(orm.Model): """ A MIS report instance has the logic to compute a report template for a given date period. @@ -294,94 +364,118 @@ class MisReportInstancePeriod(models.Model): are defined as an offset relative to a pivot date. """ - @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( + 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): + period_ids = None + valid = True + 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) + 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) + elif c.type == 'fp': + period_obj = self.pool['account.period'] + current_period_ids = period_obj.search( + cr, uid, [('special', '=', False), - ('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 + ('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 + 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 _name = 'mis.report.instance.period' - 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) - + _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, + } _order = 'sequence, id' _sql_constraints = [ @@ -393,18 +487,18 @@ class MisReportInstancePeriod(models.Model): 'Period name should be unique by report'), ] - @api.multi - def drilldown(self, expr): - assert len(self) == 1 + def drilldown(self, cr, uid, _id, expr, context=None): + this = self.browse(cr, uid, _id, context=context) if AEP.has_account_var(expr): - aep = AEP(self.env) + aep = AEP(cr) aep.parse_expr(expr) - aep.done_parsing(self.report_instance_id.root_account) + aep.done_parsing(cr, uid, this.report_instance_id.root_account, + context=context) domain = aep.get_aml_domain_for_expr( expr, - self.date_from, self.date_to, - self.period_from, self.period_to, - self.report_instance_id.target_move) + this.date_from, this.date_to, + this.period_from, this.period_to, + this.report_instance_id.target_move) return { 'name': expr + ' - ' + self.name, 'domain': domain, @@ -418,41 +512,43 @@ class MisReportInstancePeriod(models.Model): else: return False - def _fetch_queries(self): - assert len(self) == 1 + def _fetch_queries(self, cr, uid, c, context): res = {} - for query in self.report_instance_id.report_id.query_ids: - model = self.env[query.model_id.model] + report = c.report_instance_id.report_id + for query in report.query_ids: + obj = self.pool[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, '>=', self.date_from), - (query.date_field.name, '<=', self.date_to)]) + domain.extend([(query.date_field.name, '>=', c.date_from), + (query.date_field.name, '<=', c.date_to)]) else: datetime_from = _utc_midnight( - self.date_from, self._context.get('tz', 'UTC')) + c.date_from, context.get('tz', 'UTC')) datetime_to = _utc_midnight( - self.date_to, self._context.get('tz', 'UTC'), add_day=1) + c.date_to, context.get('tz', 'UTC'), add_day=1) domain.extend([(query.date_field.name, '>=', datetime_from), (query.date_field.name, '<', datetime_to)]) - # TODO: we probably don't need company_id here - if model._columns.get('company_id'): + if obj._columns.get('company_id', False): domain.extend(['|', ('company_id', '=', False), - ('company_id', '=', - self.report_instance_id.company_id.id)]) + ('company_id', '=', c.company_id.id)]) field_names = [f.name for f in query.field_ids] if not query.aggregate: - data = model.search_read(domain, field_names) + obj_ids = obj.search(cr, uid, domain, context=context) + data = obj.read( + cr, uid, obj_ids, field_names, context=context) 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']) + data = obj.read_group( + cr, uid, domain, field_names, '', context=context) + 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) + obj_ids = obj.search(cr, uid, domain, context=context) + data = obj.read( + cr, uid, obj_ids, field_names, context=context) s = AutoStruct(count=len(data)) if query.aggregate == 'min': agg = min @@ -466,7 +562,12 @@ class MisReportInstancePeriod(models.Model): res[query.name] = s return res - def _compute(self, lang_id, aep): + def _compute(self, cr, uid, lang_id, c, aep, context=None): + if context is None: + context = {} + + kpi_obj = self.pool['mis.report.kpi'] + res = {} localdict = { @@ -478,13 +579,14 @@ class MisReportInstancePeriod(models.Model): 'avg': lambda l: sum(l) / float(len(l)), } - localdict.update(self._fetch_queries()) + localdict.update(self._fetch_queries(cr, uid, c, context=context)) - aep.do_queries(self.date_from, self.date_to, - self.period_from, self.period_to, - self.report_instance_id.target_move) + aep.do_queries(cr, uid, c.date_from, c.date_to, + c.period_from, c.period_to, + c.report_instance_id.target_move, + context=context) - compute_queue = self.report_instance_id.report_id.kpi_ids + compute_queue = c.report_instance_id.report_id.kpi_ids recompute_queue = [] while True: for kpi in compute_queue: @@ -506,7 +608,8 @@ class MisReportInstancePeriod(models.Model): kpi_val_rendered = '#ERR' kpi_val_comment += '\n\n%s' % (traceback.format_exc(),) else: - kpi_val_rendered = kpi.render(lang_id, kpi_val) + kpi_val_rendered = kpi_obj.render( + cr, uid, lang_id, kpi, kpi_val, context=context) localdict[kpi.name] = kpi_val try: @@ -526,10 +629,11 @@ class MisReportInstancePeriod(models.Model): 'val_r': kpi_val_rendered, 'val_c': kpi_val_comment, 'style': kpi_style, + 'default_style': kpi.default_css_style or None, 'suffix': kpi.suffix, 'dp': kpi.dp, 'is_percentage': kpi.type == 'pct', - 'period_id': self.id, + 'period_id': c.id, 'expr': kpi.expression, 'drilldown': drilldown, } @@ -549,95 +653,142 @@ class MisReportInstancePeriod(models.Model): return res -class MisReportInstance(models.Model): +class MisReportInstance(orm.Model): """The MIS report instance combines everything to compute a MIS report template for a set of periods.""" - @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) + def _compute_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 _name = 'mis.report.instance' + _columns = { + 'name': fields.char(size=32, 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(_compute_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.related('root_account', 'company_id', + type='many2one', relation='res.company', + string='Company', readonly=True, + store=True), + 'root_account': fields.many2one('account.account', + domain='[("parent_id", "=", False)]', + string="Account chart", + required=True), + 'landscape_pdf': fields.boolean(string='Landscape PDF') + } + + _defaults = { + 'target_move': 'posted', + } + + def create(self, cr, uid, vals, context=None): + if not vals: + return context.get('active_id', None) + # TODO: explain this + if 'period_ids' in vals: + mis_report_instance_period_obj = self.pool.get( + 'mis.report.instance.period') + for idx, line in enumerate(vals['period_ids']): + if line[0] == 0: + line[2]['sequence'] = idx + 1 + else: + mis_report_instance_period_obj.write( + cr, uid, [line[1]], {'sequence': idx + 1}, + context=context) + return super(MisReportInstance, self).create(cr, uid, vals, + context=context) + + def write(self, cr, uid, ids, vals, context=None): + # TODO: explain this + res = super(MisReportInstance, self).write( + cr, uid, ids, vals, context=context) + mis_report_instance_period_obj = self.pool.get( + 'mis.report.instance.period') + for instance in self.browse(cr, uid, ids, context): + for idx, period in enumerate(instance.period_ids): + mis_report_instance_period_obj.write( + cr, uid, [period.id], {'sequence': idx + 1}, + context=context) + return res - 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(comodel_name='res.company', - string='Company', - readonly=True, - related='root_account.company_id', - store=True) - root_account = fields.Many2one(comodel_name='account.account', - domain='[("parent_id", "=", False)]', - string="Account chart", - required=True) - landscape_pdf = fields.Boolean(string='Landscape PDF') - - def _format_date(self, lang_id, date): - # format date following user language - 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') + def preview(self, cr, uid, ids, context=None): + assert len(ids) == 1 + view_id = self.pool['ir.model.data'].get_object_reference( + cr, uid, 'mis_builder', + 'mis_report_instance_result_view_form')[1] return { 'type': 'ir.actions.act_window', 'res_model': 'mis.report.instance', - 'res_id': self.id, + 'res_id': ids[0], 'view_mode': 'form', 'view_type': 'form', - 'view_id': view_id.id, + 'view_id': view_id, 'target': 'new', } - @api.multi - def compute(self): - assert len(self) == 1 + def _format_date(self, cr, uid, lang_id, date, context=None): + # 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 compute(self, cr, uid, _id, context=None): + assert isinstance(_id, (int, long)) + if context is None: + context = {} + r = self.browse(cr, uid, _id, context=context) # prepare AccountingExpressionProcessor - aep = AEP(self.env) - for kpi in self.report_id.kpi_ids: + aep = AEP(cr) + for kpi in r.report_id.kpi_ids: aep.parse_expr(kpi.expression) - aep.done_parsing(self.root_account) + aep.done_parsing(cr, uid, r.root_account, context=context) + + report_instance_period_obj = self.pool['mis.report.instance.period'] + kpi_obj = self.pool.get('mis.report.kpi') # fetch user language only once # TODO: is this necessary? - lang = self.env.user.lang + lang = self.pool['res.users'].read( + cr, uid, uid, ['lang'], context=context)['lang'] if not lang: lang = 'en_US' - lang_id = self.env['res.lang'].search([('code', '=', lang)]).id + lang_id = self.pool['res.lang'].search( + cr, uid, [('code', '=', lang)], context=context) # compute kpi values for each period kpi_values_by_period_ids = {} - for period in self.period_ids: + for period in r.period_ids: if not period.valid: continue - kpi_values = period._compute(lang_id, aep) + kpi_values = report_instance_period_obj._compute( + cr, uid, lang_id, period, aep, context=context) kpi_values_by_period_ids[period.id] = kpi_values # prepare header and content @@ -648,7 +799,7 @@ class MisReportInstance(models.Model): }) content = [] rows_by_kpi_name = {} - for kpi in self.report_id.kpi_ids: + for kpi in r.report_id.kpi_ids: rows_by_kpi_name[kpi.name] = { 'kpi_name': kpi.description, 'cols': [], @@ -657,7 +808,7 @@ class MisReportInstance(models.Model): content.append(rows_by_kpi_name[kpi.name]) # populate header and content - for period in self.period_ids: + for period in r.period_ids: if not period.valid: continue # add the column header @@ -667,15 +818,18 @@ class MisReportInstance(models.Model): date_from = period.period_from.name date_to = period.period_to.name else: - date_from = self._format_date(lang_id, period.date_from) - date_to = self._format_date(lang_id, period.date_to) + date_from = self._format_date( + cr, uid, lang_id, period.date_from) + date_to = self._format_date( + cr, uid, lang_id, period.date_to) header_date = _('from %s to %s') % (date_from, date_to) else: # one period or one day if period.period_from and period.period_to: header_date = period.period_from.name else: - header_date = self._format_date(lang_id, period.date_from) + header_date = self._format_date( + cr, uid, lang_id, period.date_from) header[0]['cols'].append(dict(name=period.name, date=header_date)) # add kpi values kpi_values = kpi_values_by_period_ids[period.id] @@ -693,14 +847,18 @@ class MisReportInstance(models.Model): compare_col.name), date='')) # add comparison values - for kpi in self.report_id.kpi_ids: + for kpi in r.report_id.kpi_ids: rows_by_kpi_name[kpi.name]['cols'].append({ - 'val_r': kpi.render_comparison( + 'val_r': kpi_obj.render_comparison( + cr, + uid, lang_id, + kpi, kpi_values[kpi.name]['val'], compare_kpi_values[kpi.name]['val'], period.normalize_factor, - compare_col.normalize_factor) + compare_col.normalize_factor, + context=context) }) return {'header': header, diff --git a/mis_builder/report/__init__.py b/mis_builder/report/__init__.py index c5280449..4a6549d5 100644 --- a/mis_builder/report/__init__.py +++ b/mis_builder/report/__init__.py @@ -23,4 +23,3 @@ ############################################################################## from . import mis_builder_xls -from . import report_mis_report_instance diff --git a/mis_builder/report/report_mis_report_instance.py b/mis_builder/report/report_mis_report_instance.py deleted file mode 100644 index 72dc3b9d..00000000 --- a/mis_builder/report/report_mis_report_instance.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## - -import logging - -from openerp import api, models - -_logger = logging.getLogger(__name__) - - -class ReportMisReportInstance(models.AbstractModel): - - _name = 'report.mis_builder.report_mis_report_instance' - - @api.multi - def render_html(self, data=None): - docs = self.env['mis.report.instance'].browse(self._ids) - docs_computed = {} - for doc in docs: - docs_computed[doc.id] = doc.compute() - docargs = { - 'doc_ids': self._ids, - 'doc_model': 'mis.report.instance', - 'docs': docs, - 'docs_computed': docs_computed, - } - return self.env['report'].\ - render('mis_builder.report_mis_report_instance', docargs) - - -class Report(models.Model): - _inherit = "report" - - @api.v7 - def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, - context=None): - report = self._get_report_from_name(cr, uid, report_name) - obj = self.pool[report.model].browse(cr, uid, ids, - context=context)[0] - context = context.copy() - if hasattr(obj, 'landscape_pdf') and obj.landscape_pdf: - context.update({'landscape': True}) - return super(Report, self).get_pdf(cr, uid, ids, report_name, - html=html, data=data, - context=context) diff --git a/mis_builder/report/report_mis_report_instance.xml b/mis_builder/report/report_mis_report_instance.xml deleted file mode 100644 index 9d0a0dc7..00000000 --- a/mis_builder/report/report_mis_report_instance.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - diff --git a/mis_builder/tests/__init__.py b/mis_builder/tests/__init__.py index 42df0b2e..28bff250 100644 --- a/mis_builder/tests/__init__.py +++ b/mis_builder/tests/__init__.py @@ -23,3 +23,7 @@ ############################################################################## from . import test_mis_builder + +checks = [ + test_mis_builder, + ] diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 31551f10..2edb656b 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -2,13 +2,6 @@ - - mis.report.view.tree mis.report @@ -107,15 +100,6 @@ - - MIS report instance QWEB PDF report - mis.report.instance - ir.actions.report.xml - mis_builder.report_mis_report_instance - qweb-pdf - - - mis.report.instance.result.view.form mis.report.instance @@ -123,7 +107,6 @@
-