3
0
Fork 0

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

9.0 mig web dashboard tile
9.0
Moises Lopez - https://www.vauxoo.com/ 2018-01-24 10:40:43 -07:00 committed by GitHub
commit 9db404662d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 506 additions and 284 deletions

View File

@ -1,36 +1,48 @@
Add Tiles to Dashboard Dashboard Tiles
====================== ===============
module to give you a dashboard where you can configure tile from any view Adds a dashboard where you can configure tiles from any view and add them as short cut.
and add them as short cut.
* Tile can be: By default, the tile displays items count of a given model restricted to a given domain.
* displayed only for a user;
* global for all users (In that case, some tiles will be hidden if Optionally, the tile can display the result of a function on a field.
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; - Function is one of `sum`, `avg`, `min`, `max` or `median`.
* Optionnaly, the tile can display the result of a function of a field; - Field must be integer or float.
* Function is one of sum/avg/min/max/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, ...); Known issues
* Context are ignored; ============
* Date filter can not be relative; * Can not edit tile from dashboard (color, sequence, function, ...).
* Combine domain of menue and filter so can not restore origin filter; * Original context is ignored.
* Support context_today; * Original domain and filter are not restored.
* Add icons; * 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`).
* Support client side action (like inbox);
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/ `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**>`_.
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**>`_.
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
---------- ----------

View File

@ -1,26 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## # © 2010-2013 OpenERP s.a. (<http://openerp.com>).
# # © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# OpenERP, Open Source Management Solution # © 2015-Today GRAP
# Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>). # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# 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 . import models from . import models

View File

@ -1,38 +1,27 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## # © 2010-2013 OpenERP s.a. (<http://openerp.com>).
# # © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# OpenERP, Open Source Management Solution # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# 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/>.
#
##############################################################################
{ {
"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,
} }

View File

@ -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 secondary_function: max
field_id: base.field_res_currency_rate secondary_field_id: base.field_res_currency_rate

View File

