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