Browse Source

[IMP] mis_builder: number format are now part of styles

Plus a default style at the report level.
Plus correct number rendering for comparisons in Excel export.
pull/189/head
Stéphane Bidoul 9 years ago
parent
commit
e8aa6dd2c8
  1. 9
      mis_builder/CHANGES.rst
  2. 170
      mis_builder/models/mis_report.py
  3. 173
      mis_builder/models/mis_report_style.py
  4. 22
      mis_builder/report/mis_report_instance_qweb.xml
  5. 37
      mis_builder/report/mis_report_instance_xlsx.py
  6. 4
      mis_builder/tests/mis.report.kpi.csv
  7. 1
      mis_builder/tests/test_fetch_query.py
  8. 167
      mis_builder/tests/test_render.py
  9. 58
      mis_builder/views/mis_builder_style.xml
  10. 22
      mis_builder/views/mis_report.xml
  11. 80
      mis_builder/views/mis_report_style.xml

9
mis_builder/CHANGES.rst

@ -3,8 +3,8 @@ Changelog
.. Future (?) .. Future (?)
.. ~~~~~~~~~~ .. ~~~~~~~~~~
..
.. *
..
.. *
9.0.1.0.0 (2016-??-??) 9.0.1.0.0 (2016-??-??)
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
@ -13,6 +13,9 @@ Part of the work for this release has been done at the Sorrento sprint
April 26-29, 2016. The rest (ie a major refactoring) has been done in April 26-29, 2016. The rest (ie a major refactoring) has been done in
the weeks after. the weeks after.
* [IMP] There is now a default style at the report level
* [CHG] Number display properties (rounding, prefix, suffix, factor) are
now defined in styles
* [CHG] Percentage difference are rounded to 1 digit instead of the kpi's * [CHG] Percentage difference are rounded to 1 digit instead of the kpi's
rounding, as the KPI rounding does not make sense in this case rounding, as the KPI rounding does not make sense in this case
* [CHG] The divider suffix (k, M, etc) is not inserted automatically anymore * [CHG] The divider suffix (k, M, etc) is not inserted automatically anymore
@ -42,7 +45,7 @@ the weeks after.
* [FIX] use =like instead of like to search for accounts, because * [FIX] use =like instead of like to search for accounts, because
the % are added by the user in the expressions the % are added by the user in the expressions
* [FIX] Correctly compute the initial balance of income and expense account * [FIX] Correctly compute the initial balance of income and expense account
based on the start of the fiscal year
based on the start of the fiscal year
* [IMP] Support date ranges (from OCA/server-tools/date_range) as a more * [IMP] Support date ranges (from OCA/server-tools/date_range) as a more
flexible alternative to fiscal periods flexible alternative to fiscal periods
* v9 migration: fiscal periods are removed, account charts are removed, * v9 migration: fiscal periods are removed, account charts are removed,

170
mis_builder/models/mis_report.py