@ -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""")

View File

@ -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})

View File

@ -1,23 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## # © 2010-2013 OpenERP s.a. (<http://openerp.com>).
# # © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# OpenERP, Open Source Management Solution # © 2015-Today GRAP
# Copyright (C) 2015-Today GRAP # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# @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 . import tile_tile from . import tile_tile

View File

@ -1,89 +1,220 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## # © 2010-2013 OpenERP s.a. (<http://openerp.com>).
# # © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# OpenERP, Open Source Management Solution # © 2015-Today GRAP
# Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>). # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# 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 import datetime
from openerp.models import Model import time
from openerp.exceptions import except_orm 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
class TileTile(Model): 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'")}),
])
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): def _get_eval_context(self):
# https://docs.python.org/3/library/statistics.html#statistics.median def _context_today():
# TODO : refactor, using statistics.median when Odoo will be available return fields.Date.from_string(fields.Date.context_today(self))
# in Python 3.4 context = self.env.context.copy()
even = (0 if len(aList) % 2 else 1) + 1 context.update({
half = (len(aList) - 1) / 2 'time': time,
return sum(sorted(aList)[half:half + even]) / float(even) 'datetime': datetime,
'relativedelta': relativedelta,
'context_today': _context_today,
'current_date': fields.Date.today(),
})
return context
def _get_tile_info(self): # Column Section
ima_obj = self.env['ir.model.access'] name = fields.Char(required=True)
res = {} sequence = fields.Integer(default=0, required=True)
for r in self: user_id = fields.Many2one('res.users', 'User')
r.active = False background_color = fields.Char(default='#0E6C7E', oldname='color')
r.count = 0 font_color = fields.Char(default='#FFFFFF')
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 group_ids = fields.Many2many(
if r.field_function and r.field_id and r.count != 0: 'res.groups',
records = model.search(eval(r.domain)) string='Groups',
vals = [x[r.field_id.name] for x in records] help='If this field is set, only users of this group can view this '
desc = r.field_id.field_description 'tile. Please note that it will only work for global tiles '
if r.field_function == 'min': '(that is, when User field is left empty)')
r.computed_value = min(vals)
r.helper = _("Minimum value of '%s'") % desc model_id = fields.Many2one('ir.model', 'Model', required=True)
elif r.field_function == 'max': domain = fields.Text(default='[]')
r.computed_value = max(vals) action_id = fields.Many2one('ir.actions.act_window', 'Action')
r.helper = _("Maximum value of '%s'") % desc
elif r.field_function == 'sum': active = fields.Boolean(
r.computed_value = sum(vals) compute='_compute_active',
r.helper = _("Total value of '%s'") % desc search='_search_active',
elif r.field_function == 'avg': readonly=True)
r.computed_value = sum(vals) / len(vals)
r.helper = _("Average value of '%s'") % desc # Primary Value
elif r.field_function == 'median': primary_function = fields.Selection(
r.computed_value = self.median(vals) FIELD_FUNCTION_SELECTION,
r.helper = _("Median value of '%s'") % desc string='Function',
return res 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', _('Unimplemented Feature. Search on Active field disabled.'))
'Search on Active field disabled.') ima = self.env['ir.model.access']
ima_obj = 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 # Constraints and onchanges
name = fields.Char(required=True) @api.multi
model_id = fields.Many2one( @api.constrains('model_id', 'primary_field_id', 'secondary_field_id')
comodel_name='ir.model', string='Model', required=True) def _check_model_id_field_id(self):
user_id = fields.Many2one( for rec in self:
comodel_name='res.users', string='User') if any([
domain = fields.Text(default='[]') rec.primary_field_id and
action_id = fields.Many2one( rec.primary_field_id.model_id.id != rec.model_id.id,
comodel_name='ir.actions.act_window', string='Action') rec.secondary_field_id and
count = fields.Integer(compute='_get_tile_info') rec.secondary_field_id.model_id.id != rec.model_id.id
computed_value = fields.Float(compute='_get_tile_info') ]):
helper = fields.Char(compute='_get_tile_info') raise ValidationError(
field_function = fields.Selection(selection=[ _("Please select a field from the selected model."))
('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)
# Constraint Section @api.onchange('model_id')
def _check_model_id_field_id(self, cr, uid, ids, context=None): def _onchange_model_id(self):
for t in self.browse(cr, uid, ids, context=context): self.primary_field_id = False
if t.field_id and t.field_id.model_id.id != t.model_id.id: self.secondary_field_id = False
return False
return True
def _check_field_id_field_function(self, cr, uid, ids, context=None): @api.onchange('primary_function', 'secondary_function')
for t in self.browse(cr, uid, ids, context=context): def _onchange_function(self):
if t.field_id and not t.field_function or\ if self.primary_function in [False, 'count']:
t.field_function and not t.field_id: self.primary_field_id = False
return False if self.secondary_function in [False, 'count']:
return True self.secondary_field_id = False
_constraints = [ # Action methods
(
_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
@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]) ['view_type', 'view_mode', 'type'])[0])
# FIXME: restore original Domain + Filter would be better
return res return res
@api.model @api.model

View File

@ -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>

View File

@ -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, /* Disable default kanban style */
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_count_without_computed_value, .openerp .oe_kanban_view .oe_dashboard_tile .oe_kanban_content div:first-child {
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_count_with_computed_value, margin-right: inherit!important;
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_computed_value {
width: 140px;
text-align: center;
} }
.openerp .oe_kanban_view .oe_dashbaord_tile .tile_label{ .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;
font-weight: bold;
}
.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{ .openerp .oe_kanban_view .oe_dashboard_tile .tile_primary_value{
font-size: 52px; font-size: 54px;
font-weight: bold;
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{ .openerp .oe_kanban_view .oe_dashboard_tile .tile_secondary_value{
font-size: 38px; display: none;
font-weight: bold; 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{ .openerp .oe_kanban_view .oe_dashboard_tile .with_secondary .tile_secondary_value{
font-size: 18px; display: block;
font-weight: bold; }
position: absolute;
right: 10px; /* SearchView Drawer */
bottom: 5px; .openerp .oe_searchview_drawer .oe_searchview_dashboard .oe_dashboard_tile_form {
font-style: italic; display: none;
}
.openerp .oe_searchview_drawer .oe_opened .oe_dashboard_tile_form {
display: block;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -19,11 +19,12 @@
// //
//############################################################################# //#############################################################################
openerp.web_dashboard_tile = function (instance) odoo.web_dashboard_tile = function (require)
{ {
var QWeb = instance.web.qweb, var QWeb = require('web.qweb')
_t = instance.web._t, var _t = require('web._t')
_lt = instance.web._lt; 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({
} }
}); });
} };

View File

@ -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>

View File

@ -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

View File

@ -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')

View File

@ -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="primary_function"/>
<field name="field_id"/> <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"/> <separator colspan="4"/>
<field name="field_function"/> <field name="error" attrs="{'invisible':[('error','=',False)]}"/>
<field name="field_id"/>
<field name="helper" colspan="4"/>
</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="primary_function"/>
<field name="field_function" /> <field name="primary_helper"/>
<field name="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"> <div style="height:100%;" t-att-class="record.secondary_function.raw_value and 'with_secondary' or 'simple'">
<b><field name="name"/></b> <div class="tile_label">
<field name="name"/>
</div>
<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 class="tile_secondary_value" t-att-title="record.secondary_helper.raw_value">
<span><field name="secondary_value"/></span>
</div>
</div> </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>
<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>
</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>
</t>
</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>