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.

416 lines
15 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. # -*- coding: utf-8 -*-
  2. <<<<<<< HEAD
  3. ##############################################################################
  4. #
  5. # OpenERP, Open Source Management Solution
  6. # Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>).
  7. # Copyright (C) 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
  8. # Copyright (C) 2015-Today GRAP
  9. # Author Markus Schneider <markus.schneider at initos.com>
  10. # @author Sylvain LE GAL (https://twitter.com/legalsylvain)
  11. #
  12. # This program is free software: you can redistribute it and/or modify
  13. # it under the terms of the GNU Affero General Public License as
  14. # published by the Free Software Foundation, either version 3 of the
  15. # License, or (at your option) any later version.
  16. #
  17. # This program is distributed in the hope that it will be useful,
  18. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. # GNU Affero General Public License for more details.
  21. #
  22. # You should have received a copy of the GNU Affero General Public License
  23. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. #
  25. ##############################################################################
  26. from openerp import api, fields
  27. from openerp.models import Model
  28. from openerp.exceptions import except_orm
  29. =======
  30. # © 2010-2013 OpenERP s.a. (<http://openerp.com>).
  31. # © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
  32. # © 2015-Today GRAP
  33. # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
  34. import datetime
  35. import time
  36. from dateutil.relativedelta import relativedelta
  37. from collections import OrderedDict
  38. from openerp import api, fields, models
  39. from openerp.tools.safe_eval import safe_eval as eval
  40. >>>>>>> 3ab676d... [IMP][8.0][web_dashboard_tile] Refactor (see changes in description) (#476)
  41. from openerp.tools.translate import _
  42. <<<<<<< HEAD
  43. class TileTile(Model):
  44. =======
  45. def median(vals):
  46. # https://docs.python.org/3/library/statistics.html#statistics.median
  47. # TODO : refactor, using statistics.median when Odoo will be available
  48. # in Python 3.4
  49. even = (0 if len(vals) % 2 else 1) + 1
  50. half = (len(vals) - 1) / 2
  51. return sum(sorted(vals)[half:half + even]) / float(even)
  52. FIELD_FUNCTIONS = OrderedDict([
  53. ('count', {
  54. 'name': 'Count',
  55. 'func': False, # its hardcoded in _compute_data
  56. 'help': _('Number of records')}),
  57. ('min', {
  58. 'name': 'Minimum',
  59. 'func': min,
  60. 'help': _("Minimum value of '%s'")}),
  61. ('max', {
  62. 'name': 'Maximum',
  63. 'func': max,
  64. 'help': _("Maximum value of '%s'")}),
  65. ('sum', {
  66. 'name': 'Sum',
  67. 'func': sum,
  68. 'help': _("Total value of '%s'")}),
  69. ('avg', {
  70. 'name': 'Average',
  71. 'func': lambda vals: sum(vals)/len(vals),
  72. 'help': _("Minimum value of '%s'")}),
  73. ('median', {
  74. 'name': 'Median',
  75. 'func': median,
  76. 'help': _("Median value of '%s'")}),
  77. ])
  78. FIELD_FUNCTION_SELECTION = [
  79. (k, FIELD_FUNCTIONS[k].get('name')) for k in FIELD_FUNCTIONS]
  80. class TileTile(models.Model):
  81. >>>>>>> 3ab676d... [IMP][8.0][web_dashboard_tile] Refactor (see changes in description) (#476)
  82. _name = 'tile.tile'
  83. _description = 'Dashboard Tile'
  84. _order = 'sequence, name'
  85. <<<<<<< HEAD
  86. def median(self, aList):
  87. # https://docs.python.org/3/library/statistics.html#statistics.median
  88. # TODO : refactor, using statistics.median when Odoo will be available
  89. # in Python 3.4
  90. even = (0 if len(aList) % 2 else 1) + 1
  91. half = (len(aList) - 1) / 2
  92. return sum(sorted(aList)[half:half + even]) / float(even)
  93. def _get_tile_info(self):
  94. ima_obj = self.env['ir.model.access']
  95. res = {}
  96. for r in self:
  97. r.active = False
  98. r.count = 0
  99. r.computed_value = 0
  100. r.helper = ''
  101. if ima_obj.check(r.model_id.model, 'read', False):
  102. # Compute count item
  103. model = self.env[r.model_id.model]
  104. r.count = model.search_count(eval(r.domain))
  105. r.active = True
  106. # Compute datas for field_id depending of field_function
  107. if r.field_function and r.field_id and r.count != 0:
  108. records = model.search(eval(r.domain))
  109. vals = [x[r.field_id.name] for x in records]
  110. desc = r.field_id.field_description
  111. if r.field_function == 'min':
  112. r.computed_value = min(vals)
  113. r.helper = _("Minimum value of '%s'") % desc
  114. elif r.field_function == 'max':
  115. r.computed_value = max(vals)
  116. r.helper = _("Maximum value of '%s'") % desc
  117. elif r.field_function == 'sum':
  118. r.computed_value = sum(vals)
  119. r.helper = _("Total value of '%s'") % desc
  120. elif r.field_function == 'avg':
  121. r.computed_value = sum(vals) / len(vals)
  122. r.helper = _("Average value of '%s'") % desc
  123. elif r.field_function == 'median':
  124. r.computed_value = self.median(vals)
  125. r.helper = _("Median value of '%s'") % desc
  126. return res
  127. =======
  128. def _get_eval_context(self):
  129. def _context_today():
  130. return fields.Date.from_string(fields.Date.context_today(self))
  131. context = self.env.context.copy()
  132. context.update({
  133. 'time': time,
  134. 'datetime': datetime,
  135. 'relativedelta': relativedelta,
  136. 'context_today': _context_today,
  137. 'current_date': fields.Date.today(),
  138. })
  139. return context
  140. # Column Section
  141. name = fields.Char(required=True)
  142. sequence = fields.Integer(default=0, required=True)
  143. user_id = fields.Many2one('res.users', 'User')
  144. background_color = fields.Char(default='#0E6C7E', oldname='color')
  145. font_color = fields.Char(default='#FFFFFF')
  146. group_ids = fields.Many2many(
  147. 'res.groups',
  148. string='Groups',
  149. help='If this field is set, only users of this group can view this '
  150. 'tile. Please note that it will only work for global tiles '
  151. '(that is, when User field is left empty)')
  152. model_id = fields.Many2one('ir.model', 'Model', required=True)
  153. domain = fields.Text(default='[]')
  154. action_id = fields.Many2one('ir.actions.act_window', 'Action')
  155. active = fields.Boolean(
  156. compute='_compute_active',
  157. search='_search_active',
  158. readonly=True)
  159. # Primary Value
  160. primary_function = fields.Selection(
  161. FIELD_FUNCTION_SELECTION,
  162. string='Function',
  163. default='count')
  164. primary_field_id = fields.Many2one(
  165. 'ir.model.fields',
  166. string='Field',
  167. domain="[('model_id', '=', model_id),"
  168. " ('ttype', 'in', ['float', 'integer'])]")
  169. primary_format = fields.Char(
  170. string='Format',
  171. help='Python Format String valid with str.format()\n'
  172. 'ie: \'{:,} Kgs\' will output \'1,000 Kgs\' if value is 1000.')
  173. primary_value = fields.Char(
  174. string='Value',
  175. compute='_compute_data')
  176. primary_helper = fields.Char(
  177. string='Helper',
  178. compute='_compute_helper')
  179. # Secondary Value
  180. secondary_function = fields.Selection(
  181. FIELD_FUNCTION_SELECTION,
  182. string='Secondary Function')
  183. secondary_field_id = fields.Many2one(
  184. 'ir.model.fields',
  185. string='Secondary Field',
  186. domain="[('model_id', '=', model_id),"
  187. " ('ttype', 'in', ['float', 'integer'])]")
  188. secondary_format = fields.Char(
  189. string='Secondary Format',
  190. help='Python Format String valid with str.format()\n'
  191. 'ie: \'{:,} Kgs\' will output \'1,000 Kgs\' if value is 1000.')
  192. secondary_value = fields.Char(
  193. string='Secondary Value',
  194. compute='_compute_data')
  195. secondary_helper = fields.Char(
  196. string='Secondary Helper',
  197. compute='_compute_helper')
  198. error = fields.Char(
  199. string='Error Details',
  200. compute='_compute_data')
  201. @api.one
  202. def _compute_data(self):
  203. if not self.active:
  204. return
  205. model = self.env[self.model_id.model]
  206. eval_context = self._get_eval_context()
  207. domain = self.domain or '[]'
  208. try:
  209. count = model.search_count(eval(domain, eval_context))
  210. except Exception as e:
  211. self.primary_value = self.secondary_value = 'ERR!'
  212. self.error = str(e)
  213. return
  214. if any([
  215. self.primary_function and
  216. self.primary_function != 'count',
  217. self.secondary_function and
  218. self.secondary_function != 'count'
  219. ]):
  220. records = model.search(eval(domain, eval_context))
  221. for f in ['primary_', 'secondary_']:
  222. f_function = f+'function'
  223. f_field_id = f+'field_id'
  224. f_format = f+'format'
  225. f_value = f+'value'
  226. value = 0
  227. if self[f_function] == 'count':
  228. value = count
  229. elif self[f_function]:
  230. func = FIELD_FUNCTIONS[self[f_function]]['func']
  231. if func and self[f_field_id] and count:
  232. vals = [x[self[f_field_id].name] for x in records]
  233. value = func(vals)
  234. if self[f_function]:
  235. try:
  236. self[f_value] = (self[f_format] or '{:,}').format(value)
  237. except ValueError as e:
  238. self[f_value] = 'F_ERR!'
  239. self.error = str(e)
  240. return
  241. else:
  242. self[f_value] = False
  243. @api.one
  244. @api.onchange('primary_function', 'primary_field_id',
  245. 'secondary_function', 'secondary_field_id')
  246. def _compute_helper(self):
  247. for f in ['primary_', 'secondary_']:
  248. f_function = f+'function'
  249. f_field_id = f+'field_id'
  250. f_helper = f+'helper'
  251. self[f_helper] = ''
  252. field_func = FIELD_FUNCTIONS.get(self[f_function], {})
  253. help = field_func.get('help', False)
  254. if help:
  255. if self[f_function] != 'count' and self[f_field_id]:
  256. desc = self[f_field_id].field_description
  257. self[f_helper] = help % desc
  258. else:
  259. self[f_helper] = help
  260. @api.one
  261. def _compute_active(self):
  262. ima = self.env['ir.model.access']
  263. self.active = ima.check(self.model_id.model, 'read', False)
  264. >>>>>>> 3ab676d... [IMP][8.0][web_dashboard_tile] Refactor (see changes in description) (#476)
  265. def _search_active(self, operator, value):
  266. cr = self.env.cr
  267. if operator != '=':
  268. raise except_orm(
  269. 'Unimplemented Feature',
  270. 'Search on Active field disabled.')
  271. ima_obj = self.env['ir.model.access']
  272. ids = []
  273. cr.execute("""
  274. SELECT tt.id, im.model
  275. FROM tile_tile tt
  276. INNER JOIN ir_model im
  277. ON tt.model_id = im.id""")
  278. for result in cr.fetchall():
  279. if (ima_obj.check(result[1], 'read', False) == value):
  280. ids.append(result[0])
  281. return [('id', 'in', ids)]
  282. <<<<<<< HEAD
  283. # Column Section
  284. name = fields.Char(required=True)
  285. model_id = fields.Many2one(
  286. comodel_name='ir.model', string='Model', required=True)
  287. user_id = fields.Many2one(
  288. comodel_name='res.users', string='User')
  289. domain = fields.Text(default='[]')
  290. action_id = fields.Many2one(
  291. comodel_name='ir.actions.act_window', string='Action')
  292. count = fields.Integer(compute='_get_tile_info')
  293. computed_value = fields.Float(compute='_get_tile_info')
  294. helper = fields.Char(compute='_get_tile_info')
  295. field_function = fields.Selection(selection=[
  296. ('min', 'Minimum'),
  297. ('max', 'Maximum'),
  298. ('sum', 'Sum'),
  299. ('avg', 'Average'),
  300. ('median', 'Median'),
  301. ], string='Function')
  302. field_id = fields.Many2one(
  303. comodel_name='ir.model.fields', string='Field',
  304. domain="[('model_id', '=', model_id),"
  305. " ('ttype', 'in', ['float', 'int'])]")
  306. active = fields.Boolean(
  307. compute='_get_tile_info', readonly=True, search='_search_active')
  308. background_color = fields.Char(default='#0E6C7E', oldname='color')
  309. font_color = fields.Char(default='#FFFFFF')
  310. sequence = fields.Integer(default=0, required=True)
  311. =======
  312. # Constraints and onchanges
  313. @api.one
  314. @api.constrains('model_id', 'primary_field_id', 'secondary_field_id')
  315. def _check_model_id_field_id(self):
  316. if any([
  317. self.primary_field_id and
  318. self.primary_field_id.model_id.id != self.model_id.id,
  319. self.secondary_field_id and
  320. self.secondary_field_id.model_id.id != self.model_id.id
  321. ]):
  322. raise ValidationError(
  323. _("Please select a field from the selected model."))
  324. @api.onchange('model_id')
  325. def _onchange_model_id(self):
  326. self.primary_field_id = False
  327. self.secondary_field_id = False
  328. @api.onchange('primary_function', 'secondary_function')
  329. def _onchange_function(self):
  330. if self.primary_function in [False, 'count']:
  331. self.primary_field_id = False
  332. if self.secondary_function in [False, 'count']:
  333. self.secondary_field_id = False
  334. >>>>>>> 3ab676d... [IMP][8.0][web_dashboard_tile] Refactor (see changes in description) (#476)
  335. # Constraint Section
  336. def _check_model_id_field_id(self, cr, uid, ids, context=None):
  337. for t in self.browse(cr, uid, ids, context=context):
  338. if t.field_id and t.field_id.model_id.id != t.model_id.id:
  339. return False
  340. return True
  341. def _check_field_id_field_function(self, cr, uid, ids, context=None):
  342. for t in self.browse(cr, uid, ids, context=context):
  343. if t.field_id and not t.field_function or\
  344. t.field_function and not t.field_id:
  345. return False
  346. return True
  347. _constraints = [
  348. (
  349. _check_model_id_field_id,
  350. "Error ! Please select a field of the selected model.",
  351. ['model_id', 'field_id']),
  352. (
  353. _check_field_id_field_function,
  354. "Error ! Please set both fields: 'Field' and 'Function'.",
  355. ['field_id', 'field_function']),
  356. ]
  357. # View / action Section
  358. @api.multi
  359. def open_link(self):
  360. res = {
  361. 'name': self.name,
  362. 'view_type': 'form',
  363. 'view_mode': 'tree',
  364. 'view_id': [False],
  365. 'res_model': self.model_id.model,
  366. 'type': 'ir.actions.act_window',
  367. 'context': self.env.context,
  368. 'nodestroy': True,
  369. 'target': 'current',
  370. 'domain': self.domain,
  371. }
  372. if self.action_id:
  373. res.update(self.action_id.read(
  374. ['view_type', 'view_mode', 'view_id', 'type'])[0])
  375. # FIXME: restore original Domain + Filter would be better
  376. return res
  377. @api.model
  378. def add(self, vals):
  379. if 'model_id' in vals and not vals['model_id'].isdigit():
  380. # need to replace model_name with its id
  381. vals['model_id'] = self.env['ir.model'].search(
  382. [('model', '=', vals['model_id'])]).id
  383. self.create(vals)