@ -40,6 +40,14 @@ class KpiMatrixRow(object):
self.account_id = account_id self.account_id = account_id
self.comment = '' self.comment = ''
self.parent_row = parent_row self.parent_row = parent_row
if not self.account_id:
self.style_props = self._matrix._style_model.merge([
self.kpi.report_id.style_id,
self.kpi.style_id])
else:
self.style_props = self._matrix._style_model.merge([
self.kpi.report_id.style_id,
self.kpi.auto_expand_accounts_style_id])
@property @property
def description(self): def description(self):
@ -48,13 +56,6 @@ class KpiMatrixRow(object):
else: else:
return self._matrix.get_account_name(self.account_id) return self._matrix.get_account_name(self.account_id)
@property
def style(self):
if not self.account_id:
return self.kpi.style
else:
return self.kpi.auto_expand_accounts_style
@property @property
def row_id(self): def row_id(self):
if not self.account_id: if not self.account_id:
@ -134,13 +135,14 @@ class KpiMatrixCell(object):
def __init__(self, row, subcol, def __init__(self, row, subcol,
val, val_rendered, val_comment, val, val_rendered, val_comment,
style=None, drilldown_arg=None):
style_props,
drilldown_arg):
self.row = row self.row = row
self.subcol = subcol self.subcol = subcol
self.val = val self.val = val
self.val_rendered = val_rendered self.val_rendered = val_rendered
self.val_comment = val_comment self.val_comment = val_comment
self.style = style
self.style_props = style_props
self.drilldown_arg = drilldown_arg self.drilldown_arg = drilldown_arg
@ -151,6 +153,7 @@ class KpiMatrix(object):
lang_model = env['res.lang'] lang_model = env['res.lang']
lang_id = lang_model._lang_get(env.user.lang) lang_id = lang_model._lang_get(env.user.lang)
self.lang = lang_model.browse(lang_id) self.lang = lang_model.browse(lang_id)
self._style_model = env['mis.report.style']
self._account_model = env['account.account'] self._account_model = env['account.account']
# data structures # data structures
# { kpi: KpiMatrixRow } # { kpi: KpiMatrixRow }
@ -224,7 +227,7 @@ class KpiMatrix(object):
val_rendered = val.name val_rendered = val.name
val_comment = val.msg val_comment = val.msg
else: else:
val_rendered = kpi.render(self.lang, val)
val_rendered = kpi.render(self.lang, row.style_props, val)
if subcol.subkpi: if subcol.subkpi:
val_comment = u'{}.{} = {}'.format( val_comment = u'{}.{} = {}'.format(
row.kpi.name, row.kpi.name,
@ -234,9 +237,9 @@ class KpiMatrix(object):
val_comment = u'{} = {}'.format( val_comment = u'{} = {}'.format(
row.kpi.name, row.kpi.name,
row.kpi.expression) row.kpi.expression)
# TODO style
# TODO FIXME style expression
cell = KpiMatrixCell(row, subcol, val, val_rendered, val_comment, cell = KpiMatrixCell(row, subcol, val, val_rendered, val_comment,
None, drilldown_arg)
row.style_props, drilldown_arg)
cell_tuple.append(cell) cell_tuple.append(cell)
col._set_cell_tuple(row, cell_tuple) col._set_cell_tuple(row, cell_tuple)
@ -279,10 +282,11 @@ class KpiMatrix(object):
base_vals, base_vals,
comparison_col.iter_subcols()): comparison_col.iter_subcols()):
# TODO FIXME average factors # TODO FIXME average factors
delta, delta_r = row.kpi.compare_and_render(
self.lang, val, base_val, 1, 1)
delta, delta_r, style_r = row.kpi.compare_and_render(
self.lang, row.style_props, val, base_val, 1, 1)
comparison_cell_tuple.append(KpiMatrixCell( comparison_cell_tuple.append(KpiMatrixCell(
row, comparison_subcol, delta, delta_r, None))
row, comparison_subcol, delta, delta_r, None,
style_r, None))
comparison_col._set_cell_tuple(row, comparison_cell_tuple) comparison_col._set_cell_tuple(row, comparison_cell_tuple)
self._comparison_cols[pos_period_key].append(comparison_col) self._comparison_cols[pos_period_key].append(comparison_col)
@ -355,11 +359,13 @@ class KpiMatrix(object):
row.parent_row.row_id or None), row.parent_row.row_id or None),
'description': row.description, 'description': row.description,
'comment': row.comment, 'comment': row.comment,
'style': row.style and row.style.to_css_style() or None,
'style': self._style_model.to_css_style(
row.style_props),
'cols': [] 'cols': []
} }
for cell in row.iter_cells(): for cell in row.iter_cells():
if cell is None: if cell is None:
# TODO use subcol style here
row_data['cols'].append({}) row_data['cols'].append({})
else: else:
col_data = { col_data = {
@ -367,7 +373,8 @@ class KpiMatrix(object):
if cell.val is not AccountingNone else None), if cell.val is not AccountingNone else None),
'val_r': cell.val_rendered, 'val_r': cell.val_rendered,
'val_c': cell.val_comment, 'val_c': cell.val_comment,
# TODO FIXME style
'style': self._style_model.to_css_style(
cell.style_props),
} }
if cell.drilldown_arg: if cell.drilldown_arg:
col_data['drilldown_arg'] = cell.drilldown_arg col_data['drilldown_arg'] = cell.drilldown_arg
@ -380,13 +387,6 @@ class KpiMatrix(object):
} }
def _get_selection_label(selection, value):
for v, l in selection:
if v == value:
return l
return ''
def _utc_midnight(d, tz_name, add_day=0): def _utc_midnight(d, tz_name, add_day=0):
d = fields.Datetime.from_string(d) + datetime.timedelta(days=add_day) d = fields.Datetime.from_string(d) + datetime.timedelta(days=add_day)
utc_tz = pytz.timezone('UTC') utc_tz = pytz.timezone('UTC')
@ -427,13 +427,13 @@ class MisReportKpi(models.Model):
inverse='_inverse_expression') inverse='_inverse_expression')
expression_ids = fields.One2many('mis.report.kpi.expression', 'kpi_id') expression_ids = fields.One2many('mis.report.kpi.expression', 'kpi_id')
auto_expand_accounts = fields.Boolean(string='Display details by account') auto_expand_accounts = fields.Boolean(string='Display details by account')
auto_expand_accounts_style = fields.Many2one(
auto_expand_accounts_style_id = fields.Many2one(
string="Style for account detail rows", string="Style for account detail rows",
comodel_name="mis.report.style", comodel_name="mis.report.style",
required=False required=False
) )
style = fields.Many2one(
string="Row style",
style_id = fields.Many2one(
string="Style",
comodel_name="mis.report.style", comodel_name="mis.report.style",
required=False required=False
) )
@ -445,18 +445,8 @@ class MisReportKpi(models.Model):
('pct', _('Percentage')), ('pct', _('Percentage')),
('str', _('String'))], ('str', _('String'))],
required=True, required=True,
string='Type',
string='Value type',
default='num') default='num')
divider = fields.Selection([('1e-6', _('µ')),
('1e-3', _('m')),
('1', _('1')),
('1e3', _('k')),
('1e6', _('M'))],
string='Factor',
default='1')
dp = fields.Integer(string='Rounding', default=0)
prefix = fields.Char(size=16, string='Prefix')
suffix = fields.Char(size=16, string='Suffix')
compare_method = fields.Selection([('diff', _('Difference')), compare_method = fields.Selection([('diff', _('Difference')),
('pct', _('Percentage')), ('pct', _('Percentage')),
('none', _('None'))], ('none', _('None'))],
@ -544,57 +534,57 @@ class MisReportKpi(models.Model):
def _onchange_type(self): def _onchange_type(self):
if self.type == 'num': if self.type == 'num':
self.compare_method = 'pct' self.compare_method = 'pct'
self.divider = '1'
self.dp = 0
elif self.type == 'pct': elif self.type == 'pct':
self.compare_method = 'diff' self.compare_method = 'diff'
self.divider = '1'
self.dp = 0
elif self.type == 'str': elif self.type == 'str':
self.compare_method = 'none' self.compare_method = 'none'
self.divider = ''
self.dp = 0
def get_expression_for_subkpi(self, subkpi): def get_expression_for_subkpi(self, subkpi):
for expression in self.expression_ids: for expression in self.expression_ids:
if expression.subkpi_id == subkpi: if expression.subkpi_id == subkpi:
return expression.name return expression.name
def render(self, lang, value):
@api.multi
def render(self, lang, style_props, 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 or value is AccountingNone:
return ''
elif self.type == 'num':
return self._render_num(lang, value, self.divider,
self.dp, self.prefix, self.suffix)
self.ensure_one()
style_obj = self.env['mis.report.style']
if self.type == 'num':
return style_obj.render_num(lang, value, style_props.divider,
style_props.dp,
style_props.prefix, style_props.suffix)
elif self.type == 'pct': elif self.type == 'pct':
return self._render_num(lang, value, 0.01,
self.dp, '', '%')
return style_obj.render_pct(lang, value, style_props.dp)
else: else:
return unicode(value)
return style_obj.render_str(lang, value)
def compare_and_render(self, lang, value, base_value,
@api.multi
def compare_and_render(self, lang, style_props, value, base_value,
average_value=1, average_base_value=1): average_value=1, average_base_value=1):
""" render the comparison of two KPI values, ready for display """ render the comparison of two KPI values, ready for display
Returns a tuple, with the numeric comparison and its string rendering.
Returns a triple, with
* the numeric comparison
* its string rendering
* the update style properties
If the difference is 0, an empty string is returned. If the difference is 0, an empty string is returned.
""" """
assert len(self) == 1
self.ensure_one()
style_obj = self.env['mis.report.style']
delta = AccountingNone
style_r = style_props.copy()
if value is None: if value is None:
value = AccountingNone value = AccountingNone
if base_value is None: if base_value is None:
base_value = AccountingNone base_value = AccountingNone
if self.type == 'pct': if self.type == 'pct':
delta = value - base_value delta = value - base_value
if delta and round(delta, self.dp + 2) != 0:
return delta, self._render_num(
lang,
delta,
0.01, self.dp, '', _('pp'),
sign='+')
if delta and round(delta, (style_props.dp or 0) + 2) != 0:
style_r.update(dict(
divider=0.01, prefix='', suffix=_('pp')))
else:
delta = AccountingNone
elif self.type == 'num': elif self.type == 'num':
if value and average_value: if value and average_value:
value = value / float(average_value) value = value / float(average_value)
@ -602,43 +592,27 @@ class MisReportKpi(models.Model):
base_value = base_value / float(average_base_value) base_value = base_value / float(average_base_value)
if self.compare_method == 'diff': if self.compare_method == 'diff':
delta = value - base_value delta = value - base_value
if delta and round(delta, self.dp) != 0:
return delta, self._render_num(
lang,
delta,
self.divider, self.dp, self.prefix, self.suffix,
sign='+')
if delta and round(delta, style_props.dp or 0) != 0:
pass
else:
delta = AccountingNone
elif self.compare_method == 'pct': elif self.compare_method == 'pct':
if base_value and round(base_value, self.dp) != 0:
if base_value and round(base_value, style_props.dp or 0) != 0:
delta = (value - base_value) / abs(base_value) delta = (value - base_value) / abs(base_value)
if delta and round(delta, self.dp) != 0:
return delta, self._render_num(
lang,
delta,
0.01, 1, '', '%',
sign='+')
else:
return AccountingNone, ''
return 0, ''
def _render_num(self, lang, value, divider,
dp, prefix, suffix, sign='-'):
# format number following user language
value = round(value / float(divider or 1), dp) or 0
value = lang.format(
'%%%s.%df' % (sign, dp),
value,
grouping=True)
value = value.replace('-', u'\N{NON-BREAKING HYPHEN}')
if prefix:
prefix = prefix + u'\N{NO-BREAK SPACE}'
else:
prefix = ''
if suffix:
suffix = u'\N{NO-BREAK SPACE}' + suffix
if delta and round(delta, 1) != 0:
style_r.update(dict(
divider=0.01, dp=1, prefix='', suffix='%'))
else:
delta = AccountingNone
if delta is not AccountingNone:
delta_r = style_obj.render_num(
lang, delta,
style_r.divider, style_r.dp,
style_r.prefix, style_r.suffix,
sign='+')
return delta, delta_r, style_r
else: else:
suffix = ''
return prefix + value + suffix
return AccountingNone, '', style_r
class MisReportSubkpi(models.Model): class MisReportSubkpi(models.Model):
@ -773,6 +747,8 @@ class MisReport(models.Model):
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)
style_id = fields.Many2one(string="Style",
comodel_name="mis.report.style")
query_ids = fields.One2many('mis.report.query', 'report_id', query_ids = fields.One2many('mis.report.query', 'report_id',
string='Queries', string='Queries',
copy=True) copy=True)

173
mis_builder/models/mis_report_style.py

@ -3,16 +3,45 @@
# © 2016 ACSONE SA/NV (<http://acsone.eu>) # © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models
from openerp import api, fields, models, _
from openerp.exceptions import UserError
from .accounting_none import AccountingNone
class PropertyDict(dict):
def __getattr__(self, name):
return self.get(name)
def copy(self):
return PropertyDict(self)
PROPS = [
'color',
'background_color',
'font_style',
'font_weight',
'font_size',
'indent_level',
'prefix',
'suffix',
'dp',
'divider',
]
class MisReportKpiStyle(models.Model): class MisReportKpiStyle(models.Model):
_name = 'mis.report.style' _name = 'mis.report.style'
@api.depends('indent_level')
@api.one
@api.constrains('indent_level')
def check_positive_val(self): def check_positive_val(self):
return self.indent_level > 0
if self.indent_level < 0:
raise UserError(_('Indent level must be greater than '
'or equal to 0'))
_font_style_selection = [ _font_style_selection = [
('normal', 'Normal'), ('normal', 'Normal'),
@ -25,7 +54,7 @@ class MisReportKpiStyle(models.Model):
] ]
_font_size_selection = [ _font_size_selection = [
('medium', ''),
('medium', 'medium'),
('xx-small', 'xx-small'), ('xx-small', 'xx-small'),
('x-small', 'x-small'), ('x-small', 'x-small'),
('small', 'small'), ('small', 'small'),
@ -34,7 +63,7 @@ class MisReportKpiStyle(models.Model):
('xx-large', 'xx-large'), ('xx-large', 'xx-large'),
] ]
_font_size_to_size = {
_font_size_to_xlsx_size = {
'medium': 11, 'medium': 11,
'xx-small': 5, 'xx-small': 5,
'x-small': 7, 'x-small': 7,
@ -44,57 +73,129 @@ class MisReportKpiStyle(models.Model):
'xx-large': 17 'xx-large': 17
} }
# style name
# TODO enforce uniqueness
name = fields.Char(string='Style name', required=True) name = fields.Char(string='Style name', required=True)
# color
color_inherit = fields.Boolean(default=True)
color = fields.Char( color = fields.Char(
string='Text color', string='Text color',
help='Text color in valid RGB code (from #000000 to #FFFFFF)', help='Text color in valid RGB code (from #000000 to #FFFFFF)',
) )
background_color_inherit = fields.Boolean(default=True)
background_color = fields.Char( background_color = fields.Char(
help='Background color in valid RGB code (from #000000 to #FFFFFF)' help='Background color in valid RGB code (from #000000 to #FFFFFF)'
) )
# font
font_style_inherit = fields.Boolean(default=True)
font_style = fields.Selection( font_style = fields.Selection(
selection=_font_style_selection, selection=_font_style_selection,
) )
font_weight_inherit = fields.Boolean(default=True)
font_weight = fields.Selection( font_weight = fields.Selection(
selection=_font_weight_selection selection=_font_weight_selection
) )
font_size_inherit = fields.Boolean(default=True)
font_size = fields.Selection( font_size = fields.Selection(
selection=_font_size_selection selection=_font_size_selection
) )
# indent
indent_level_inherit = fields.Boolean(default=True)
indent_level = fields.Integer() indent_level = fields.Integer()
# number format
prefix_inherit = fields.Boolean(default=True)
prefix = fields.Char(size=16, string='Prefix')
suffix_inherit = fields.Boolean(default=True)
suffix = fields.Char(size=16, string='Suffix')
dp_inherit = fields.Boolean(default=True)
dp = fields.Integer(string='Rounding', default=0)
divider_inherit = fields.Boolean(default=True)
divider = fields.Selection([('1e-6', _('µ')),
('1e-3', _('m')),
('1', _('1')),
('1e3', _('k')),
('1e6', _('M'))],
string='Factor',
default='1')
@api.multi
def font_size_to_size(self):
self.ensure_one()
return self._font_size_to_size.get(self.font_size, 11)
@api.multi
def to_xlsx_format_properties(self):
self.ensure_one()
props = {
'italic': self.font_style == 'italic',
'bold': self.font_weight == 'bold',
'font_color': self.color,
'bg_color': self.background_color,
'indent': self.indent_level,
'size': self.font_size_to_size()
}
return props
@api.multi
def to_css_style(self):
self.ensure_one()
css_attributes = [
('font-style', self.font_style),
('font-weight', self.font_weight),
('font-size', self.font_size),
('color', self.color),
('background-color', self.background_color),
('indent-level', self.indent_level)
@api.model
def merge(self, styles):
r = PropertyDict()
for style in styles:
if not style:
continue
if isinstance(style, dict):
r.update(style)
else:
for prop in PROPS:
inherit = getattr(style, prop + '_inherit', None)
if inherit is None:
value = getattr(style, prop)
if value:
r[prop] = value
elif not inherit:
value = getattr(style, prop)
r[prop] = value
return r
@api.model
def render_num(self, lang, value,
divider=1.0, dp=0, prefix=None, suffix=None, sign='-'):
# format number following user language
if value is None or value is AccountingNone:
return u''
value = round(value / float(divider or 1), dp or 0) or 0
r = lang.format('%%%s.%df' % (sign, dp or 0), value, grouping=True)
r = r.replace('-', u'\N{NON-BREAKING HYPHEN}')
if prefix:
r = prefix + u'\N{NO-BREAK SPACE}' + r
if suffix:
r = r + u'\N{NO-BREAK SPACE}' + suffix
return r
@api.model
def render_pct(self, lang, value, dp=1, sign='-'):
return self.render_num(lang, value, divider=0.01,
dp=dp, suffix='%', sign=sign)
@api.model
def render_str(self, lang, value):
if value is None or value is AccountingNone:
return u''
return unicode(value)
@api.model
def to_xlsx_style(self, props):
num_format = '0.'
if props.dp:
num_format += '0' * props.dp
if props.prefix:
num_format = u'"{} "{}'.format(props.prefix, num_format)
if props.suffix:
num_format = u'{}" {}"'.format(num_format, props.suffix)
xlsx_attributes = [
('italic', props.font_style == 'italic'),
('bold', props.font_weight == 'bold'),
('size', self._font_size_to_xlsx_size.get(props.font_size, 11)),
('font_color', props.color),
('bg_color', props.background_color),
('indent', props.indent_level),
('num_format', num_format),
] ]
return dict([a for a in xlsx_attributes
if a[1] is not None])
css_list = [
'%s:%s' % x for x in css_attributes if x[1]
@api.model
def to_css_style(self, props):
css_attributes = [
('font-style', props.font_style),
('font-weight', props.font_weight),
('font-size', props.font_size),
('color', props.color),
('background-color', props.background_color),
('indent-level', props.indent_level)
] ]
return ';'.join(css_item for css_item in css_list)
return '; '.join(['%s: %s' % a for a in css_attributes
if a[1] is not None]) or None

22
mis_builder/report/mis_report_instance_qweb.xml

@ -17,11 +17,18 @@
</xpath> </xpath>
</template> </template>
<!--
TODO we use divs with css table layout, but this has drawbacks:
(bad layout of first column, no colspan for first header row),
consider getting back to a plain HTML table.
-->
<template id="report_mis_report_instance"> <template id="report_mis_report_instance">
<t t-call="report.html_container"> <t t-call="report.html_container">
<t t-foreach="docs" t-as="o"> <t t-foreach="docs" t-as="o">
<t t-call="report.internal_layout"> <t t-call="report.internal_layout">
<t t-set="matrix" t-value="o._compute_matrix()"/> <t t-set="matrix" t-value="o._compute_matrix()"/>
<t t-set="style_obj" t-value="o.env['mis.report.style']"/>
<div class="page"> <div class="page">
<h2><span t-field="o.name" /> - <span t-field="o.company_id.name" /></h2> <h2><span t-field="o.name" /> - <span t-field="o.company_id.name" /></h2>
<div class="mis_table"> <div class="mis_table">
@ -57,7 +64,7 @@
</div> </div>
<div class="mis_tbody"> <div class="mis_tbody">
<div t-foreach="matrix.iter_rows()" t-as="row" class="mis_row"> <div t-foreach="matrix.iter_rows()" t-as="row" class="mis_row">
<div t-att-style="row.style and row.style.to_css_style() or ''" class="mis_cell mis_rowlabel">
<div t-att-style="style_obj.to_css_style(row.style_props)" class="mis_cell mis_rowlabel">
<t t-esc="row.description"/> <t t-esc="row.description"/>
<t t-if="row.comment"> <t t-if="row.comment">
<br/> <br/>
@ -65,16 +72,9 @@
</t> </t>
</div> </div>
<t t-foreach="row.iter_cells()" t-as="cell"> <t t-foreach="row.iter_cells()" t-as="cell">
<t t-if="cell">
<div t-att-style="cell.row.style and cell.row.style.to_css_style() or ''" class="mis_cell mis_amount">
<div t-att-style="cell.style and cell.style.to_css_style() or ''">
<t t-esc="cell.val_rendered"/>
</div>
</div>
</t>
<t t-if="not cell">
<div class="mis_cell mis_amount"></div>
</t>
<div t-att-style="cell and style_obj.to_css_style(cell.style_props) or ''" class="mis_cell mis_amount">
<t t-esc="cell and cell.val_rendered or ''"/>
</div>
</t> </t>
</div> </div>
</div> </div>

37
mis_builder/report/mis_report_instance_xlsx.py

@ -14,7 +14,7 @@ _logger = logging.getLogger(__name__)
try: try:
from openerp.addons.report_xlsx.report.report_xlsx import ReportXlsx from openerp.addons.report_xlsx.report.report_xlsx import ReportXlsx
except ImportError: except ImportError:
_logger.debug("report_xslx not installed, Excel export non functional")
_logger.debug("report_xlsx not installed, Excel export non functional")
class ReportXslx: class ReportXslx:
pass pass
@ -33,23 +33,11 @@ class MisBuilderXslx(ReportXlsx):
super(MisBuilderXslx, self).__init__( super(MisBuilderXslx, self).__init__(
name, table, rml, parser, header, store) name, table, rml, parser, header, store)
def make_number_format(self, kpi, comparison=False):
# TODO FIXME comparison
number_format = '#'
if kpi.dp:
number_format += '.'
number_format += '0' * kpi.dp
# TODO FIXME factor
if kpi.prefix:
number_format = u'"{} "{}'.format(kpi.prefix, number_format)
if kpi.suffix:
number_format = u'{}" {}"'.format(number_format, kpi.suffix)
return number_format
def generate_xlsx_report(self, workbook, data, objects): def generate_xlsx_report(self, workbook, data, objects):
# get the computed result of the report # get the computed result of the report
matrix = objects._compute_matrix() matrix = objects._compute_matrix()
style_obj = self.env['mis.report.style']
# create worksheet # create worksheet
report_name = '{} - {}'.format( report_name = '{} - {}'.format(
@ -107,10 +95,7 @@ class MisBuilderXslx(ReportXlsx):
# rows # rows
for row in matrix.iter_rows(): for row in matrix.iter_rows():
if row.style:
row_xlsx_style = row.style.to_xlsx_format_properties()
else:
row_xlsx_style = {}
row_xlsx_style = style_obj.to_xlsx_style(row.style_props)
row_format = workbook.add_format(row_xlsx_style) row_format = workbook.add_format(row_xlsx_style)
col_pos = 0 col_pos = 0
sheet.write(row_pos, col_pos, row.description, row_format) sheet.write(row_pos, col_pos, row.description, row_format)
@ -118,19 +103,13 @@ class MisBuilderXslx(ReportXlsx):
for cell in row.iter_cells(): for cell in row.iter_cells():
col_pos += 1 col_pos += 1
if not cell or cell.val is AccountingNone: if not cell or cell.val is AccountingNone:
# TODO col/subcol format
sheet.write(row_pos, col_pos, '', row_format) sheet.write(row_pos, col_pos, '', row_format)
continue continue
kpi_xlsx_style = dict(row_xlsx_style)
kpi_xlsx_style.update({
'num_format': self.make_number_format(row.kpi),
'align': 'right'
})
kpi_format = workbook.add_format(kpi_xlsx_style)
# TODO FIXME kpi computed style
# TODO FIXME pct in comparision columns
val = cell.val
if row.kpi.type == 'pct':
val = val / 0.01
cell_xlsx_style = style_obj.to_xlsx_style(cell.style_props)
cell_xlsx_style['align'] = 'right'
kpi_format = workbook.add_format(cell_xlsx_style)
val = cell.val / float(cell.style_props.get('divider', 1))
sheet.write(row_pos, col_pos, val, kpi_format) sheet.write(row_pos, col_pos, val, kpi_format)
col_width[col_pos] = max(col_width[col_pos], col_width[col_pos] = max(col_width[col_pos],
len(cell.val_rendered or '')) len(cell.val_rendered or ''))

4
mis_builder/tests/mis.report.kpi.csv

@ -1,2 +1,2 @@
"id","compare_method","description","expression","divider","name","dp","sequence","type","suffix"
"mis_report_kpi_test","Percentage","total test","len(test)","","total_test","","1","Numeric",""
"id","description","expression","name"
"mis_report_kpi_test","total test","len(test)","total_test"

1
mis_builder/tests/test_fetch_query.py

@ -23,6 +23,7 @@ class TestFetchQuery(common.TransactionCase):
'cols': [{'val': 0, 'cols': [{'val': 0,
'val_r': u'0', 'val_r': u'0',
'val_c': u'total_test = len(test)', 'val_c': u'total_test = len(test)',
'style': None,
}] }]
}], }],
'header': 'header':

