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.

285 lines
9.7 KiB

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