2021-03-23 19:36:01 +00:00
|
|
|
# Copyright 2016 Therp BV (<http://therp.nl>)
|
|
|
|
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
2021-10-10 02:21:14 +00:00
|
|
|
# Copyright 2020 CorporateHub (https://corporatehub.eu)
|
2021-03-23 19:36:01 +00:00
|
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
from flectra import _, api, fields, models
|
|
|
|
from flectra.exceptions import ValidationError
|
|
|
|
|
|
|
|
from .accounting_none import AccountingNone
|
|
|
|
from .data_error import DataError
|
|
|
|
|
|
|
|
if sys.version_info.major >= 3:
|
|
|
|
unicode = str
|
|
|
|
|
|
|
|
|
|
|
|
class PropertyDict(dict):
|
|
|
|
def __getattr__(self, name):
|
|
|
|
return self.get(name)
|
|
|
|
|
|
|
|
def copy(self): # pylint: disable=copy-wo-api-one,method-required-super
|
|
|
|
return PropertyDict(self)
|
|
|
|
|
|
|
|
|
|
|
|
PROPS = [
|
|
|
|
"color",
|
|
|
|
"background_color",
|
|
|
|
"font_style",
|
|
|
|
"font_weight",
|
|
|
|
"font_size",
|
|
|
|
"indent_level",
|
|
|
|
"prefix",
|
|
|
|
"suffix",
|
|
|
|
"dp",
|
|
|
|
"divider",
|
|
|
|
"hide_empty",
|
|
|
|
"hide_always",
|
|
|
|
]
|
|
|
|
|
|
|
|
TYPE_NUM = "num"
|
|
|
|
TYPE_PCT = "pct"
|
|
|
|
TYPE_STR = "str"
|
|
|
|
|
|
|
|
CMP_DIFF = "diff"
|
|
|
|
CMP_PCT = "pct"
|
|
|
|
CMP_NONE = "none"
|
|
|
|
|
|
|
|
|
|
|
|
class MisReportKpiStyle(models.Model):
|
|
|
|
|
|
|
|
_name = "mis.report.style"
|
|
|
|
_description = "MIS Report Style"
|
|
|
|
|
|
|
|
@api.constrains("indent_level")
|
|
|
|
def check_positive_val(self):
|
|
|
|
for record in self:
|
|
|
|
if record.indent_level < 0:
|
|
|
|
raise ValidationError(
|
|
|
|
_("Indent level must be greater than " "or equal to 0")
|
|
|
|
)
|
|
|
|
|
|
|
|
_font_style_selection = [("normal", "Normal"), ("italic", "Italic")]
|
|
|
|
|
|
|
|
_font_weight_selection = [("nornal", "Normal"), ("bold", "Bold")]
|
|
|
|
|
|
|
|
_font_size_selection = [
|
|
|
|
("medium", "medium"),
|
|
|
|
("xx-small", "xx-small"),
|
|
|
|
("x-small", "x-small"),
|
|
|
|
("small", "small"),
|
|
|
|
("large", "large"),
|
|
|
|
("x-large", "x-large"),
|
|
|
|
("xx-large", "xx-large"),
|
|
|
|
]
|
|
|
|
|
|
|
|
_font_size_to_xlsx_size = {
|
|
|
|
"medium": 11,
|
|
|
|
"xx-small": 5,
|
|
|
|
"x-small": 7,
|
|
|
|
"small": 9,
|
|
|
|
"large": 13,
|
|
|
|
"x-large": 15,
|
|
|
|
"xx-large": 17,
|
|
|
|
}
|
|
|
|
|
|
|
|
# style name
|
|
|
|
# TODO enforce uniqueness
|
|
|
|
name = fields.Char(string="Style name", required=True)
|
|
|
|
|
|
|
|
# color
|
|
|
|
color_inherit = fields.Boolean(default=True)
|
|
|
|
color = fields.Char(
|
|
|
|
string="Text color",
|
|
|
|
help="Text color in valid RGB code (from #000000 to #FFFFFF)",
|
|
|
|
default="#000000",
|
|
|
|
)
|
|
|
|
background_color_inherit = fields.Boolean(default=True)
|
|
|
|
background_color = fields.Char(
|
|
|
|
help="Background color in valid RGB code (from #000000 to #FFFFFF)",
|
|
|
|
default="#FFFFFF",
|
|
|
|
)
|
|
|
|
# font
|
|
|
|
font_style_inherit = fields.Boolean(default=True)
|
|
|
|
font_style = fields.Selection(selection=_font_style_selection)
|
|
|
|
font_weight_inherit = fields.Boolean(default=True)
|
|
|
|
font_weight = fields.Selection(selection=_font_weight_selection)
|
|
|
|
font_size_inherit = fields.Boolean(default=True)
|
|
|
|
font_size = fields.Selection(selection=_font_size_selection)
|
|
|
|
# indent
|
|
|
|
indent_level_inherit = fields.Boolean(default=True)
|
|
|
|
indent_level = fields.Integer()
|
|
|
|
# number format
|
|
|
|
prefix_inherit = fields.Boolean(default=True)
|
2021-10-10 02:21:14 +00:00
|
|
|
prefix = fields.Char(string="Prefix")
|
2021-03-23 19:36:01 +00:00
|
|
|
suffix_inherit = fields.Boolean(default=True)
|
2021-10-10 02:21:14 +00:00
|
|
|
suffix = fields.Char(string="Suffix")
|
2021-03-23 19:36:01 +00:00
|
|
|
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",
|
|
|
|
)
|
|
|
|
hide_empty_inherit = fields.Boolean(default=True)
|
|
|
|
hide_empty = fields.Boolean(default=False)
|
|
|
|
hide_always_inherit = fields.Boolean(default=True)
|
|
|
|
hide_always = fields.Boolean(default=False)
|
|
|
|
|
|
|
|
@api.model
|
|
|
|
def merge(self, styles):
|
|
|
|
"""Merge several styles, giving priority to the last.
|
|
|
|
|
|
|
|
Returns a PropertyDict of style properties.
|
|
|
|
"""
|
|
|
|
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 not inherit:
|
|
|
|
value = getattr(style, prop)
|
|
|
|
r[prop] = value
|
|
|
|
return r
|
|
|
|
|
|
|
|
@api.model
|
|
|
|
def render(self, lang, style_props, type, value, sign="-"):
|
|
|
|
if type == TYPE_NUM:
|
|
|
|
return self.render_num(
|
|
|
|
lang,
|
|
|
|
value,
|
|
|
|
style_props.divider,
|
|
|
|
style_props.dp,
|
|
|
|
style_props.prefix,
|
|
|
|
style_props.suffix,
|
|
|
|
sign=sign,
|
|
|
|
)
|
|
|
|
elif type == TYPE_PCT:
|
|
|
|
return self.render_pct(lang, value, style_props.dp, sign=sign)
|
|
|
|
else:
|
|
|
|
return self.render_str(lang, value)
|
|
|
|
|
|
|
|
@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 compare_and_render(
|
|
|
|
self,
|
|
|
|
lang,
|
|
|
|
style_props,
|
|
|
|
type,
|
|
|
|
compare_method,
|
|
|
|
value,
|
|
|
|
base_value,
|
|
|
|
average_value=1,
|
|
|
|
average_base_value=1,
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
:param lang: res.lang record
|
|
|
|
:param style_props: PropertyDict with style properties
|
|
|
|
:param type: num, pct or str
|
|
|
|
:param compare_method: diff, pct, none
|
|
|
|
:param value: value to compare (value - base_value)
|
|
|
|
:param base_value: value compared with (value - base_value)
|
|
|
|
:param average_value: value = value / average_value
|
|
|
|
:param average_base_value: base_value = base_value / average_base_value
|
|
|
|
:return: tuple with 4 elements
|
|
|
|
- delta = comparison result (Float or AccountingNone)
|
|
|
|
- delta_r = delta rendered in formatted string (String)
|
|
|
|
- delta_style = PropertyDict with style properties
|
|
|
|
- delta_type = Type of the comparison result (num or pct)
|
|
|
|
"""
|
|
|
|
delta = AccountingNone
|
|
|
|
delta_r = ""
|
|
|
|
delta_style = style_props.copy()
|
|
|
|
delta_type = TYPE_NUM
|
|
|
|
if isinstance(value, DataError) or isinstance(base_value, DataError):
|
|
|
|
return AccountingNone, "", delta_style, delta_type
|
|
|
|
if value is None:
|
|
|
|
value = AccountingNone
|
|
|
|
if base_value is None:
|
|
|
|
base_value = AccountingNone
|
|
|
|
if type == TYPE_PCT:
|
|
|
|
delta = value - base_value
|
|
|
|
if delta and round(delta, (style_props.dp or 0) + 2) != 0:
|
|
|
|
delta_style.update(divider=0.01, prefix="", suffix=_("pp"))
|
|
|
|
else:
|
|
|
|
delta = AccountingNone
|
|
|
|
elif type == TYPE_NUM:
|
|
|
|
if value and average_value:
|
|
|
|
# pylint: disable=redefined-variable-type
|
|
|
|
value = value / float(average_value)
|
|
|
|
if base_value and average_base_value:
|
|
|
|
# pylint: disable=redefined-variable-type
|
|
|
|
base_value = base_value / float(average_base_value)
|
|
|
|
if compare_method == CMP_DIFF:
|
|
|
|
delta = value - base_value
|
|
|
|
if delta and round(delta, style_props.dp or 0) != 0:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
delta = AccountingNone
|
|
|
|
elif compare_method == CMP_PCT:
|
|
|
|
if base_value and round(base_value, style_props.dp or 0) != 0:
|
|
|
|
delta = (value - base_value) / abs(base_value)
|
|
|
|
if delta and round(delta, 3) != 0:
|
|
|
|
delta_style.update(dp=1)
|
|
|
|
delta_type = TYPE_PCT
|
|
|
|
else:
|
|
|
|
delta = AccountingNone
|
|
|
|
if delta is not AccountingNone:
|
|
|
|
delta_r = self.render(lang, delta_style, delta_type, delta, sign="+")
|
|
|
|
return delta, delta_r, delta_style, delta_type
|
|
|
|
|
|
|
|
@api.model
|
|
|
|
def to_xlsx_style(self, type, props, no_indent=False):
|
|
|
|
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),
|
|
|
|
]
|
|
|
|
if type == TYPE_NUM:
|
|
|
|
num_format = u"#,##0"
|
|
|
|
if props.dp:
|
|
|
|
num_format += u"."
|
|
|
|
num_format += u"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.append(("num_format", num_format))
|
|
|
|
elif type == TYPE_PCT:
|
|
|
|
num_format = u"0"
|
|
|
|
if props.dp:
|
|
|
|
num_format += u"."
|
|
|
|
num_format += u"0" * props.dp
|
|
|
|
num_format += "%"
|
|
|
|
xlsx_attributes.append(("num_format", num_format))
|
|
|
|
if props.indent_level is not None and not no_indent:
|
|
|
|
xlsx_attributes.append(("indent", props.indent_level))
|
|
|
|
return dict([a for a in xlsx_attributes if a[1] is not None])
|
|
|
|
|
|
|
|
@api.model
|
|
|
|
def to_css_style(self, props, no_indent=False):
|
|
|
|
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),
|
|
|
|
]
|
|
|
|
if props.indent_level is not None and not no_indent:
|
|
|
|
css_attributes.append(("text-indent", "{}em".format(props.indent_level)))
|
|
|
|
return (
|
|
|
|
"; ".join(["%s: %s" % a for a in css_attributes if a[1] is not None])
|
|
|
|
or None
|
|
|
|
)
|