diff --git a/web_dashboard_tile/README.rst b/web_dashboard_tile/README.rst index c87fb47d6..daa3a2ebf 100644 --- a/web_dashboard_tile/README.rst +++ b/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 ===== * 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: -.. 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 =========== @@ -38,10 +50,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. 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 -`here `_. +`here `_. Credits @@ -52,6 +61,7 @@ Contributors * Markus Schneider * Sylvain Le Gal (https://twitter.com/legalsylvain) +* Iván Todorovich Maintainer ---------- diff --git a/web_dashboard_tile/__init__.py b/web_dashboard_tile/__init__.py index 35d8f47b1..1d098b583 100644 --- a/web_dashboard_tile/__init__.py +++ b/web_dashboard_tile/__init__.py @@ -1,26 +1,7 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2010-2013 OpenERP s.a. (). -# Copyright (C) 2014 initOS GmbH & Co. KG (). -# Copyright (C) 2015-Today GRAP -# Author Markus Schneider -# @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 . -# -############################################################################## +# © 2010-2013 OpenERP s.a. (). +# © 2014 initOS GmbH & Co. KG (). +# © 2015-Today GRAP +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html from . import models diff --git a/web_dashboard_tile/__openerp__.py b/web_dashboard_tile/__openerp__.py index 3e636b152..dbda3d3b7 100644 --- a/web_dashboard_tile/__openerp__.py +++ b/web_dashboard_tile/__openerp__.py @@ -1,38 +1,27 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2010-2013 OpenERP s.a. (). -# Copyright (C) 2014 initOS GmbH & Co. KG (). -# Author Markus Schneider -# -# 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 . -# -############################################################################## +# © 2010-2013 OpenERP s.a. (). +# © 2014 initOS GmbH & Co. KG (). +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html { "name": "Dashboard Tile", "summary": "Add Tiles to Dashboard", - "version": "8.0.1.0.0", + "version": "9.0.1.0.0", "depends": [ 'web', 'board', 'mail', '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", 'license': 'AGPL-3', + 'contributors': [ + 'initOS GmbH & Co. KG', + 'GRAP', + 'Iván Todorovich ' + ], 'data': [ 'views/tile.xml', 'views/templates.xml', @@ -46,5 +35,4 @@ 'qweb': [ 'static/src/xml/custom_xml.xml', ], - 'installable': False, } diff --git a/web_dashboard_tile/demo/tile_tile.yml b/web_dashboard_tile/demo/tile_tile.yml index 6b23b71bd..dd9da0165 100644 --- a/web_dashboard_tile/demo/tile_tile.yml +++ b/web_dashboard_tile/demo/tile_tile.yml @@ -36,5 +36,5 @@ name: Currencies (Max Rate) model_id: base.model_res_currency domain: [] - field_function: max - field_id: base.field_res_currency_rate + secondary_function: max + secondary_field_id: base.field_res_currency_rate diff --git a/web_dashboard_tile/migrations/8.0.3.0/post-migration.py b/web_dashboard_tile/migrations/8.0.3.0/post-migration.py new file mode 100644 index 000000000..7cfcc1c00 --- /dev/null +++ b/web_dashboard_tile/migrations/8.0.3.0/post-migration.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# © 2016 Iván Todorovich +# 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""") diff --git a/web_dashboard_tile/migrations/8.0.4.0/post-migration.py b/web_dashboard_tile/migrations/8.0.4.0/post-migration.py new file mode 100644 index 000000000..570f814e4 --- /dev/null +++ b/web_dashboard_tile/migrations/8.0.4.0/post-migration.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# © 2016 Iván Todorovich +# 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}) diff --git a/web_dashboard_tile/models/__init__.py b/web_dashboard_tile/models/__init__.py index fc23e732e..97fec216c 100644 --- a/web_dashboard_tile/models/__init__.py +++ b/web_dashboard_tile/models/__init__.py @@ -1,23 +1,7 @@ # -*- 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 . -# -############################################################################## +# © 2010-2013 OpenERP s.a. (). +# © 2014 initOS GmbH & Co. KG (). +# © 2015-Today GRAP +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html from . import tile_tile diff --git a/web_dashboard_tile/models/tile_tile.py b/web_dashboard_tile/models/tile_tile.py index 4743afcb5..218cec406 100644 --- a/web_dashboard_tile/models/tile_tile.py +++ b/web_dashboard_tile/models/tile_tile.py @@ -1,89 +1,220 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2010-2013 OpenERP s.a. (). -# Copyright (C) 2014 initOS GmbH & Co. KG (). -# Copyright (C) 2015-Today GRAP -# Author Markus Schneider -# @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 . -# -############################################################################## +# © 2010-2013 OpenERP s.a. (). +# © 2014 initOS GmbH & Co. KG (). +# © 2015-Today GRAP +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html -from openerp import api, fields -from openerp.models import Model -from openerp.exceptions import except_orm +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.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' + _description = 'Dashboard Tile' _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_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 - 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 + # 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') - # 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 + 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): cr = self.env.cr if operator != '=': 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 = [] cr.execute(""" SELECT tt.id, im.model @@ -91,65 +222,37 @@ class TileTile(Model): INNER JOIN ir_model im ON tt.model_id = im.id""") 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]) 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.")) - # 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 + @api.onchange('model_id') + def _onchange_model_id(self): + self.primary_field_id = False + self.secondary_field_id = False - 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 + @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 - _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 def open_link(self): res = { @@ -166,8 +269,7 @@ class TileTile(Model): } if self.action_id: 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 @api.model diff --git a/web_dashboard_tile/security/rules.xml b/web_dashboard_tile/security/rules.xml index 8d653b70c..05637bfdc 100644 --- a/web_dashboard_tile/security/rules.xml +++ b/web_dashboard_tile/security/rules.xml @@ -6,7 +6,16 @@ tile.owner - [('user_id','in',[False,user.id])] + + [ + '|', + ('user_id','=',user.id), + ('user_id','=',False), + '|', + ('group_ids','=',False), + ('group_ids','in',[g.id for g in user.groups_id]), + ] + diff --git a/web_dashboard_tile/static/src/css/tile.css b/web_dashboard_tile/static/src/css/tile.css index 1d057ceea..0620a9c65 100644 --- a/web_dashboard_tile/static/src/css/tile.css +++ b/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; height: 150px; border: 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; - text-align: center; +/* 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_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; 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; left: 5px; + right: 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; left: 5px; + right: 5px; + bottom: 5px; +} + +.openerp .oe_kanban_view .oe_dashboard_tile .with_secondary .tile_primary_value{ + font-size: 38px; 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; } diff --git a/web_dashboard_tile/static/src/img/screenshot_dashboard.png b/web_dashboard_tile/static/src/img/screenshot_dashboard.png index 9cbdeecd3..65cfc1275 100644 Binary files a/web_dashboard_tile/static/src/img/screenshot_dashboard.png and b/web_dashboard_tile/static/src/img/screenshot_dashboard.png differ diff --git a/web_dashboard_tile/static/src/js/custom_js.js b/web_dashboard_tile/static/src/js/custom_js.js index c192ef293..615a74d6b 100644 --- a/web_dashboard_tile/static/src/js/custom_js.js +++ b/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({ sum: function (obj) { return _.reduce(obj, function (a, b) { return a + b; }, 0); } }); @@ -35,13 +36,13 @@ _.mixin({ var self = this; this.$('#add_dashboard_tile').on('click', this, function (){ self.save_tile(); - }) + }); }, render_data: function(dashboard_choices){ var selection = instance.web.qweb.render( "SearchView.addtodashboard.selection", { selections: dashboard_choices}); - this.$("form input").before(selection) + this.$("form input").before(selection); }, save_tile: function () { var self = this; @@ -97,4 +98,4 @@ _.mixin({ } }); -} +}; diff --git a/web_dashboard_tile/static/src/xml/custom_xml.xml b/web_dashboard_tile/static/src/xml/custom_xml.xml index 3e0c23167..38a0e3e96 100644 --- a/web_dashboard_tile/static/src/xml/custom_xml.xml +++ b/web_dashboard_tile/static/src/xml/custom_xml.xml @@ -2,7 +2,7 @@ -
+
diff --git a/web_dashboard_tile/tests/__init__.py b/web_dashboard_tile/tests/__init__.py new file mode 100644 index 000000000..4f8c7e8bf --- /dev/null +++ b/web_dashboard_tile/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# © 2016 Antonio Espinosa - +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# flake8: noqa + +from . import test_tile diff --git a/web_dashboard_tile/tests/test_tile.py b/web_dashboard_tile/tests/test_tile.py new file mode 100644 index 000000000..90604ab9a --- /dev/null +++ b/web_dashboard_tile/tests/test_tile.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# © 2016 Antonio Espinosa - +# 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') diff --git a/web_dashboard_tile/views/tile.xml b/web_dashboard_tile/views/tile.xml index 36411ad84..a42d952f8 100644 --- a/web_dashboard_tile/views/tile.xml +++ b/web_dashboard_tile/views/tile.xml @@ -9,8 +9,10 @@ - - + + + + @@ -34,11 +36,50 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -53,37 +94,31 @@ - - - - + + + + + -