Browse Source

Merge pull request #507 from adhoc-dev/9.0-mig-web_dashboard_tile

9.0 mig web dashboard tile
pull/856/head
Moises Lopez - https://www.vauxoo.com/ 6 years ago
committed by GitHub
parent
commit
9db404662d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 64
      web_dashboard_tile/README.rst
  2. 27
      web_dashboard_tile/__init__.py
  3. 36
      web_dashboard_tile/__openerp__.py
  4. 4
      web_dashboard_tile/demo/tile_tile.yml
  5. 13
      web_dashboard_tile/migrations/8.0.3.0/post-migration.py
  6. 26
      web_dashboard_tile/migrations/8.0.4.0/post-migration.py
  7. 24
      web_dashboard_tile/models/__init__.py
  8. 364
      web_dashboard_tile/models/tile_tile.py
  9. 11
      web_dashboard_tile/security/rules.xml
  10. 55
      web_dashboard_tile/static/src/css/tile.css
  11. BIN
      web_dashboard_tile/static/src/img/screenshot_dashboard.png
  12. 15
      web_dashboard_tile/static/src/js/custom_js.js
  13. 2
      web_dashboard_tile/static/src/xml/custom_xml.xml
  14. 6
      web_dashboard_tile/tests/__init__.py
  15. 52
      web_dashboard_tile/tests/test_tile.py
  16. 95
      web_dashboard_tile/views/tile.xml

64
web_dashboard_tile/README.rst