167
mis_builder/tests/test_render.py

@ -11,132 +11,147 @@ class TestRendering(common.TransactionCase):
def setUp(self): def setUp(self):
super(TestRendering, self).setUp() super(TestRendering, self).setUp()
self.kpi = self.env['mis.report.kpi'].create(dict(
self.style_obj = self.env['mis.report.style']
self.kpi_obj = self.env['mis.report.kpi']
self.style = self.style_obj.create(dict(
name='teststyle',
))
self.kpi = self.kpi_obj.create(dict(
name='testkpi', name='testkpi',
description='Test KPI',
description='test kpi',
type='num', type='num',
dp=0,
style_id=self.style.id,
)) ))
self.lang = self.env['res.lang'].search([('code', '=', 'en_US')])[0] self.lang = self.env['res.lang'].search([('code', '=', 'en_US')])[0]
def _render(self, value):
style_props = self.style_obj.merge([self.style])
return self.kpi.render(self.lang, style_props, value)
def _compare_and_render(self, value, base_value):
style_props = self.style_obj.merge([self.style])
return self.kpi.compare_and_render(self.lang, style_props,
value, base_value)[:2]
def test_render(self): def test_render(self):
self.assertEquals(u'1', self.kpi.render(self.lang, 1))
self.assertEquals(u'1', self.kpi.render(self.lang, 1.1))
self.assertEquals(u'2', self.kpi.render(self.lang, 1.6))
self.kpi.dp = 2
self.assertEquals(u'1.00', self.kpi.render(self.lang, 1))
self.assertEquals(u'1.10', self.kpi.render(self.lang, 1.1))
self.assertEquals(u'1.60', self.kpi.render(self.lang, 1.6))
self.assertEquals(u'1.61', self.kpi.render(self.lang, 1.606))
self.assertEquals(u'12,345.67', self.kpi.render(self.lang, 12345.67))
self.assertEquals(u'1', self._render(1))
self.assertEquals(u'1', self._render(1.1))
self.assertEquals(u'2', self._render(1.6))
self.style.dp_inherit = False
self.style.dp = 2
self.assertEquals(u'1.00', self._render(1))
self.assertEquals(u'1.10', self._render(1.1))
self.assertEquals(u'1.60', self._render(1.6))
self.assertEquals(u'1.61', self._render(1.606))
self.assertEquals(u'12,345.67', self._render(12345.67))
def test_render_negative(self): def test_render_negative(self):
# non breaking hyphen # non breaking hyphen
self.assertEquals(u'\u20111', self.kpi.render(self.lang, -1))
self.assertEquals(u'\u20111', self._render(-1))
def test_render_zero(self): def test_render_zero(self):
self.assertEquals(u'0', self.kpi.render(self.lang, 0))
self.assertEquals(u'', self.kpi.render(self.lang, None))
self.assertEquals(u'', self.kpi.render(self.lang, AccountingNone))
self.assertEquals(u'0', self._render(0))
self.assertEquals(u'', self._render(None))
self.assertEquals(u'', self._render(AccountingNone))
def test_render_suffix(self): def test_render_suffix(self):
self.kpi.suffix = u''
self.assertEquals(u'1\xa0', self.kpi.render(self.lang, 1))
self.kpi.suffix = u'k€'
self.kpi.divider = '1e3'
self.assertEquals(u'1\xa0k€', self.kpi.render(self.lang, 1000))
self.style.suffix_inherit = False
self.style.suffix = u''
self.assertEquals(u'1\xa0', self._render(1))
self.style.suffix = u'k€'
self.style.divider_inherit = False
self.style.divider = '1e3'
self.assertEquals(u'1\xa0k€', self._render(1000))
def test_render_prefix(self): def test_render_prefix(self):
self.kpi.prefix = u'$'
self.assertEquals(u'$\xa01', self.kpi.render(self.lang, 1))
self.kpi.prefix = u'k$'
self.kpi.divider = '1e3'
self.assertEquals(u'k$\xa01', self.kpi.render(self.lang, 1000))
self.style.prefix_inherit = False
self.style.prefix = u'$'
self.assertEquals(u'$\xa01', self._render(1))
self.style.prefix = u'k$'
self.style.divider_inherit = False
self.style.divider = '1e3'
self.assertEquals(u'k$\xa01', self._render(1000))
def test_render_divider(self): def test_render_divider(self):
self.kpi.divider = '1e3'
self.kpi.dp = 0
self.assertEquals(u'1', self.kpi.render(self.lang, 1000))
self.kpi.divider = '1e6'
self.kpi.dp = 3
self.assertEquals(u'0.001', self.kpi.render(self.lang, 1000))
self.kpi.divider = '1e-3'
self.kpi.dp = 0
self.assertEquals(u'1,000', self.kpi.render(self.lang, 1))
self.kpi.divider = '1e-6'
self.kpi.dp = 0
self.assertEquals(u'1,000,000', self.kpi.render(self.lang, 1))
self.style.divider_inherit = False
self.style.divider = '1e3'
self.style.dp_inherit = False
self.style.dp = 0
self.assertEquals(u'1', self._render(1000))
self.style.divider = '1e6'
self.style.dp = 3
self.assertEquals(u'0.001', self._render(1000))
self.style.divider = '1e-3'
self.style.dp = 0
self.assertEquals(u'1,000', self._render(1))
self.style.divider = '1e-6'
self.style.dp = 0
self.assertEquals(u'1,000,000', self._render(1))
def test_render_pct(self): def test_render_pct(self):
self.kpi.type = 'pct' self.kpi.type = 'pct'
self.assertEquals(u'100\xa0%', self.kpi.render(self.lang, 1))
self.assertEquals(u'50\xa0%', self.kpi.render(self.lang, 0.5))
self.kpi.dp = 2
self.assertEquals(u'51.23\xa0%', self.kpi.render(self.lang, 0.5123))
self.assertEquals(u'100\xa0%', self._render(1))
self.assertEquals(u'50\xa0%', self._render(0.5))
self.style.dp_inherit = False
self.style.dp = 2
self.assertEquals(u'51.23\xa0%', self._render(0.5123))
def test_render_string(self): def test_render_string(self):
self.kpi.type = 'str' self.kpi.type = 'str'
self.assertEquals(u'', self.kpi.render(self.lang, ''))
self.assertEquals(u'', self.kpi.render(self.lang, None))
self.assertEquals(u'abcdé', self.kpi.render(self.lang, u'abcdé'))
self.assertEquals(u'', self._render(''))
self.assertEquals(u'', self._render(None))
self.assertEquals(u'abcdé', self._render(u'abcdé'))
def test_compare_num_pct(self): def test_compare_num_pct(self):
self.assertEquals('pct', self.kpi.compare_method) self.assertEquals('pct', self.kpi.compare_method)
self.assertEquals((1.0, u'+100.0\xa0%'), self.assertEquals((1.0, u'+100.0\xa0%'),
self.kpi.compare_and_render(self.lang, 100, 50))
self._compare_and_render(100, 50))
self.assertEquals((0.5, u'+50.0\xa0%'), self.assertEquals((0.5, u'+50.0\xa0%'),
self.kpi.compare_and_render(self.lang, 75, 50))
self._compare_and_render(75, 50))
self.assertEquals((0.5, u'+50.0\xa0%'), self.assertEquals((0.5, u'+50.0\xa0%'),
self.kpi.compare_and_render(self.lang, -25, -50))
self._compare_and_render(-25, -50))
self.assertEquals((1.0, u'+100.0\xa0%'), self.assertEquals((1.0, u'+100.0\xa0%'),
self.kpi.compare_and_render(self.lang, 0, -50))
self._compare_and_render(0, -50))
self.assertEquals((2.0, u'+200.0\xa0%'), self.assertEquals((2.0, u'+200.0\xa0%'),
self.kpi.compare_and_render(self.lang, 50, -50))
self._compare_and_render(50, -50))
self.assertEquals((-0.5, u'\u201150.0\xa0%'), self.assertEquals((-0.5, u'\u201150.0\xa0%'),
self.kpi.compare_and_render(self.lang, 25, 50))
self._compare_and_render(25, 50))
self.assertEquals((-1.0, u'\u2011100.0\xa0%'), self.assertEquals((-1.0, u'\u2011100.0\xa0%'),
self.kpi.compare_and_render(self.lang, 0, 50))
self._compare_and_render(0, 50))
self.assertEquals((-2.0, u'\u2011200.0\xa0%'), self.assertEquals((-2.0, u'\u2011200.0\xa0%'),
self.kpi.compare_and_render(self.lang, -50, 50))
self._compare_and_render(-50, 50))
self.assertEquals((-0.5, u'\u201150.0\xa0%'), self.assertEquals((-0.5, u'\u201150.0\xa0%'),
self.kpi.compare_and_render(self.lang, -75, -50))
self._compare_and_render(-75, -50))
self.assertEquals((AccountingNone, u''), self.assertEquals((AccountingNone, u''),
self.kpi.compare_and_render(
self.lang, 50, AccountingNone))
self._compare_and_render(50, AccountingNone))
self.assertEquals((AccountingNone, u''), self.assertEquals((AccountingNone, u''),
self.kpi.compare_and_render(
self.lang, 50, None))
self._compare_and_render(50, None))
self.assertEquals((-1.0, u'\u2011100.0\xa0%'), self.assertEquals((-1.0, u'\u2011100.0\xa0%'),
self.kpi.compare_and_render(
self.lang, AccountingNone, 50))
self._compare_and_render(AccountingNone, 50))
self.assertEquals((-1.0, u'\u2011100.0\xa0%'), self.assertEquals((-1.0, u'\u2011100.0\xa0%'),
self.kpi.compare_and_render(
self.lang, None, 50))
self._compare_and_render(None, 50))
def test_compare_num_diff(self): def test_compare_num_diff(self):
self.kpi.compare_method = 'diff' self.kpi.compare_method = 'diff'
self.assertEquals((25, u'+25'), self.assertEquals((25, u'+25'),
self.kpi.compare_and_render(self.lang, 75, 50))
self._compare_and_render(75, 50))
self.assertEquals((-25, u'\u201125'), self.assertEquals((-25, u'\u201125'),
self.kpi.compare_and_render(self.lang, 25, 50))
self.kpi.suffix = u''
self._compare_and_render(25, 50))
self.style.suffix_inherit = False
self.style.suffix = u''
self.assertEquals((-25, u'\u201125\xa0'), self.assertEquals((-25, u'\u201125\xa0'),
self.kpi.compare_and_render(self.lang, 25, 50))
self.kpi.suffix = u''
self._compare_and_render(25, 50))
self.style.suffix = u''
self.assertEquals((50.0, u'+50'), self.assertEquals((50.0, u'+50'),
self.kpi.compare_and_render(
self.lang, 50, AccountingNone))
self._compare_and_render(50, AccountingNone))
self.assertEquals((50.0, u'+50'), self.assertEquals((50.0, u'+50'),
self.kpi.compare_and_render(
self.lang, 50, None))
self._compare_and_render(50, None))
self.assertEquals((-50.0, u'\u201150'), self.assertEquals((-50.0, u'\u201150'),
self.kpi.compare_and_render(
self.lang, AccountingNone, 50))
self._compare_and_render(AccountingNone, 50))
self.assertEquals((-50.0, u'\u201150'), self.assertEquals((-50.0, u'\u201150'),
self.kpi.compare_and_render(
self.lang, None, 50))
self._compare_and_render(None, 50))
def test_compare_pct(self): def test_compare_pct(self):
self.kpi.type = 'pct' self.kpi.type = 'pct'
self.assertEquals((0.25, u'+25\xa0pp'), self.assertEquals((0.25, u'+25\xa0pp'),
self.kpi.compare_and_render(self.lang, 0.75, 0.50))
self._compare_and_render(0.75, 0.50))

