Browse Source

[IMP] mis_builder: new api

pull/86/head
Stéphane Bidoul 10 years ago
parent
commit
203891ddb5
  1. 718
      mis_builder/models/mis_builder.py
  2. 2
      mis_builder/report/report_mis_report_instance.py
  3. 10
      mis_builder/views/mis_builder.xml

718
mis_builder/models/mis_builder.py

@ -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.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.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),
tools.DEFAULT_SERVER_DATETIME_FORMAT)
return fields.Datetime.to_string(local_timestamp.astimezone(utc_tz))
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,
string='Name'),
'description': fields.char(required=True,
name = fields.Char(size=32, required=True,
string='Name')
description = fields.Char(required=True,
string='Description', 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')),
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')), ('pct', _('Percentage')),
('str', _('String'))], ('str', _('String'))],
required=True, required=True,
string='Type'),
'divider': fields.selection([('1e-6', _('µ')),
string='Type',
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'),
'dp': fields.integer(string='Rounding'),
'suffix': fields.char(size=16, string='Suffix'),
'compare_method': fields.selection([('diff', _('Difference')),
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')), ('pct', _('Percentage')),
('none', _('None'))], ('none', _('None'))],
required=True, 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,
}
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' _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
@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']),
]
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 """ """ 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 """ """ 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':
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: else:
return unicode(value) 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 """ """ 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='+',
context=context)
elif kpi.type == 'num':
0.01, self.dp, _('pp'), sign='+')
elif self.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='+',
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( 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='+',
context=context)
0.01, self.dp, '%', sign='+')
return '' 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( 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(
cr, uid, lang_id,
value = self.env.registry['res.lang'].format(
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):
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')),
@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')), ('avg', _('Average')),
('min', _('Min')), ('min', _('Min')),
('max', _('Max'))], ('max', _('Max'))],
string='Aggregate'),
'date_field': fields.many2one('ir.model.fields', required=True,
string='Aggregate')
date_field = fields.Many2one('ir.model.fields', required=True,
string='Date field', string='Date field',
domain=[('ttype', 'in', domain=[('ttype', 'in',
('date', 'datetime'))]),
'domain': fields.char(string='Domain'),
'report_id': fields.many2one('mis.report', string='Report',
ondelete='cascade'),
}
('date', 'datetime'))])
domain = fields.Char(string='Domain')
report_id = fields.Many2one('mis.report', string='Report',
ondelete='cascade')
_order = 'name' _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
_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)
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,
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 # 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):
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':
@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 = 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,
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), [('special', '=', False),
('date_start', '<=', d), ('date_start', '<=', d),
('date_stop', '>=', d), ('date_stop', '>=', d),
('company_id', '=', c.company_id.id)],
context=context)
if current_period_ids:
all_period_ids = period_obj.search(
cr, uid,
('company_id', '=',
self.report_instance_id.company_id.id)])
if current_periods:
all_periods = self.env['account.period'].search(
[('special', '=', False), [('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' _name = 'mis.report.instance.period'
_columns = {
'name': fields.char(size=32, required=True,
string='Description', translate=True),
'type': fields.selection([('d', _('Day')),
name = fields.Char(size=32, required=True,
string='Description', translate=True)
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'),
'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,
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', type='boolean',
multi='dates', string='Valid'),
'sequence': fields.integer(string='Sequence'),
'report_instance_id': fields.many2one('mis.report.instance',
string='Valid')
sequence = fields.Integer(string='Sequence', default=100)
report_instance_id = fields.Many2one('mis.report.instance',
string='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(
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', string='Factor',
help='Factor to use to normalize the period (used in comparison'),
}
_defaults = {
'offset': -1,
'duration': 1,
'sequence': 100,
'normalize_factor': 1,
}
help='Factor to use to normalize the period (used in comparison',
default=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):
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): if AEP.has_account_var(expr):
env = Environment(cr, uid, context)
aep = AEP(env)
aep = AEP(self.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,
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 { 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 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 [] 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),
(query.date_field.name, '<=', c.date_to)])
domain.extend([(query.date_field.name, '>=', self.date_from),
(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)
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': 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: 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)
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': 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):
if context is None:
context = {}
kpi_obj = self.pool['mis.report.kpi']
def _compute(self, lang_id, aep):
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,
c.period_from, c.period_to,
c.report_instance_id.target_move)
localdict.update(self._fetch_queries())
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(
cr, uid, lang_id, kpi, kpi_val, context=context)
kpi_val_rendered = kpi.render(lang_id, kpi_val)
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):
res = {}
for r in self.browse(cr, uid, ids, context=context):
if r.date:
res[r.id] = r.date
@api.one
@api.depends('date')
def _compute_pivot_date(self):
if self.date:
self.pivot_date = self.date
else: else:
res[r.id] = fields.date.context_today(self, cr, uid,
context=context)
return res
self.pivot_date = fields.Date.context_today(self)
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,
@api.one
@api.depends('company_id')
def _compute_root_account(self):
self.root_account = False
accounts = self.env['account.account'].search(
[('parent_id', '=', False), [('parent_id', '=', False),
('company_id', '=', r.company_id.id)],
context=context)
if len(account_ids) == 1:
res[r.id] = account_ids[0]
return res
('company_id', '=', self.company_id.id)])
if len(accounts) == 1:
self.root_account = accounts[0]
_name = 'mis.report.instance' _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',
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 ' 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',
'(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, required=True,
string='Report'),
'period_ids': fields.one2many('mis.report.instance.period',
string='Report')
period_ids = fields.One2many('mis.report.instance.period',
'report_instance_id', 'report_instance_id',
required=True, 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):
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 # 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 { 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
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 # 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.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: if not lang:
lang = 'en_US' 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 # 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(
cr, uid, lang_id, period, aep, context=context)
kpi_values = period._compute(lang_id, aep)
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,
context=context),
or self._format_date(lang_id, period.date_from),
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,
context=context)))
or self._format_date(lang_id, period.date_to)))
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:
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, 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,
context=context)})
compare_col.normalize_factor)
})
return {'header': header, return {'header': header,
'content': content} 'content': content}

2
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 = 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',

10
mis_builder/views/mis_builder.xml

@ -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"
on_change="onchange_field_ids(field_ids, context)"/>
<field name="field_ids" domain="[('model_id', '=', model_id)]" widget="many2many_tags"/>
<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="name" on_change="onchange_name(name, context)"/>
<field name="description"/>
<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>

Loading…
Cancel
Save