tile_tile.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. # -*- coding: utf-8 -*-
  2. # © 2010-2013 OpenERP s.a. (<http://openerp.com>).
  3. # © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
  4. # © 2015-Today GRAP
  5. # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
  6. import datetime
  7. import time
  8. from dateutil.relativedelta import relativedelta
  9. from collections import OrderedDict
  10. from openerp import api, fields, models
  11. from openerp.tools.safe_eval import safe_eval as eval
  12. from openerp.tools.translate import _
  13. from openerp.exceptions import ValidationError, except_orm
  14. def median(vals):
  15. # https://docs.python.org/3/library/statistics.html#statistics.median
  16. # TODO : refactor, using statistics.median when Odoo will be available
  17. # in Python 3.4
  18. even = (0 if len(vals) % 2 else 1) + 1
  19. half = (len(vals) - 1) / 2
  20. return sum(sorted(vals)[half:half + even]) / float(even)
  21. FIELD_FUNCTIONS = OrderedDict([
  22. ('count', {
  23. 'name': 'Count',
  24. 'func': False, # its hardcoded in _compute_data
  25. # 'help': _('Number of records')
  26. }),
  27. ('min', {
  28. 'name': 'Minimum',
  29. 'func': min,
  30. 'help': _("Minimum value of '%s'")}),
  31. ('max', {
  32. 'name': 'Maximum',
  33. 'func': max,
  34. 'help': _("Maximum value of '%s'")}),
  35. ('sum', {
  36. 'name': 'Sum',
  37. 'func': sum,
  38. # 'help': _("Total value of '%s'")
  39. }),
  40. ('avg', {
  41. 'name': 'Average',
  42. 'func': lambda vals: sum(vals)/len(vals),
  43. 'help': _("Minimum value of '%s'")}),
  44. ('median', {
  45. 'name': 'Median',
  46. 'func': median,
  47. 'help': _("Median value of '%s'")}),
  48. ])
  49. FIELD_FUNCTION_SELECTION = [
  50. (k, FIELD_FUNCTIONS[k].get('name')) for k in FIELD_FUNCTIONS]
  51. class TileTile(models.Model):
  52. _name = 'tile.tile'
  53. _description = 'Dashboard Tile'
  54. _order = 'sequence, name'
  55. def _get_eval_context(self):
  56. def _context_today():
  57. return fields.Date.from_string(fields.Date.context_today(self))
  58. context = self.env.context.copy()
  59. context.update({
  60. 'time': time,
  61. 'datetime': datetime,
  62. 'relativedelta': relativedelta,
  63. 'context_today': _context_today,
  64. 'current_date': fields.Date.today(),
  65. })
  66. return context
  67. # Column Section
  68. name = fields.Char(required=True)
  69. sequence = fields.Integer(default=0, required=True)
  70. user_id = fields.Many2one('res.users', 'User')
  71. background_color = fields.Char(default='#0E6C7E', oldname='color')
  72. font_color = fields.Char(default='#FFFFFF')
  73. group_ids = fields.Many2many(
  74. 'res.groups',
  75. string='Groups',
  76. help='If this field is set, only users of this group can view this '
  77. 'tile. Please note that it will only work for global tiles '
  78. '(that is, when User field is left empty)')
  79. model_id = fields.Many2one('ir.model', 'Model', required=True)
  80. domain = fields.Text(default='[]')
  81. action_id = fields.Many2one('ir.actions.act_window', 'Action')
  82. active = fields.Boolean(
  83. compute='_compute_active',
  84. search='_search_active',
  85. readonly=True)
  86. # Primary Value
  87. primary_function = fields.Selection(
  88. FIELD_FUNCTION_SELECTION,
  89. string='Function',
  90. default='count')
  91. primary_field_id = fields.Many2one(
  92. 'ir.model.fields',
  93. string='Field',
  94. domain="[('model_id', '=', model_id),"
  95. " ('ttype', 'in', ['float', 'integer'])]")
  96. primary_format = fields.Char(
  97. string='Format',
  98. help='Python Format String valid with str.format()\n'
  99. 'ie: \'{:,} Kgs\' will output \'1,000 Kgs\' if value is 1000.')
  100. primary_value = fields.Char(
  101. string='Value',
  102. compute='_compute_data')
  103. primary_helper = fields.Char(
  104. string='Helper',
  105. compute='_compute_helper')
  106. # Secondary Value
  107. secondary_function = fields.Selection(
  108. FIELD_FUNCTION_SELECTION,
  109. string='Secondary Function')
  110. secondary_field_id = fields.Many2one(
  111. 'ir.model.fields',
  112. string='Secondary Field',
  113. domain="[('model_id', '=', model_id),"
  114. " ('ttype', 'in', ['float', 'integer'])]")
  115. secondary_format = fields.Char(
  116. string='Secondary Format',
  117. help='Python Format String valid with str.format()\n'
  118. 'ie: \'{:,} Kgs\' will output \'1,000 Kgs\' if value is 1000.')
  119. secondary_value = fields.Char(
  120. string='Secondary Value',
  121. compute='_compute_data')
  122. secondary_helper = fields.Char(
  123. string='Secondary Helper',
  124. compute='_compute_helper')
  125. error = fields.Char(
  126. string='Error Details',
  127. compute='_compute_data')
  128. @api.one
  129. def _compute_data(self):
  130. if not self.active:
  131. return
  132. model = self.env[self.model_id.model]
  133. eval_context = self._get_eval_context()
  134. domain = self.domain or '[]'
  135. try:
  136. count = model.search_count(eval(domain, eval_context))
  137. except Exception as e:
  138. self.primary_value = self.secondary_value = 'ERR!'
  139. self.error = str(e)
  140. return
  141. if any([
  142. self.primary_function and
  143. self.primary_function != 'count',
  144. self.secondary_function and
  145. self.secondary_function != 'count'
  146. ]):
  147. records = model.search(eval(domain, eval_context))
  148. for f in ['primary_', 'secondary_']:
  149. f_function = f+'function'
  150. f_field_id = f+'field_id'
  151. f_format = f+'format'
  152. f_value = f+'value'
  153. value = 0
  154. if self[f_function] == 'count':
  155. value = count
  156. elif self[f_function]:
  157. func = FIELD_FUNCTIONS[self[f_function]]['func']
  158. if func and self[f_field_id] and count:
  159. vals = [x[self[f_field_id].name] for x in records]
  160. value = func(vals)
  161. if self[f_function]:
  162. try:
  163. self[f_value] = (self[f_format] or '{:,}').format(value)
  164. except ValueError as e:
  165. self[f_value] = 'F_ERR!'
  166. self.error = str(e)
  167. return
  168. else:
  169. self[f_value] = False
  170. @api.one
  171. @api.onchange('primary_function', 'primary_field_id',
  172. 'secondary_function', 'secondary_field_id')
  173. def _compute_helper(self):
  174. for f in ['primary_', 'secondary_']:
  175. f_function = f+'function'
  176. f_field_id = f+'field_id'
  177. f_helper = f+'helper'
  178. self[f_helper] = ''
  179. field_func = FIELD_FUNCTIONS.get(self[f_function], {})
  180. help = field_func.get('help', False)
  181. if help:
  182. if self[f_function] != 'count' and self[f_field_id]:
  183. desc = self[f_field_id].field_description
  184. self[f_helper] = help % desc
  185. else:
  186. self[f_helper] = help
  187. @api.one
  188. def _compute_active(self):
  189. ima = self.env['ir.model.access']
  190. self.active = ima.check(self.model_id.model, 'read', False)
  191. def _search_active(self, operator, value):
  192. cr = self.env.cr
  193. if operator != '=':
  194. raise except_orm(
  195. _('Unimplemented Feature. Search on Active field disabled.'))
  196. ima = self.env['ir.model.access']
  197. ids = []
  198. cr.execute("""
  199. SELECT tt.id, im.model
  200. FROM tile_tile tt
  201. INNER JOIN ir_model im
  202. ON tt.model_id = im.id""")
  203. for result in cr.fetchall():
  204. if (ima.check(result[1], 'read', False) == value):
  205. ids.append(result[0])
  206. return [('id', 'in', ids)]
  207. # Constraints and onchanges
  208. @api.one
  209. @api.constrains('model_id', 'primary_field_id', 'secondary_field_id')
  210. def _check_model_id_field_id(self):
  211. if any([
  212. self.primary_field_id and
  213. self.primary_field_id.model_id.id != self.model_id.id,
  214. self.secondary_field_id and
  215. self.secondary_field_id.model_id.id != self.model_id.id
  216. ]):
  217. raise ValidationError(
  218. _("Please select a field from the selected model."))
  219. @api.onchange('model_id')
  220. def _onchange_model_id(self):
  221. self.primary_field_id = False
  222. self.secondary_field_id = False
  223. @api.onchange('primary_function', 'secondary_function')
  224. def _onchange_function(self):
  225. if self.primary_function in [False, 'count']:
  226. self.primary_field_id = False
  227. if self.secondary_function in [False, 'count']:
  228. self.secondary_field_id = False
  229. # Action methods
  230. @api.multi
  231. def open_link(self):
  232. res = {
  233. 'name': self.name,
  234. 'view_type': 'form',
  235. 'view_mode': 'tree',
  236. 'view_id': [False],
  237. 'res_model': self.model_id.model,
  238. 'type': 'ir.actions.act_window',
  239. 'context': self.env.context,
  240. 'nodestroy': True,
  241. 'target': 'current',
  242. 'domain': self.domain,
  243. }
  244. if self.action_id:
  245. res.update(self.action_id.read(
  246. ['view_type', 'view_mode', 'type'])[0])
  247. return res
  248. @api.model
  249. def add(self, vals):
  250. if 'model_id' in vals and not vals['model_id'].isdigit():
  251. # need to replace model_name with its id
  252. vals['model_id'] = self.env['ir.model'].search(
  253. [('model', '=', vals['model_id'])]).id
  254. self.create(vals)