58
mis_builder/views/mis_builder_style.xml

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="mis_report_style_view_tree">
<field name="name">mis.report.style.view.tree</field>
<field name="model">mis.report.style</field>
<field name="arch" type="xml">
<tree string="MIS Report Styles">
<field name="name"/>
<field name="color"/>
<field name="background_color"/>
<field name="font_style"/>
<field name="font_weight"/>
<field name="font_size"/>
<field name="indent_level"/>
</tree>
</field>
</record>
<record id="mis_report_style_view_form" model="ir.ui.view">
<field name="name">mis.report.style.view.form</field>
<field name="model">mis.report.style</field>
<field name="arch" type="xml">
<form string="MIS Report Style" version="7.0">
<sheet>
<group string="Style" col="2">
<field name="name" />
</group>
<group string="Color" col="2">
<field name="color" />
<field name="background_color" />
</group>
<group string="Font" col="2">
<field name="font_style" />
<field name="font_weight" />
<field name="font_size" />
</group>
<group string="Indent">
<field name="indent_level" />
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="mis_report_style_view_action">
<field name="name">MIS Report Styles</field>
<field name="view_id" ref="mis_report_style_view_tree"/>
<field name="res_model">mis.report.style</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="mis_report_style_view_menu" parent="account.menu_account_reports" name="MIS Report Styles" action="mis_report_style_view_action" sequence="22"/>
</data>
</openerp>

