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.

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