You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

271 lines
8.7 KiB

  1. # -*- coding: utf-8 -*-
  2. # © 2016 Therp BV (<http://therp.nl>)
  3. # © 2016 ACSONE SA/NV (<http://acsone.eu>)
  4. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  5. from openerp import api, fields, models, _
  6. from openerp.exceptions import UserError
  7. from .accounting_none import AccountingNone
  8. class PropertyDict(dict):
  9. def __getattr__(self, name):
  10. return self.get(name)
  11. def copy(self):
  12. return PropertyDict(self)
  13. PROPS = [
  14. 'color',
  15. 'background_color',
  16. 'font_style',
  17. 'font_weight',
  18. 'font_size',
  19. 'indent_level',
  20. 'prefix',
  21. 'suffix',
  22. 'dp',
  23. 'divider',
  24. ]
  25. TYPE_NUM = 'num'
  26. TYPE_PCT = 'pct'
  27. TYPE_STR = 'str'
  28. CMP_DIFF = 'diff'
  29. CMP_PCT = 'pct'
  30. CMP_NONE = 'none'
  31. class MisReportKpiStyle(models.Model):
  32. _name = 'mis.report.style'
  33. @api.one
  34. @api.constrains('indent_level')
  35. def check_positive_val(self):
  36. if self.indent_level < 0:
  37. raise UserError(_('Indent level must be greater than '
  38. 'or equal to 0'))
  39. _font_style_selection = [
  40. ('normal', 'Normal'),
  41. ('italic', 'Italic'),
  42. ]
  43. _font_weight_selection = [
  44. ('nornal', 'Normal'),
  45. ('bold', 'Bold'),
  46. ]
  47. _font_size_selection = [
  48. ('medium', 'medium'),
  49. ('xx-small', 'xx-small'),
  50. ('x-small', 'x-small'),
  51. ('small', 'small'),
  52. ('large', 'large'),
  53. ('x-large', 'x-large'),
  54. ('xx-large', 'xx-large'),
  55. ]
  56. _font_size_to_xlsx_size = {
  57. 'medium': 11,
  58. 'xx-small': 5,
  59. 'x-small': 7,
  60. 'small': 9,
  61. 'large': 13,
  62. 'x-large': 15,
  63. 'xx-large': 17
  64. }
  65. # style name
  66. # TODO enforce uniqueness
  67. name = fields.Char(string='Style name', required=True)
  68. # color
  69. color_inherit = fields.Boolean(default=True)
  70. color = fields.Char(
  71. string='Text color',
  72. help='Text color in valid RGB code (from #000000 to #FFFFFF)',
  73. )
  74. background_color_inherit = fields.Boolean(default=True)
  75. background_color = fields.Char(
  76. help='Background color in valid RGB code (from #000000 to #FFFFFF)'
  77. )
  78. # font
  79. font_style_inherit = fields.Boolean(default=True)
  80. font_style = fields.Selection(
  81. selection=_font_style_selection,
  82. )
  83. font_weight_inherit = fields.Boolean(default=True)
  84. font_weight = fields.Selection(
  85. selection=_font_weight_selection
  86. )
  87. font_size_inherit = fields.Boolean(default=True)
  88. font_size = fields.Selection(
  89. selection=_font_size_selection
  90. )
  91. # indent
  92. indent_level_inherit = fields.Boolean(default=True)
  93. indent_level = fields.Integer()
  94. # number format
  95. prefix_inherit = fields.Boolean(default=True)
  96. prefix = fields.Char(size=16, string='Prefix')
  97. suffix_inherit = fields.Boolean(default=True)
  98. suffix = fields.Char(size=16, string='Suffix')
  99. dp_inherit = fields.Boolean(default=True)
  100. dp = fields.Integer(string='Rounding', default=0)
  101. divider_inherit = fields.Boolean(default=True)
  102. divider = fields.Selection([('1e-6', _('µ')),
  103. ('1e-3', _('m')),
  104. ('1', _('1')),
  105. ('1e3', _('k')),
  106. ('1e6', _('M'))],
  107. string='Factor',
  108. default='1')
  109. @api.model
  110. def merge(self, styles):
  111. """ Merge several styles, giving priority to the last.
  112. Returns a PropertyDict of style properties.
  113. """
  114. r = PropertyDict()
  115. for style in styles:
  116. if not style:
  117. continue
  118. if isinstance(style, dict):
  119. r.update(style)
  120. else:
  121. for prop in PROPS:
  122. inherit = getattr(style, prop + '_inherit', None)
  123. if inherit is None:
  124. value = getattr(style, prop)
  125. if value:
  126. r[prop] = value
  127. elif not inherit:
  128. value = getattr(style, prop)
  129. r[prop] = value
  130. return r
  131. @api.model
  132. def render(self, lang, style_props, type, value):
  133. if type == 'num':
  134. return self.render_num(lang, value, style_props.divider,
  135. style_props.dp,
  136. style_props.prefix, style_props.suffix)
  137. elif type == 'pct':
  138. return self.render_pct(lang, value, style_props.dp)
  139. else:
  140. return self.render_str(lang, value)
  141. @api.model
  142. def render_num(self, lang, value,
  143. divider=1.0, dp=0, prefix=None, suffix=None, sign='-'):
  144. # format number following user language
  145. if value is None or value is AccountingNone:
  146. return u''
  147. value = round(value / float(divider or 1), dp or 0) or 0
  148. r = lang.format('%%%s.%df' % (sign, dp or 0), value, grouping=True)
  149. r = r.replace('-', u'\N{NON-BREAKING HYPHEN}')
  150. if prefix:
  151. r = prefix + u'\N{NO-BREAK SPACE}' + r
  152. if suffix:
  153. r = r + u'\N{NO-BREAK SPACE}' + suffix
  154. return r
  155. @api.model
  156. def render_pct(self, lang, value, dp=1, sign='-'):
  157. return self.render_num(lang, value, divider=0.01,
  158. dp=dp, suffix='%', sign=sign)
  159. @api.model
  160. def render_str(self, lang, value):
  161. if value is None or value is AccountingNone:
  162. return u''
  163. return unicode(value)
  164. @api.model
  165. def compare_and_render(self, lang, style_props, type, compare_method,
  166. value, base_value,
  167. average_value=1, average_base_value=1):
  168. delta = AccountingNone
  169. style_r = style_props.copy()
  170. if value is None:
  171. value = AccountingNone
  172. if base_value is None:
  173. base_value = AccountingNone
  174. if type == TYPE_PCT:
  175. delta = value - base_value
  176. if delta and round(delta, (style_props.dp or 0) + 2) != 0:
  177. style_r.update(dict(
  178. divider=0.01, prefix='', suffix=_('pp')))
  179. else:
  180. delta = AccountingNone
  181. elif type == TYPE_NUM:
  182. if value and average_value:
  183. value = value / float(average_value)
  184. if base_value and average_base_value:
  185. base_value = base_value / float(average_base_value)
  186. if compare_method == CMP_DIFF:
  187. delta = value - base_value
  188. if delta and round(delta, style_props.dp or 0) != 0:
  189. pass
  190. else:
  191. delta = AccountingNone
  192. elif compare_method == CMP_PCT:
  193. if base_value and round(base_value, style_props.dp or 0) != 0:
  194. delta = (value - base_value) / abs(base_value)
  195. if delta and round(delta, 1) != 0:
  196. style_r.update(dict(
  197. divider=0.01, dp=1, prefix='', suffix='%'))
  198. else:
  199. delta = AccountingNone
  200. if delta is not AccountingNone:
  201. delta_r = self.render_num(
  202. lang, delta,
  203. style_r.divider, style_r.dp,
  204. style_r.prefix, style_r.suffix,
  205. sign='+')
  206. return delta, delta_r, style_r
  207. else:
  208. return AccountingNone, '', style_r
  209. @api.model
  210. def to_xlsx_style(self, props):
  211. num_format = '0'
  212. if props.dp:
  213. num_format += '.'
  214. num_format += '0' * props.dp
  215. if props.prefix:
  216. num_format = u'"{} "{}'.format(props.prefix, num_format)
  217. if props.suffix:
  218. num_format = u'{}" {}"'.format(num_format, props.suffix)
  219. xlsx_attributes = [
  220. ('italic', props.font_style == 'italic'),
  221. ('bold', props.font_weight == 'bold'),
  222. ('size', self._font_size_to_xlsx_size.get(props.font_size, 11)),
  223. ('font_color', props.color),
  224. ('bg_color', props.background_color),
  225. ('indent', props.indent_level),
  226. ('num_format', num_format),
  227. ]
  228. return dict([a for a in xlsx_attributes
  229. if a[1] is not None])
  230. @api.model
  231. def to_css_style(self, props):
  232. css_attributes = [
  233. ('font-style', props.font_style),
  234. ('font-weight', props.font_weight),
  235. ('font-size', props.font_size),
  236. ('color', props.color),
  237. ('background-color', props.background_color),
  238. ('indent-level', props.indent_level)
  239. ]
  240. return '; '.join(['%s: %s' % a for a in css_attributes
  241. if a[1] is not None]) or None