22
mis_builder/views/mis_report.xml

@ -22,6 +22,7 @@
<group col="2"> <group col="2">
<field name="name"/> <field name="name"/>
<field name="description"/> <field name="description"/>
<field name="style_id"/>
</group> </group>
<group string="Sub KPI's"> <group string="Sub KPI's">
<field name="subkpi_ids" nolabel="1" colspan="2"> <field name="subkpi_ids" nolabel="1" colspan="2">
@ -51,14 +52,10 @@
<field name="sequence" widget="handle"/> <field name="sequence" widget="handle"/>
<field name="description"/> <field name="description"/>
<field name="name"/> <field name="name"/>
<field name="multi"/>
<field name="expression"/>
<field name="type"/> <field name="type"/>
<field name="dp" attrs="{'invisible': [('type', '=', 'str')]}"/>
<field name="divider" attrs="{'invisible': [('type', '=', 'str')]}"/>
<field name="prefix"/>
<field name="suffix"/>
<field name="compare_method" attrs="{'invisible': [('type', '=', 'str')]}"/> <field name="compare_method" attrs="{'invisible': [('type', '=', 'str')]}"/>
<field name="multi"/>
<field name="expression"/>
</tree> </tree>
</field> </field>
</group> </group>
@ -103,15 +100,8 @@
<field name="description"/> <field name="description"/>
<field name="name"/> <field name="name"/>
<field name="type"/> <field name="type"/>
<field name="dp"
attrs="{'invisible': [('type', '=', 'str')]}"/>
<field name="divider"
attrs="{'invisible': [('type', '=', 'str')]}"/>
<field name="compare_method"
attrs="{'invisible': [('type', '=', 'str')]}"/>
<field name="prefix"/>
<field name="suffix"/>
<field name="style"/>
<field name="compare_method"/>
<field name="style_id"/>
<field name="style_expression"/> <field name="style_expression"/>
<!--<field name="sequence" />--> <!--<field name="sequence" />-->
</group> </group>
@ -131,7 +121,7 @@
</group> </group>
<group col="4" string="Auto expand"> <group col="4" string="Auto expand">
<field name="auto_expand_accounts"/> <field name="auto_expand_accounts"/>
<field name="auto_expand_accounts_style"
<field name="auto_expand_accounts_style_id"
attrs="{'invisible': [('auto_expand_accounts', '!=', True)]}"/> attrs="{'invisible': [('auto_expand_accounts', '!=', True)]}"/>
</group> </group>
<group col="2" string="Legend (for kpi expressions)"> <group col="2" string="Legend (for kpi expressions)">