@ -1,36 +1,48 @@
Add Tiles to Dashboard
======================
Dashboard Tiles
===============
module to give you a dashboard where you can configure tile from any view
and add them as short cut.
Adds a dashboard where you can configure tiles from any view and add them as short cut.
* Tile can be:
* displayed only for a user;
* global for all users (In that case, some tiles will be hidden if
the current user doesn't have access to the given model);
* The tile displays items count of a given model restricted to a given domain;
* Optionnaly, the tile can display the result of a function of a field;
* Function is one of sum/avg/min/max/median;
* Field must be integer or float;
By default, the tile displays items count of a given model restricted to a given domain.
Optionally, the tile can display the result of a function on a field.
- Function is one of `sum`, `avg`, `min`, `max` or `median`.
- Field must be integer or float.
Tile can be:
- Displayed only for a user.
- Global for all users.
- Restricted to some groups.
*Note: The tile will be hidden if the current user doesn't have access to the given model.*
Usage Usage
===== =====
* Dashboad sample, displaying Sale Orders to invoice: * Dashboad sample, displaying Sale Orders to invoice:
.. image:: /web_dashboard_tile/static/src/img/screenshot_dashboard.png
.. image:: ./static/src/img/screenshot_dashboard.png
* Tree view displayed when user click on the tile: * Tree view displayed when user click on the tile:
.. image:: /web_dashboard_tile/static/src/img/screenshot_action_click.png
Known issues / Roadmap
======================
.. image:: ./static/src/img/screenshot_action_click.png
* Can not edit tile from dashboard (color, sequence, function, ...);
* Context are ignored;
* Date filter can not be relative;
* Combine domain of menue and filter so can not restore origin filter;
* Support context_today;
* Add icons;
* Support client side action (like inbox);
Known issues
============
* Can not edit tile from dashboard (color, sequence, function, ...).
* Original context is ignored.
* Original domain and filter are not restored.
* To preserve a relative date domain, you have to manually edit the tile's domain from `Configuration > User Interface > Dashboard Tile`. You can use the same variables available in filters (`uid`, `context_today()`, `current_date`, `time`, `datetime`, `relativedelta`).
Roadmap
=======
* Add icons.
* Support client side action (like inbox).
* Restore original Domain + Filter when an action is set.
* Posibility to hide the tile based on a field expression.
* Posibility to set the background color based on a field expression.
Bug Tracker Bug Tracker
=========== ===========
@ -38,10 +50,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_. Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
In case of trouble, please check there if your issue has already been reported. In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/
web/issues/new?body=module:%20
web_dashboard_tile%0Aversion:%20
8.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`here <https://github.com/OCA/web/issues/new?body=module:%20web_dashboard_tile%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits Credits
@ -52,6 +61,7 @@ Contributors
* Markus Schneider <markus.schneider at initos.com> * Markus Schneider <markus.schneider at initos.com>
* Sylvain Le Gal (https://twitter.com/legalsylvain) * Sylvain Le Gal (https://twitter.com/legalsylvain)
* Iván Todorovich <ivan.todorovich@gmail.com>
Maintainer Maintainer
---------- ----------

27
web_dashboard_tile/__init__.py

@ -1,26 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>).
# Copyright (C) 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# Copyright (C) 2015-Today GRAP
# Author Markus Schneider <markus.schneider at initos.com>
# @author Sylvain LE GAL (https://twitter.com/legalsylvain)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# © 2010-2013 OpenERP s.a. (<http://openerp.com>).
# © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# © 2015-Today GRAP
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from . import models from . import models

36
web_dashboard_tile/__openerp__.py

@ -1,38 +1,27 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>).
# Copyright (C) 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# Author Markus Schneider <markus.schneider at initos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# © 2010-2013 OpenERP s.a. (<http://openerp.com>).
# © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
{ {
"name": "Dashboard Tile", "name": "Dashboard Tile",
"summary": "Add Tiles to Dashboard", "summary": "Add Tiles to Dashboard",
"version": "8.0.1.0.0",
"version": "9.0.1.0.0",
"depends": [ "depends": [
'web', 'web',
'board', 'board',
'mail', 'mail',
'web_widget_color', 'web_widget_color',
], ],
'author': "initOS GmbH & Co. KG,GRAP,Odoo Community Association (OCA)",
'author': 'initOS GmbH & Co. KG, '
'GRAP, '
'Odoo Community Association (OCA)',
"category": "web", "category": "web",
'license': 'AGPL-3', 'license': 'AGPL-3',
'contributors': [
'initOS GmbH & Co. KG',
'GRAP',
'Iván Todorovich <ivan.todorovich@gmail.com>'
],
'data': [ 'data': [
'views/tile.xml', 'views/tile.xml',
'views/templates.xml', 'views/templates.xml',
@ -46,5 +35,4 @@
'qweb': [ 'qweb': [
'static/src/xml/custom_xml.xml', 'static/src/xml/custom_xml.xml',
], ],
'installable': False,
} }

4
web_dashboard_tile/demo/tile_tile.yml

@ -36,5 +36,5 @@
name: Currencies (Max Rate) name: Currencies (Max Rate)
model_id: base.model_res_currency model_id: base.model_res_currency
domain: [] domain: []
field_function: max
field_id: base.field_res_currency_rate
secondary_function: max
secondary_field_id: base.field_res_currency_rate

13
web_dashboard_tile/migrations/8.0.3.0/post-migration.py

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# © 2016 Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
def migrate(cr, version):
if version is None:
return
# Rename old fields
cr.execute("""UPDATE tile_tile SET primary_function = 'count'""")
cr.execute("""UPDATE tile_tile SET secondary_function = field_function""")
cr.execute("""UPDATE tile_tile SET secondary_field_id = field_id""")

26
web_dashboard_tile/migrations/8.0.4.0/post-migration.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# © 2016 Iván Todorovich <ivan.todorovich@gmail.com>
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
def migrate(cr, version):
if version is None:
return
# Update ir.rule
cr.execute("""
SELECT res_id FROM ir_model_data
WHERE name = 'model_tile_rule'
AND module = 'web_dashboard_tile'""")
rule_id = cr.fetchone()[0]
new_domain = """[
"|",
("user_id","=",user.id),
("user_id","=",False),
"|",
("group_ids","=",False),
("group_ids","in",[g.id for g in user.groups_id]),
]"""
cr.execute("""
UPDATE ir_rule SET domain_force = '%(domain)s'
WHERE id = '%(id)s' """ % {'domain': new_domain, 'id': rule_id})

24
web_dashboard_tile/models/__init__.py

@ -1,23 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2015-Today GRAP
# @author Sylvain LE GAL (https://twitter.com/legalsylvain)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# © 2010-2013 OpenERP s.a. (<http://openerp.com>).
# © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# © 2015-Today GRAP
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from . import tile_tile from . import tile_tile

364
web_dashboard_tile/models/tile_tile.py

@ -1,89 +1,220 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>).
# Copyright (C) 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# Copyright (C) 2015-Today GRAP
# Author Markus Schneider <markus.schneider at initos.com>
# @author Sylvain LE GAL (https://twitter.com/legalsylvain)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import api, fields
from openerp.models import Model
from openerp.exceptions import except_orm
# © 2010-2013 OpenERP s.a. (<http://openerp.com>).
# © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# © 2015-Today GRAP
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
import datetime
import time
from dateutil.relativedelta import relativedelta
from collections import OrderedDict
from openerp import api, fields, models
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.exceptions import ValidationError, except_orm
def median(vals):
# https://docs.python.org/3/library/statistics.html#statistics.median
# TODO : refactor, using statistics.median when Odoo will be available
# in Python 3.4
even = (0 if len(vals) % 2 else 1) + 1
half = (len(vals) - 1) / 2
return sum(sorted(vals)[half:half + even]) / float(even)
FIELD_FUNCTIONS = OrderedDict([
('count', {
'name': 'Count',
'func': False, # its hardcoded in _compute_data
'help': _('Number of records')}),
('min', {
'name': 'Minimum',
'func': min,
'help': _("Minimum value of '%s'")}),
('max', {
'name': 'Maximum',
'func': max,
'help': _("Maximum value of '%s'")}),
('sum', {
'name': 'Sum',
'func': sum,
'help': _("Total value of '%s'")}),
('avg', {
'name': 'Average',
'func': lambda vals: sum(vals) / len(vals),
'help': _("Minimum value of '%s'")}),
('median', {
'name': 'Median',
'func': median,
'help': _("Median value of '%s'")}),
])
class TileTile(Model):
FIELD_FUNCTION_SELECTION = [
(k, FIELD_FUNCTIONS[k].get('name')) for k in FIELD_FUNCTIONS]
class TileTile(models.Model):
_name = 'tile.tile' _name = 'tile.tile'
_description = 'Dashboard Tile'
_order = 'sequence, name' _order = 'sequence, name'
def median(self, aList):
# https://docs.python.org/3/library/statistics.html#statistics.median
# TODO : refactor, using statistics.median when Odoo will be available
# in Python 3.4
even = (0 if len(aList) % 2 else 1) + 1
half = (len(aList) - 1) / 2
return sum(sorted(aList)[half:half + even]) / float(even)
def _get_tile_info(self):
ima_obj = self.env['ir.model.access']
res = {}
for r in self:
r.active = False
r.count = 0
r.computed_value = 0
r.helper = ''
if ima_obj.check(r.model_id.model, 'read', False):
# Compute count item
model = self.env[r.model_id.model]
r.count = model.search_count(eval(r.domain))
r.active = True
# Compute datas for field_id depending of field_function
if r.field_function and r.field_id and r.count != 0:
records = model.search(eval(r.domain))
vals = [x[r.field_id.name] for x in records]
desc = r.field_id.field_description
if r.field_function == 'min':
r.computed_value = min(vals)
r.helper = _("Minimum value of '%s'") % desc
elif r.field_function == 'max':
r.computed_value = max(vals)
r.helper = _("Maximum value of '%s'") % desc
elif r.field_function == 'sum':
r.computed_value = sum(vals)
r.helper = _("Total value of '%s'") % desc
elif r.field_function == 'avg':
r.computed_value = sum(vals) / len(vals)
r.helper = _("Average value of '%s'") % desc
elif r.field_function == 'median':
r.computed_value = self.median(vals)
r.helper = _("Median value of '%s'") % desc
return res
def _get_eval_context(self):
def _context_today():
return fields.Date.from_string(fields.Date.context_today(self))
context = self.env.context.copy()
context.update({
'time': time,
'datetime': datetime,
'relativedelta': relativedelta,
'context_today': _context_today,
'current_date': fields.Date.today(),
})
return context
# Column Section
name = fields.Char(required=True)
sequence = fields.Integer(default=0, required=True)
user_id = fields.Many2one('res.users', 'User')
background_color = fields.Char(default='#0E6C7E', oldname='color')
font_color = fields.Char(default='#FFFFFF')
group_ids = fields.Many2many(
'res.groups',
string='Groups',
help='If this field is set, only users of this group can view this '
'tile. Please note that it will only work for global tiles '
'(that is, when User field is left empty)')
model_id = fields.Many2one('ir.model', 'Model', required=True)
domain = fields.Text(default='[]')
action_id = fields.Many2one('ir.actions.act_window', 'Action')
active = fields.Boolean(
compute='_compute_active',
search='_search_active',
readonly=True)
# Primary Value
primary_function = fields.Selection(
FIELD_FUNCTION_SELECTION,
string='Function',
default='count')
primary_field_id = fields.Many2one(
'ir.model.fields',
string='Field',
domain="[('model_id', '=', model_id),"
" ('ttype', 'in', ['float', 'integer', 'monetary'])]")
primary_format = fields.Char(
string='Format',
help='Python Format String valid with str.format()\n'
'ie: \'{:,} Kgs\' will output \'1,000 Kgs\' if value is 1000.')
primary_value = fields.Char(
string='Value',
compute='_compute_data')
primary_helper = fields.Char(
string='Helper',
compute='_compute_helper')
# Secondary Value
secondary_function = fields.Selection(
FIELD_FUNCTION_SELECTION,
string='Secondary Function')
secondary_field_id = fields.Many2one(
'ir.model.fields',
string='Secondary Field',
domain="[('model_id', '=', model_id),"
" ('ttype', 'in', ['float', 'integer', 'monetary'])]")
secondary_format = fields.Char(
string='Secondary Format',
help='Python Format String valid with str.format()\n'
'ie: \'{:,} Kgs\' will output \'1,000 Kgs\' if value is 1000.')
secondary_value = fields.Char(
string='Secondary Value',
compute='_compute_data')
secondary_helper = fields.Char(
string='Secondary Helper',
compute='_compute_helper')
error = fields.Char(
string='Error Details',
compute='_compute_data')
@api.one
def _compute_data(self):
if not self.active:
return
model = self.env[self.model_id.model]
eval_context = self._get_eval_context()
domain = self.domain or '[]'
try:
count = model.search_count(eval(domain, eval_context))
except Exception as e:
self.primary_value = self.secondary_value = 'ERR!'
self.error = str(e)
return
if any([
self.primary_function and
self.primary_function != 'count',
self.secondary_function and
self.secondary_function != 'count'
]):
records = model.search(eval(domain, eval_context))
for f in ['primary_', 'secondary_']:
f_function = f + 'function'
f_field_id = f + 'field_id'
f_format = f + 'format'
f_value = f + 'value'
value = 0
if self[f_function] == 'count':
value = count
elif self[f_function]:
func = FIELD_FUNCTIONS[self[f_function]]['func']
if func and self[f_field_id] and count:
vals = [x[self[f_field_id].name] for x in records]
value = func(vals)
if self[f_function]:
try:
self[f_value] = (self[f_format] or '{:,}').format(value)
except ValueError as e:
self[f_value] = 'F_ERR!'
self.error = str(e)
return
else:
self[f_value] = False
@api.one
@api.onchange('primary_function', 'primary_field_id',
'secondary_function', 'secondary_field_id')
def _compute_helper(self):
for f in ['primary_', 'secondary_']:
f_function = f + 'function'
f_field_id = f + 'field_id'
f_helper = f + 'helper'
self[f_helper] = ''
field_func = FIELD_FUNCTIONS.get(self[f_function], {})
help = field_func.get('help', False)
if help:
if self[f_function] != 'count' and self[f_field_id]:
desc = self[f_field_id].field_description
self[f_helper] = help % desc
else:
self[f_helper] = help
@api.one
def _compute_active(self):
ima = self.env['ir.model.access']
for rec in self:
rec.active = ima.check(rec.model_id.model, 'read', False)
def _search_active(self, operator, value): def _search_active(self, operator, value):
cr = self.env.cr cr = self.env.cr
if operator != '=': if operator != '=':
raise except_orm( raise except_orm(
'Unimplemented Feature',
'Search on Active field disabled.')
ima_obj = self.env['ir.model.access']
_('Unimplemented Feature. Search on Active field disabled.'))
ima = self.env['ir.model.access']
ids = [] ids = []
cr.execute(""" cr.execute("""
SELECT tt.id, im.model SELECT tt.id, im.model
@ -91,65 +222,37 @@ class TileTile(Model):
INNER JOIN ir_model im INNER JOIN ir_model im
ON tt.model_id = im.id""") ON tt.model_id = im.id""")
for result in cr.fetchall(): for result in cr.fetchall():
if (ima_obj.check(result[1], 'read', False) == value):
if (ima.check(result[1], 'read', False) == value):
ids.append(result[0]) ids.append(result[0])
return [('id', 'in', ids)] return [('id', 'in', ids)]
# Column Section
name = fields.Char(required=True)
model_id = fields.Many2one(
comodel_name='ir.model', string='Model', required=True)
user_id = fields.Many2one(
comodel_name='res.users', string='User')
domain = fields.Text(default='[]')
action_id = fields.Many2one(
comodel_name='ir.actions.act_window', string='Action')
count = fields.Integer(compute='_get_tile_info')
computed_value = fields.Float(compute='_get_tile_info')
helper = fields.Char(compute='_get_tile_info')
field_function = fields.Selection(selection=[
('min', 'Minimum'),
('max', 'Maximum'),
('sum', 'Sum'),
('avg', 'Average'),
('median', 'Median'),
], string='Function')
field_id = fields.Many2one(
comodel_name='ir.model.fields', string='Field',
domain="[('model_id', '=', model_id),"
" ('ttype', 'in', ['float', 'int'])]")
active = fields.Boolean(
compute='_get_tile_info', readonly=True, search='_search_active')
background_color = fields.Char(default='#0E6C7E', oldname='color')
font_color = fields.Char(default='#FFFFFF')
sequence = fields.Integer(default=0, required=True)
# Constraints and onchanges
@api.multi
@api.constrains('model_id', 'primary_field_id', 'secondary_field_id')
def _check_model_id_field_id(self):
for rec in self:
if any([
rec.primary_field_id and
rec.primary_field_id.model_id.id != rec.model_id.id,
rec.secondary_field_id and
rec.secondary_field_id.model_id.id != rec.model_id.id
]):
raise ValidationError(
_("Please select a field from the selected model."))
@api.onchange('model_id')
def _onchange_model_id(self):
self.primary_field_id = False
self.secondary_field_id = False
@api.onchange('primary_function', 'secondary_function')
def _onchange_function(self):
if self.primary_function in [False, 'count']:
self.primary_field_id = False
if self.secondary_function in [False, 'count']:
self.secondary_field_id = False
# Constraint Section
def _check_model_id_field_id(self, cr, uid, ids, context=None):
for t in self.browse(cr, uid, ids, context=context):
if t.field_id and t.field_id.model_id.id != t.model_id.id:
return False
return True
def _check_field_id_field_function(self, cr, uid, ids, context=None):
for t in self.browse(cr, uid, ids, context=context):
if t.field_id and not t.field_function or\
t.field_function and not t.field_id:
return False
return True
_constraints = [
(
_check_model_id_field_id,
"Error ! Please select a field of the selected model.",
['model_id', 'field_id']),
(
_check_field_id_field_function,
"Error ! Please set both fields: 'Field' and 'Function'.",
['field_id', 'field_function']),
]
# View / action Section
# Action methods
@api.multi @api.multi
def open_link(self): def open_link(self):
res = { res = {
@ -166,8 +269,7 @@ class TileTile(Model):
} }
if self.action_id: if self.action_id:
res.update(self.action_id.read( res.update(self.action_id.read(
['view_type', 'view_mode', 'view_id', 'type'])[0])
# FIXME: restore original Domain + Filter would be better
['view_type', 'view_mode', 'type'])[0])
return res return res
@api.model @api.model

11
web_dashboard_tile/security/rules.xml

@ -6,7 +6,16 @@
<field name="name">tile.owner</field> <field name="name">tile.owner</field>
<field name="model_id" ref="model_tile_tile" /> <field name="model_id" ref="model_tile_tile" />
<field name="groups" eval="[(4, ref('base.group_user'))]"/> <field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="domain_force">[('user_id','in',[False,user.id])]</field>
<field name="domain_force">
[
'|',
('user_id','=',user.id),
('user_id','=',False),
'|',
('group_ids','=',False),
('group_ids','in',[g.id for g in user.groups_id]),
]
</field>
</record> </record>
</data> </data>

55
web_dashboard_tile/static/src/css/tile.css

@ -1,44 +1,59 @@
.openerp .oe_kanban_view .oe_dashbaord_tile{
.openerp .oe_kanban_view .oe_dashboard_tile {
width: 150px; width: 150px;
height: 150px; height: 150px;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
} }
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_label,
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_count_without_computed_value,
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_count_with_computed_value,
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_computed_value {
width: 140px;
/* Disable default kanban style */
.openerp .oe_kanban_view .oe_dashboard_tile .oe_kanban_content div:first-child {
margin-right: inherit!important;
}
.openerp .oe_kanban_view .oe_dashboard_tile .tile_label,
.openerp .oe_kanban_view .oe_dashboard_tile .tile_primary_value,
.openerp .oe_kanban_view .oe_dashboard_tile .tile_secondary_value {
text-align: center; text-align: center;
font-weight: bold;
} }
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_label{
.openerp .oe_kanban_view .oe_dashboard_tile .tile_label {
padding: 5px; padding: 5px;
font-size: 15px; font-size: 15px;
} }
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_count_without_computed_value{
font-size: 52px;
font-weight: bold;
.openerp .oe_kanban_view .oe_dashboard_tile .tile_primary_value{
font-size: 54px;
position: absolute; position: absolute;
left: 5px; left: 5px;
right: 5px;
bottom: 5px; bottom: 5px;
} }
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_count_with_computed_value{
font-size: 38px;
font-weight: bold;
.openerp .oe_kanban_view .oe_dashboard_tile .tile_secondary_value{
display: none;
font-size: 18px;
font-style: italic;
position: absolute; position: absolute;
left: 5px; left: 5px;
right: 5px;
bottom: 5px;
}
.openerp .oe_kanban_view .oe_dashboard_tile .with_secondary .tile_primary_value{
font-size: 38px;
bottom: 30px; bottom: 30px;
} }
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_computed_value{
font-size: 18px;
font-weight: bold;
position: absolute;
right: 10px;
bottom: 5px;
font-style: italic;
.openerp .oe_kanban_view .oe_dashboard_tile .with_secondary .tile_secondary_value{
display: block;
}
/* SearchView Drawer */
.openerp .oe_searchview_drawer .oe_searchview_dashboard .oe_dashboard_tile_form {
display: none;
}
.openerp .oe_searchview_drawer .oe_opened .oe_dashboard_tile_form {
display: block;
} }

BIN
web_dashboard_tile/static/src/img/screenshot_dashboard.png

Before

Width: 796  |  Height: 162  |  Size: 45 KiB

After

Width: 771  |  Height: 155  |  Size: 20 KiB

15
web_dashboard_tile/static/src/js/custom_js.js

@ -19,11 +19,12 @@
// //
//############################################################################# //#############################################################################
openerp.web_dashboard_tile = function (instance)
odoo.web_dashboard_tile = function (require)
{ {
var QWeb = instance.web.qweb,
_t = instance.web._t,
_lt = instance.web._lt;
var QWeb = require('web.qweb')
var _t = require('web._t')
var _lt = require('web._lt')
_.mixin({ _.mixin({
sum: function (obj) { return _.reduce(obj, function (a, b) { return a + b; }, 0); } sum: function (obj) { return _.reduce(obj, function (a, b) { return a + b; }, 0); }
}); });
@ -35,13 +36,13 @@ _.mixin({
var self = this; var self = this;
this.$('#add_dashboard_tile').on('click', this, function (){ this.$('#add_dashboard_tile').on('click', this, function (){
self.save_tile(); self.save_tile();
})
});
}, },
render_data: function(dashboard_choices){ render_data: function(dashboard_choices){
var selection = instance.web.qweb.render( var selection = instance.web.qweb.render(
"SearchView.addtodashboard.selection", { "SearchView.addtodashboard.selection", {
selections: dashboard_choices}); selections: dashboard_choices});
this.$("form input").before(selection)
this.$("form input").before(selection);
}, },
save_tile: function () { save_tile: function () {
var self = this; var self = this;
@ -97,4 +98,4 @@ _.mixin({
} }
}); });
}
};

2
web_dashboard_tile/static/src/xml/custom_xml.xml

@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve"> <templates id="template" xml:space="preserve">
<t t-extend="SearchView.addtodashboard"> <t t-extend="SearchView.addtodashboard">
<t t-jquery="form" t-operation="after"> <t t-jquery="form" t-operation="after">
<div>
<div class="oe_dashboard_tile_form">
<label for="dashboard_tile_new_name">Tile:</label> <label for="dashboard_tile_new_name">Tile:</label>
<input id="dashboard_tile_new_name" /> <input id="dashboard_tile_new_name" />
<button id="add_dashboard_tile">Create</button> <button id="add_dashboard_tile">Create</button>

6
web_dashboard_tile/tests/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# flake8: noqa
from . import test_tile

52
web_dashboard_tile/tests/test_tile.py

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp.tests.common import TransactionCase
class TestTile(TransactionCase):
def test_tile(self):
tile_obj = self.env['tile.tile']
model_id = self.env['ir.model'].search([
('model', '=', 'tile.tile')])
field_id = self.env['ir.model.fields'].search([
('model_id', '=', model_id.id),
('name', '=', 'sequence')])
self.tile1 = tile_obj.create({
'name': 'Count / Sum',
'sequence': 1,
'model_id': model_id.id,
'domain': "[('model_id', '=', %d)]" % model_id.id,
'secondary_function': 'sum',
'secondary_field_id': field_id.id})
self.tile2 = tile_obj.create({
'name': 'Min / Max',
'sequence': 2,
'model_id': model_id.id,
'domain': "[('model_id', '=', %d)]" % model_id.id,
'primary_function': 'min',
'primary_field_id': field_id.id,
'secondary_function': 'max',
'secondary_field_id': field_id.id})
self.tile3 = tile_obj.create({
'name': 'Avg / Median',
'sequence': 3,
'model_id': model_id.id,
'domain': "[('model_id', '=', %d)]" % model_id.id,
'primary_function': 'avg',
'primary_field_id': field_id.id,
'secondary_function': 'median',
'secondary_field_id': field_id.id})
# count
self.assertEqual(self.tile1.primary_value, '3')
# sum
self.assertEqual(self.tile1.secondary_value, '6')
# min
self.assertEqual(self.tile2.primary_value, '1')
# max
self.assertEqual(self.tile2.secondary_value, '3')
# average
self.assertEqual(self.tile3.primary_value, '2')
# median
self.assertEqual(self.tile3.secondary_value, '2.0')

95
web_dashboard_tile/views/tile.xml

@ -9,8 +9,10 @@
<field name="name"/> <field name="name"/>
<field name="domain"/> <field name="domain"/>
<field name="model_id"/> <field name="model_id"/>
<field name="field_function"/>
<field name="field_id"/>
<field name="primary_function"/>
<field name="primary_field_id"/>
<field name="secondary_function"/>
<field name="secondary_field_id"/>
<field name="user_id"/> <field name="user_id"/>
<field name="background_color" widget="color"/> <field name="background_color" widget="color"/>
</tree> </tree>
@ -34,11 +36,50 @@
<field name="model_id"/> <field name="model_id"/>
<field name="action_id"/> <field name="action_id"/>
<field name="domain" colspan="4"/> <field name="domain" colspan="4"/>
<separator string="Optional Field Informations" colspan="4"/>
<field name="field_function"/>
<field name="field_id"/>
<field name="helper" colspan="4"/>
<separator colspan="4"/>
<field name="error" attrs="{'invisible':[('error','=',False)]}"/>
</group> </group>
<notebook>
<page string="Main Value">
<group>
<group>
<field name="primary_function"/>
<field name="primary_field_id" attrs="{
'invisible':[('primary_function','in',[False,'count'])],
'required':[('primary_function','not in',[False,'count'])],
}"/>
</group>
<group>
<field name="primary_format"/>
</group>
<group>
<field name="primary_helper"/>
<field name="primary_value" attrs="{'invisible':[('primary_value','=',False)]}"/>
</group>
</group>
</page>
<page string="Secondary Value">
<group>
<group>
<field name="secondary_function"/>
<field name="secondary_field_id" attrs="{
'invisible':[('secondary_function','in',[False,'count'])],
'required':[('secondary_function','not in',[False,'count'])],
}"/>
</group>
<group>
<field name="secondary_format"/>
</group>
<group>
<field name="secondary_helper"/>
<field name="secondary_value" attrs="{'invisible':[('secondary_value','=',False)]}"/>
</group>
</group>
</page>
<page string="Groups">
<field name="group_ids"/>
</page>
</notebook>
</sheet> </sheet>
</form> </form>
</field> </field>
@ -53,37 +94,31 @@
<field name="domain"/> <field name="domain"/>
<field name="model_id"/> <field name="model_id"/>
<field name="action_id"/> <field name="action_id"/>
<field name="count"/>
<field name="background_color"/> <field name="background_color"/>
<field name="font_color"/> <field name="font_color"/>
<field name="field_id" />
<field name="field_function" />
<field name="helper" />
<field name="primary_function"/>
<field name="primary_helper"/>
<field name="secondary_function"/>
<field name="secondary_helper"/>
<templates> <templates>
<t t-name="kanban-box"> <t t-name="kanban-box">
<div t-attf-class="oe_dashbaord_tile oe_kanban_global_click" t-attf-style="background-color:#{record.background_color.raw_value}" >
<div t-attf-class="oe_dashboard_tile oe_kanban_global_click" t-attf-style="background-color:#{record.background_color.raw_value}" >
<div class="oe_kanban_content"> <div class="oe_kanban_content">
<a type="object" name="open_link" args="[]" t-attf-style="color:#{record.font_color.raw_value};"> <a type="object" name="open_link" args="[]" t-attf-style="color:#{record.font_color.raw_value};">
<div class="tile_label">
<b><field name="name"/></b>
</div>
<div style="padding-left: 0.5em; height: 115px;">
</div>
<t t-if="record.field_id.raw_value != '' and record.field_function.raw_value != '' and record.count.raw_value !=0">
<div class="tile_count_with_computed_value">
<span><field name="count"/></span>
<div style="height:100%;" t-att-class="record.secondary_function.raw_value and 'with_secondary' or 'simple'">
<div class="tile_label">
<field name="name"/>
</div> </div>
<div class="tile_computed_value" t-att-title="record.helper.raw_value">
<img t-att-src="_s + '/web_dashboard_tile/static/src/img/' + record.field_function.raw_value + '.png'"/>
<span><field name="computed_value"/></span>
<div class="tile_primary_value" t-att-title="record.primary_helper.raw_value">
<t t-set="l" t-value="record.primary_value.raw_value.length" />
<t t-set="s" t-value="l>=12 and 35 or l>=10 and 45 or l>=8 and 55 or l>=6 and 75 or l>4 and 85 or 100"/>
<span t-attf-style="font-size: #{s}%;"><field name="primary_value"/></span>
</div> </div>
</t>
<t t-if="!(record.field_id.raw_value != '' and record.field_function.raw_value != '' and record.count.raw_value !=0)">
<div class="tile_count_without_computed_value">
<span><field name="count"/></span>
<div class="tile_secondary_value" t-att-title="record.secondary_helper.raw_value">
<span><field name="secondary_value"/></span>
</div> </div>
</t>
</div>
</a> </a>
</div> </div>
<div class="oe_clear"></div> <div class="oe_clear"></div>
@ -117,9 +152,9 @@
<record id="mail_dashboard" model="ir.ui.menu"> <record id="mail_dashboard" model="ir.ui.menu">
<field name="name">Dashboard</field> <field name="name">Dashboard</field>
<field name="sequence" eval="9"/>
<field name="sequence" eval="0"/>
<field name="action" ref="action_kanban_dashboard_tile"/> <field name="action" ref="action_kanban_dashboard_tile"/>
<field name="parent_id" ref="mail.mail_feeds"/>
<field name="parent_id" ref=""/>
</record> </record>
</data> </data>

Loading…
Cancel
Save