[IMP] mis_builder: new api

pull/86/head
Stéphane Bidoul 2015-06-07 13:51:24 +02:00
parent aeb1b48cdf
commit 203891ddb5
3 changed files with 338 additions and 450 deletions

View File

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

View File

@ -38,7 +38,7 @@ class ReportMisReportInstance(models.AbstractModel):
docs = self.env['mis.report.instance'].browse(self._ids) docs = self.env['mis.report.instance'].browse(self._ids)
docs_computed = {} docs_computed = {}
for doc in docs: for doc in docs:
docs_computed[doc.id] = doc.compute()[0] docs_computed[doc.id] = doc.compute()
docargs = { docargs = {
'doc_ids': self._ids, 'doc_ids': self._ids,
'doc_model': 'mis.report.instance', 'doc_model': 'mis.report.instance',

View File

@ -33,8 +33,7 @@
<tree string="Queries" editable="bottom"> <tree string="Queries" editable="bottom">
<field name="name"/> <field name="name"/>
<field name="model_id"/> <field name="model_id"/>
<field name="field_ids" domain="[('model_id', '=', model_id)]" widget="many2many_tags" <field name="field_ids" domain="[('model_id', '=', model_id)]" widget="many2many_tags"/>
on_change="onchange_field_ids(field_ids, context)"/>
<field name="field_names"/> <field name="field_names"/>
<field name="aggregate"/> <field name="aggregate"/>
<field name="date_field" domain="[('model_id', '=', model_id), ('ttype', 'in', ('date', 'datetime'))]"/> <field name="date_field" domain="[('model_id', '=', model_id), ('ttype', 'in', ('date', 'datetime'))]"/>
@ -44,10 +43,10 @@
<field name="kpi_ids"> <field name="kpi_ids">
<tree string="KPI's" editable="bottom"> <tree string="KPI's" editable="bottom">
<field name="sequence" widget="handle"/> <field name="sequence" widget="handle"/>
<field name="description" on_change="onchange_description(description, name, context)"/> <field name="description"/>
<field name="name" on_change="onchange_name(name, context)"/> <field name="name"/>
<field name="expression"/> <field name="expression"/>
<field name="type" on_change="onchange_type(type, context)"/> <field name="type"/>
<field name="dp" attrs="{'invisible': [('type', '=', 'str')]}"/> <field name="dp" attrs="{'invisible': [('type', '=', 'str')]}"/>
<field name="divider" attrs="{'invisible': [('type', '=', 'str')]}"/> <field name="divider" attrs="{'invisible': [('type', '=', 'str')]}"/>
<field name="suffix"/> <field name="suffix"/>
@ -190,7 +189,6 @@
<field name="report_instance_id" invisible="1"/> <field name="report_instance_id" invisible="1"/>
<field name="id" invisible="1"/> <field name="id" invisible="1"/>
<field name="comparison_column_ids" domain="[('report_instance_id', '=', report_instance_id), ('id', '!=', id)]" widget="many2many_tags"/> <field name="comparison_column_ids" domain="[('report_instance_id', '=', report_instance_id), ('id', '!=', id)]" widget="many2many_tags"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree> </tree>
</field> </field>
</group> </group>