80
mis_builder/views/mis_report_style.xml

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="mis_report_style_view_tree">
<field name="name">mis.report.style.view.tree</field>
<field name="model">mis.report.style</field>
<field name="arch" type="xml">
<tree string="MIS Report Styles">
<field name="name"/>
</tree>
</field>
</record>
<record id="mis_report_style_view_form" model="ir.ui.view">
<field name="name">mis.report.style.view.form</field>
<field name="model">mis.report.style</field>
<field name="arch" type="xml">
<form string="MIS Report Style" version="7.0">
<sheet>
<group string="Style" col="2">
<field name="name" />
</group>
<group string="Number" col="4">
<field name="dp_inherit" string="Rounding inherit"/>
<field name="dp"
attrs="{'invisible': [('dp_inherit', '=', True)]}"/>
<field name="divider_inherit" string="Factor inherit"/>
<field name="divider"
attrs="{'invisible': [('divider_inherit', '=', True)]}"/>
<field name="prefix_inherit"/>
<field name="prefix"
attrs="{'invisible': [('prefix_inherit', '=', True)]}"/>
<field name="suffix_inherit"/>
<field name="suffix"
attrs="{'invisible': [('suffix_inherit', '=', True)]}"/>
</group>
<group string="Color" col="4">
<field name="color_inherit" />
<field name="color"
attrs="{'invisible': [('color_inherit', '=', True)]}"
widget="color" />
<field name="background_color_inherit" />
<field name="background_color"
attrs="{'invisible': [('background_color_inherit', '=', True)]}"
widget="color" />
</group>
<group string="Font" col="4">
<field name="font_style_inherit" />
<field name="font_style"
attrs="{'invisible': [('font_style_inherit', '=', True)]}" />
<field name="font_weight_inherit" />
<field name="font_weight"
attrs="{'invisible': [('font_weight_inherit', '=', True)]}" />
<field name="font_size_inherit" />
<field name="font_size"
attrs="{'invisible': [('font_size_inherit', '=', True)]}" />
</group>
<group string="Indent" col="4">
<field name="indent_level_inherit" />
<field name="indent_level"
attrs="{'invisible': [('indent_level_inherit', '=', True)]}" />
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="mis_report_style_view_action">
<field name="name">MIS Report Styles</field>
<field name="view_id" ref="mis_report_style_view_tree"/>
<field name="res_model">mis.report.style</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="mis_report_style_view_menu" parent="account.menu_account_reports" name="MIS Report Styles" action="mis_report_style_view_action" sequence="22"/>
</data>
</openerp>
Loading…
Cancel
Save