Browse Source

[IMP] kpi_dashboard: black, isort, prettier

myc-14.0-py3o
Enric Tobella 4 years ago
parent
commit
c90a783925
  1. 85
      kpi_dashboard/demo/demo_dashboard.xml
  2. 95
      kpi_dashboard/models/kpi_dashboard.py
  3. 148
      kpi_dashboard/models/kpi_kpi.py
  4. 18
      kpi_dashboard/security/security.xml
  5. 147
      kpi_dashboard/static/src/js/dashboard_controller.js
  6. 18
      kpi_dashboard/static/src/js/dashboard_model.js
  7. 117
      kpi_dashboard/static/src/js/dashboard_renderer.js
  8. 36
      kpi_dashboard/static/src/js/dashboard_view.js
  9. 69
      kpi_dashboard/static/src/js/field_widget.js
  10. 88
      kpi_dashboard/static/src/js/widget/abstract_widget.js
  11. 10
      kpi_dashboard/static/src/js/widget/counter_widget.js
  12. 91
      kpi_dashboard/static/src/js/widget/graph_widget.js
  13. 57
      kpi_dashboard/static/src/js/widget/integer_widget.js
  14. 33
      kpi_dashboard/static/src/js/widget/meter_widget.js
  15. 17
      kpi_dashboard/static/src/js/widget/number_widget.js
  16. 13
      kpi_dashboard/static/src/js/widget/text_widget.js
  17. 4
      kpi_dashboard/static/src/js/widget_registry.js
  18. 42
      kpi_dashboard/static/src/scss/kpi_dashboard.scss
  19. 51
      kpi_dashboard/static/src/xml/dashboard.xml
  20. 86
      kpi_dashboard/templates/assets.xml
  21. 24
      kpi_dashboard/tests/test_formula.py
  22. 176
      kpi_dashboard/tests/test_kpi_dashboard.py
  23. 155
      kpi_dashboard/views/kpi_dashboard.xml
  24. 180
      kpi_dashboard/views/kpi_kpi.xml
  25. 15
      kpi_dashboard/views/kpi_menu.xml
  26. 25
      kpi_dashboard/wizards/kpi_dashboard_menu.xml

85
kpi_dashboard/demo/demo_dashboard.xml

@ -8,7 +8,6 @@
<field name="background_color">#020202</field> <field name="background_color">#020202</field>
<field name="compute_on_fly_refresh">30</field> <field name="compute_on_fly_refresh">30</field>
</record> </record>
<record id="widget_number_01" model="kpi.kpi"> <record id="widget_number_01" model="kpi.kpi">
<field name="name">Number 01</field> <field name="name">Number 01</field>
<field name="prefix">$</field> <field name="prefix">$</field>
@ -18,7 +17,6 @@
result = {"value": 10000,"previous": 12000} result = {"value": 10000,"previous": 12000}
</field> </field>
</record> </record>
<record id="widget_number_02" model="kpi.kpi"> <record id="widget_number_02" model="kpi.kpi">
<field name="name">Number 02</field> <field name="name">Number 02</field>
<field name="suffix"></field> <field name="suffix"></field>
@ -28,10 +26,11 @@ result = {"value": 10000,"previous": 12000}
result = {"value": 12000,"previous": 10000} result = {"value": 12000,"previous": 10000}
</field> </field>
</record> </record>
<function model="kpi.kpi" name="compute"
eval="[[ref('widget_number_01'), ref('widget_number_02')]]"/>
<function
model="kpi.kpi"
name="compute"
eval="[[ref('widget_number_01'), ref('widget_number_02')]]"
/>
<record id="widget_meter_01" model="kpi.kpi"> <record id="widget_meter_01" model="kpi.kpi">
<field name="name">Meter 01</field> <field name="name">Meter 01</field>
<field name="suffix"></field> <field name="suffix"></field>
@ -41,7 +40,6 @@ result = {"value": 12000,"previous": 10000}
result = {"min": 0, "max": 100, "value": 90} result = {"min": 0, "max": 100, "value": 90}
</field> </field>
</record> </record>
<record id="widget_meter_02" model="kpi.kpi"> <record id="widget_meter_02" model="kpi.kpi">
<field name="name">Meter 02</field> <field name="name">Meter 02</field>
<field name="prefix">$</field> <field name="prefix">$</field>
@ -51,10 +49,11 @@ result = {"min": 0, "max": 100, "value": 90}
result = {"min": 0, "max": 100, "value": 40} result = {"min": 0, "max": 100, "value": 40}
</field> </field>
</record> </record>
<function model="kpi.kpi" name="compute"
eval="[[ref('widget_meter_01'), ref('widget_meter_02')]]"/>
<function
model="kpi.kpi"
name="compute"
eval="[[ref('widget_meter_01'), ref('widget_meter_02')]]"
/>
<record id="widget_graph" model="kpi.kpi"> <record id="widget_graph" model="kpi.kpi">
<field name="name">Graph</field> <field name="name">Graph</field>
<field name="computation_method">code</field> <field name="computation_method">code</field>
@ -84,125 +83,116 @@ result = {"graphs": [
]} ]}
</field> </field>
</record> </record>
<function model="kpi.kpi" name="compute"
eval="[[ref('widget_graph')]]"/>
<function model="kpi.kpi" name="compute" eval="[[ref('widget_graph')]]" />
<record id="widget_integer" model="kpi.kpi"> <record id="widget_integer" model="kpi.kpi">
<field name="name">Integer counter</field> <field name="name">Integer counter</field>
<field name="computation_method">code</field> <field name="computation_method">code</field>
<field name="widget">integer</field> <field name="widget">integer</field>
<field name="compute_on_fly" eval="True"/>
<field name="compute_on_fly" eval="True" />
<field name="code"> <field name="code">
result = {"value": self.env.context.get('counter', 990)} result = {"value": self.env.context.get('counter', 990)}
</field> </field>
</record> </record>
<record id="widget_counter" model="kpi.kpi"> <record id="widget_counter" model="kpi.kpi">
<field name="name">Counter</field> <field name="name">Counter</field>
<field name="computation_method">code</field> <field name="computation_method">code</field>
<field name="widget">counter</field> <field name="widget">counter</field>
<field name="compute_on_fly" eval="True"/>
<field name="compute_on_fly" eval="True" />
<field name="code"> <field name="code">
result = {"value": self.env.context.get('counter', 990)} result = {"value": self.env.context.get('counter', 990)}
</field> </field>
</record> </record>
<record id="dashboard_widget_text" model="kpi.dashboard.item"> <record id="dashboard_widget_text" model="kpi.dashboard.item">
<field name="name">Dashboard title</field> <field name="name">Dashboard title</field>
<field name="dashboard_id" ref="demo_dashboard"/>
<field name="dashboard_id" ref="demo_dashboard" />
<field name="column">1</field> <field name="column">1</field>
<field name="row">1</field> <field name="row">1</field>
<field name="size_x">4</field> <field name="size_x">4</field>
<field name="color">#707070</field> <field name="color">#707070</field>
<field name="font_color">#000000</field> <field name="font_color">#000000</field>
</record> </record>
<record id="dashboard_widget_number_01" model="kpi.dashboard.item"> <record id="dashboard_widget_number_01" model="kpi.dashboard.item">
<field name="name">Number 01</field> <field name="name">Number 01</field>
<field name="dashboard_id" ref="demo_dashboard"/>
<field name="kpi_id" ref="widget_number_01"/>
<field name="dashboard_id" ref="demo_dashboard" />
<field name="kpi_id" ref="widget_number_01" />
<field name="column">1</field> <field name="column">1</field>
<field name="row">2</field> <field name="row">2</field>
<field name="size_y">4</field> <field name="size_y">4</field>
<field name="color">#47bbb3</field> <field name="color">#47bbb3</field>
<field name="font_color">#ffffff</field> <field name="font_color">#ffffff</field>
</record> </record>
<record id="dashboard_widget_number_02" model="kpi.dashboard.item"> <record id="dashboard_widget_number_02" model="kpi.dashboard.item">
<field name="name">Number 02</field> <field name="name">Number 02</field>
<field name="dashboard_id" ref="demo_dashboard"/>
<field name="kpi_id" ref="widget_number_02"/>
<field name="dashboard_id" ref="demo_dashboard" />
<field name="kpi_id" ref="widget_number_02" />
<field name="column">1</field> <field name="column">1</field>
<field name="row">6</field> <field name="row">6</field>
<field name="size_y">4</field> <field name="size_y">4</field>
<field name="color">#ec663c</field> <field name="color">#ec663c</field>
<field name="font_color">#ffffff</field> <field name="font_color">#ffffff</field>
</record> </record>
<record id="dashboard_widget_meter_01" model="kpi.dashboard.item"> <record id="dashboard_widget_meter_01" model="kpi.dashboard.item">
<field name="name">Meter 01</field> <field name="name">Meter 01</field>
<field name="dashboard_id" ref="demo_dashboard"/>
<field name="kpi_id" ref="widget_meter_01"/>
<field name="dashboard_id" ref="demo_dashboard" />
<field name="kpi_id" ref="widget_meter_01" />
<field name="column">2</field> <field name="column">2</field>
<field name="row">2</field> <field name="row">2</field>
<field name="size_y">4</field> <field name="size_y">4</field>
<field name="color">#9c4274</field> <field name="color">#9c4274</field>
<field name="font_color">#ffffff</field> <field name="font_color">#ffffff</field>
</record> </record>
<record id="dashboard_widget_meter_02" model="kpi.dashboard.item"> <record id="dashboard_widget_meter_02" model="kpi.dashboard.item">
<field name="name">Meter 02</field> <field name="name">Meter 02</field>
<field name="dashboard_id" ref="demo_dashboard"/>
<field name="kpi_id" ref="widget_meter_02"/>
<field name="dashboard_id" ref="demo_dashboard" />
<field name="kpi_id" ref="widget_meter_02" />
<field name="column">2</field> <field name="column">2</field>
<field name="row">6</field> <field name="row">6</field>
<field name="size_y">4</field> <field name="size_y">4</field>
<field name="color">#12b0c5</field> <field name="color">#12b0c5</field>
<field name="font_color">#ffffff</field> <field name="font_color">#ffffff</field>
</record> </record>
<record id="dashboard_widget_add_counter" model="kpi.dashboard.item"> <record id="dashboard_widget_add_counter" model="kpi.dashboard.item">
<field name="name">+1 to Counter</field> <field name="name">+1 to Counter</field>
<field name="dashboard_id" ref="demo_dashboard"/>
<field name="dashboard_id" ref="demo_dashboard" />
<field name="column">3</field> <field name="column">3</field>
<field name="row">10</field> <field name="row">10</field>
<field name="size_y">1</field> <field name="size_y">1</field>
<field name="size_x">2</field> <field name="size_x">2</field>
<field name="color">#B41F1F</field> <field name="color">#B41F1F</field>
<field name="font_color">#EEBF77</field> <field name="font_color">#EEBF77</field>
<field name="modify_context" eval="True"/>
<field name="modify_context_expression">{'counter': (context.counter or 990) + 1}</field>
<field name="modify_color" eval="True"/>
<field name="modify_color_expression">check_if(((context.counter or 990) + 1) % 2, '#ff0000', '#00ff00')</field>
<field name="modify_context" eval="True" />
<field
name="modify_context_expression"
>{'counter': (context.counter or 990) + 1}</field>
<field name="modify_color" eval="True" />
<field
name="modify_color_expression"
>check_if(((context.counter or 990) + 1) % 2, '#ff0000', '#00ff00')</field>
</record> </record>
<record id="dashboard_widget_counter" model="kpi.dashboard.item"> <record id="dashboard_widget_counter" model="kpi.dashboard.item">
<field name="name">Counter</field> <field name="name">Counter</field>
<field name="dashboard_id" ref="demo_dashboard"/>
<field name="kpi_id" ref="widget_counter"/>
<field name="dashboard_id" ref="demo_dashboard" />
<field name="kpi_id" ref="widget_counter" />
<field name="column">3</field> <field name="column">3</field>
<field name="row">11</field> <field name="row">11</field>
<field name="size_y">3</field> <field name="size_y">3</field>
<field name="color">#4B0082</field> <field name="color">#4B0082</field>
<field name="font_color">#ffffff</field> <field name="font_color">#ffffff</field>
</record> </record>
<record id="dashboard_widget_integer" model="kpi.dashboard.item"> <record id="dashboard_widget_integer" model="kpi.dashboard.item">
<field name="name">Integer</field> <field name="name">Integer</field>
<field name="dashboard_id" ref="demo_dashboard"/>
<field name="kpi_id" ref="widget_integer"/>
<field name="dashboard_id" ref="demo_dashboard" />
<field name="kpi_id" ref="widget_integer" />
<field name="column">4</field> <field name="column">4</field>
<field name="row">11</field> <field name="row">11</field>
<field name="size_y">3</field> <field name="size_y">3</field>
<field name="color">#ffffff</field> <field name="color">#ffffff</field>
<field name="font_color">#4B0082</field> <field name="font_color">#4B0082</field>
</record> </record>
<record id="dashboard_widget_graph" model="kpi.dashboard.item"> <record id="dashboard_widget_graph" model="kpi.dashboard.item">
<field name="name">Graph</field> <field name="name">Graph</field>
<field name="dashboard_id" ref="demo_dashboard"/>
<field name="kpi_id" ref="widget_graph"/>
<field name="dashboard_id" ref="demo_dashboard" />
<field name="kpi_id" ref="widget_graph" />
<field name="column">3</field> <field name="column">3</field>
<field name="row">2</field> <field name="row">2</field>
<field name="size_x">2</field> <field name="size_x">2</field>
@ -210,5 +200,4 @@ result = {"value": self.env.context.get('counter', 990)}
<field name="color">#ff9618</field> <field name="color">#ff9618</field>
<field name="font_color">#ffffff</field> <field name="font_color">#ffffff</field>
</record> </record>
</odoo> </odoo>

95
kpi_dashboard/models/kpi_dashboard.py

@ -1,7 +1,7 @@
# Copyright 2020 Creu Blanca # Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
@ -17,8 +17,7 @@ class KpiDashboard(models.Model):
) )
number_of_columns = fields.Integer(default=5, required=True) number_of_columns = fields.Integer(default=5, required=True)
compute_on_fly_refresh = fields.Integer( compute_on_fly_refresh = fields.Integer(
default=0,
help="Seconds to refresh on fly elements"
default=0, help="Seconds to refresh on fly elements"
) )
width = fields.Integer(compute="_compute_width") width = fields.Integer(compute="_compute_width")
margin_y = fields.Integer(default=10, required=True) margin_y = fields.Integer(default=10, required=True)
@ -34,9 +33,7 @@ class KpiDashboard(models.Model):
if "group_ids" in vals: if "group_ids" in vals:
for rec in self: for rec in self:
if rec.menu_id: if rec.menu_id:
rec.menu_id.write(
{"groups_id": [(6, 0, rec.group_ids.ids)]}
)
rec.menu_id.write({"groups_id": [(6, 0, rec.group_ids.ids)]})
return res return res
@api.depends("widget_dimension_x", "margin_x", "number_of_columns") @api.depends("widget_dimension_x", "margin_x", "number_of_columns")
@ -78,7 +75,7 @@ class KpiDashboard(models.Model):
return { return {
"parent_id": menu.id or False, "parent_id": menu.id or False,
"name": self.name, "name": self.name,
"action": "%s,%s" % (action._name, action.id),
"action": "{},{}".format(action._name, action.id),
"groups_id": [(6, 0, self.group_ids.ids)], "groups_id": [(6, 0, self.group_ids.ids)],
} }
@ -106,13 +103,11 @@ class KpiDashboardItem(models.Model):
name = fields.Char(required=True) name = fields.Char(required=True)
kpi_id = fields.Many2one("kpi.kpi") kpi_id = fields.Many2one("kpi.kpi")
dashboard_id = fields.Many2one(
"kpi.dashboard", required=True, ondelete="cascade"
)
dashboard_id = fields.Many2one("kpi.dashboard", required=True, ondelete="cascade")
column = fields.Integer(required=True, default=1) column = fields.Integer(required=True, default=1)
row = fields.Integer(required=True, default=1) row = fields.Integer(required=True, default=1)
end_row = fields.Integer(store=True, compute='_compute_end_row')
end_column = fields.Integer(store=True, compute='_compute_end_column')
end_row = fields.Integer(store=True, compute="_compute_end_row")
end_column = fields.Integer(store=True, compute="_compute_end_column")
size_x = fields.Integer(required=True, default=1) size_x = fields.Integer(required=True, default=1)
size_y = fields.Integer(required=True, default=1) size_y = fields.Integer(required=True, default=1)
color = fields.Char() color = fields.Char()
@ -122,44 +117,43 @@ class KpiDashboardItem(models.Model):
modify_color = fields.Boolean() modify_color = fields.Boolean()
modify_color_expression = fields.Char() modify_color_expression = fields.Char()
@api.depends('row', 'size_y')
@api.depends("row", "size_y")
def _compute_end_row(self): def _compute_end_row(self):
for r in self: for r in self:
r.end_row = r.row + r.size_y - 1 r.end_row = r.row + r.size_y - 1
@api.depends('column', 'size_x')
@api.depends("column", "size_x")
def _compute_end_column(self): def _compute_end_column(self):
for r in self: for r in self:
r.end_column = r.column + r.size_x - 1 r.end_column = r.column + r.size_x - 1
@api.constrains('size_y')
@api.constrains("size_y")
def _check_size_y(self): def _check_size_y(self):
for rec in self: for rec in self:
if rec.size_y > 10: if rec.size_y > 10:
raise ValidationError(_(
'Size Y of the widget cannot be bigger than 10'))
raise ValidationError(
_("Size Y of the widget cannot be bigger than 10")
)
def _check_size_domain(self): def _check_size_domain(self):
return [ return [
('dashboard_id', '=', self.dashboard_id.id),
('id', '!=', self.id),
('row', '<=', self.end_row),
('end_row', '>=', self.row),
('column', '<=', self.end_column),
('end_column', '>=', self.column),
("dashboard_id", "=", self.dashboard_id.id),
("id", "!=", self.id),
("row", "<=", self.end_row),
("end_row", ">=", self.row),
("column", "<=", self.end_column),
("end_column", ">=", self.column),
] ]
@api.constrains('end_row', 'end_column', 'row', 'column')
@api.constrains("end_row", "end_column", "row", "column")
def _check_size(self): def _check_size(self):
for r in self: for r in self:
if self.search(r._check_size_domain(), limit=1): if self.search(r._check_size_domain(), limit=1):
raise ValidationError(_(
'Widgets cannot be crossed by other widgets'
))
raise ValidationError(_("Widgets cannot be crossed by other widgets"))
if r.end_column > r.dashboard_id.number_of_columns: if r.end_column > r.dashboard_id.number_of_columns:
raise ValidationError(_(
'Widget %s is bigger than expected'
) % r.display_name)
raise ValidationError(
_("Widget %s is bigger than expected") % r.display_name
)
@api.onchange("kpi_id") @api.onchange("kpi_id")
def _onchange_kpi(self): def _onchange_kpi(self):
@ -181,9 +175,9 @@ class KpiDashboardItem(models.Model):
"modify_color": self.modify_color, "modify_color": self.modify_color,
} }
if self.modify_context: if self.modify_context:
vals['modify_context_expression'] = self.modify_context_expression
vals["modify_context_expression"] = self.modify_context_expression
if self.modify_color: if self.modify_color:
vals['modify_color_expression'] = self.modify_color_expression
vals["modify_color_expression"] = self.modify_color_expression
if self.kpi_id: if self.kpi_id:
vals.update( vals.update(
{ {
@ -195,15 +189,19 @@ class KpiDashboardItem(models.Model):
} }
) )
if self.kpi_id.compute_on_fly: if self.kpi_id.compute_on_fly:
vals.update({
"value": self.kpi_id._compute_value(),
"value_last_update": fields.Datetime.now(),
})
vals.update(
{
"value": self.kpi_id._compute_value(),
"value_last_update": fields.Datetime.now(),
}
)
else: else:
vals.update({
"value": self.kpi_id.value,
"value_last_update": self.kpi_id.value_last_update,
})
vals.update(
{
"value": self.kpi_id.value,
"value_last_update": self.kpi_id.value_last_update,
}
)
if self.kpi_id.action_ids: if self.kpi_id.action_ids:
vals["actions"] = self.kpi_id.action_ids.read_dashboard() vals["actions"] = self.kpi_id.action_ids.read_dashboard()
else: else:
@ -219,12 +217,13 @@ class KpiDashboardItem(models.Model):
def technical_config(self): def technical_config(self):
self.ensure_one() self.ensure_one()
return { return {
'name': self.display_name,
'res_model': self._name,
'res_id': self.id,
'type': 'ir.actions.act_window',
'view_mode': 'form',
'target': 'new',
'view_id': self.env.ref(
'kpi_dashboard.kpi_dashboard_item_config_form_view').id,
"name": self.display_name,
"res_model": self._name,
"res_id": self.id,
"type": "ir.actions.act_window",
"view_mode": "form",
"target": "new",
"view_id": self.env.ref(
"kpi_dashboard.kpi_dashboard_item_config_form_view"
).id,
} }

148
kpi_dashboard/models/kpi_kpi.py

@ -1,17 +1,20 @@
# Copyright 2020 Creu Blanca # Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
import ast import ast
from odoo.tools.safe_eval import safe_eval
from odoo.addons.base.models.ir_cron import _intervalTypes
from odoo.tools.float_utils import float_compare
import re
import json
import datetime import datetime
import json
import re
from dateutil import relativedelta from dateutil import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.float_utils import float_compare
from odoo.tools.safe_eval import safe_eval
from odoo.addons.base.models.ir_cron import _intervalTypes
class KpiKpi(models.Model): class KpiKpi(models.Model):
_name = "kpi.kpi" _name = "kpi.kpi"
@ -30,8 +33,13 @@ class KpiKpi(models.Model):
args = fields.Char() args = fields.Char()
kwargs = fields.Char() kwargs = fields.Char()
widget = fields.Selection( widget = fields.Selection(
[('integer', 'Integer'), ("number", "Number"), ("meter", "Meter"),
('counter', 'Counter'), ("graph", "Graph")],
[
("integer", "Integer"),
("number", "Number"),
("meter", "Meter"),
("counter", "Counter"),
("graph", "Graph"),
],
required=True, required=True,
default="number", default="number",
) )
@ -40,22 +48,21 @@ class KpiKpi(models.Model):
suffix = fields.Char() suffix = fields.Char()
action_ids = fields.One2many( action_ids = fields.One2many(
"kpi.kpi.action", "kpi.kpi.action",
inverse_name='kpi_id',
help="Actions that can be opened from the KPI"
inverse_name="kpi_id",
help="Actions that can be opened from the KPI",
) )
code = fields.Text("Code") code = fields.Text("Code")
store_history = fields.Boolean() store_history = fields.Boolean()
store_history_interval = fields.Selection( store_history_interval = fields.Selection(
selection=lambda self:
self.env['ir.cron']._fields['interval_type'].selection,
selection=lambda self: self.env["ir.cron"]._fields["interval_type"].selection,
) )
store_history_interval_number = fields.Integer() store_history_interval_number = fields.Integer()
compute_on_fly = fields.Boolean() compute_on_fly = fields.Boolean()
history_ids = fields.One2many("kpi.kpi.history", inverse_name="kpi_id") history_ids = fields.One2many("kpi.kpi.history", inverse_name="kpi_id")
computed_value = fields.Serialized(compute='_compute_computed_value')
computed_date = fields.Datetime(compute='_compute_computed_value')
computed_value = fields.Serialized(compute="_compute_computed_value")
computed_date = fields.Datetime(compute="_compute_computed_value")
@api.depends('value', 'value_last_update', 'compute_on_fly')
@api.depends("value", "value_last_update", "compute_on_fly")
def _compute_computed_value(self): def _compute_computed_value(self):
for record in self: for record in self:
if record.compute_on_fly: if record.compute_on_fly:
@ -95,18 +102,19 @@ class KpiKpi(models.Model):
value = self._compute_value() value = self._compute_value()
self.write({"value": value}) self.write({"value": value})
if self.store_history: if self.store_history:
last = self.env['kpi.kpi.history'].search([
('kpi_id', '=', self.id)
], limit=1)
last = self.env["kpi.kpi.history"].search(
[("kpi_id", "=", self.id)], limit=1
)
if ( if (
not last or
not self.store_history_interval or
last.create_date + _intervalTypes[self.store_history_interval](
self.store_history_interval_number) < fields.Datetime.now()
):
self.env["kpi.kpi.history"].create(
self._generate_history_vals(value)
not last
or not self.store_history_interval
or last.create_date
+ _intervalTypes[self.store_history_interval](
self.store_history_interval_number
) )
< fields.Datetime.now()
):
self.env["kpi.kpi.history"].create(self._generate_history_vals(value))
notifications = [] notifications = []
for dashboard_item in self.dashboard_item_ids: for dashboard_item in self.dashboard_item_ids:
channel = "kpi_dashboard_%s" % dashboard_item.dashboard_id.id channel = "kpi_dashboard_%s" % dashboard_item.dashboard_id.id
@ -152,9 +160,9 @@ class KpiKpi(models.Model):
if len(message) > 0: if len(message) > 0:
message += _(" or ") message += _(" or ")
message += forbidden[-1] message += forbidden[-1]
raise ValidationError(_(
"The code cannot contain the following terms: %s."
) % message)
raise ValidationError(
_("The code cannot contain the following terms: %s.") % message
)
results = self._get_code_input_dict() results = self._get_code_input_dict()
savepoint = "kpi_formula_%s" % self.id savepoint = "kpi_formula_%s" % self.id
self.env.cr.execute("savepoint %s" % savepoint) self.env.cr.execute("savepoint %s" % savepoint)
@ -164,30 +172,34 @@ class KpiKpi(models.Model):
def show_value(self): def show_value(self):
self.ensure_one() self.ensure_one()
action = self.env.ref('kpi_dashboard.kpi_kpi_act_window')
action = self.env.ref("kpi_dashboard.kpi_kpi_act_window")
result = action.read()[0] result = action.read()[0]
result.update({
'res_id': self.id,
'target': 'new',
'view_mode': 'form',
'views': [(self.env.ref(
'kpi_dashboard.kpi_kpi_widget_form_view'
).id, 'form')],
})
result.update(
{
"res_id": self.id,
"target": "new",
"view_mode": "form",
"views": [
(self.env.ref("kpi_dashboard.kpi_kpi_widget_form_view").id, "form")
],
}
)
return result return result
class KpiKpiAction(models.Model): class KpiKpiAction(models.Model):
_name = 'kpi.kpi.action'
_description = 'KPI action'
_name = "kpi.kpi.action"
_description = "KPI action"
kpi_id = fields.Many2one('kpi.kpi', required=True, ondelete='cascade')
kpi_id = fields.Many2one("kpi.kpi", required=True, ondelete="cascade")
action = fields.Reference( action = fields.Reference(
selection=[('ir.actions.report', 'ir.actions.report'),
('ir.actions.act_window', 'ir.actions.act_window'),
('ir.actions.act_url', 'ir.actions.act_url'),
('ir.actions.server', 'ir.actions.server'),
('ir.actions.client', 'ir.actions.client')],
selection=[
("ir.actions.report", "ir.actions.report"),
("ir.actions.act_window", "ir.actions.act_window"),
("ir.actions.act_url", "ir.actions.act_url"),
("ir.actions.server", "ir.actions.server"),
("ir.actions.client", "ir.actions.client"),
],
required=True, required=True,
) )
context = fields.Char() context = fields.Char()
@ -196,43 +208,45 @@ class KpiKpiAction(models.Model):
result = {} result = {}
for r in self: for r in self:
result[r.id] = { result[r.id] = {
'id': r.action.id,
'type': r.action._name,
'name': r.action.name,
'context': safe_eval(r.context or '{}')
"id": r.action.id,
"type": r.action._name,
"name": r.action.name,
"context": safe_eval(r.context or "{}"),
} }
return result return result
class KpiKpiHistory(models.Model): class KpiKpiHistory(models.Model):
_name = 'kpi.kpi.history'
_description = 'KPI history'
_order = 'create_date DESC'
_name = "kpi.kpi.history"
_description = "KPI history"
_order = "create_date DESC"
kpi_id = fields.Many2one( kpi_id = fields.Many2one(
'kpi.kpi', required=True, ondelete='cascade', readonly=True
"kpi.kpi", required=True, ondelete="cascade", readonly=True
) )
value = fields.Serialized(readonly=True) value = fields.Serialized(readonly=True)
raw_value = fields.Char(compute='_compute_raw_value')
name = fields.Char(related='kpi_id.name')
raw_value = fields.Char(compute="_compute_raw_value")
name = fields.Char(related="kpi_id.name")
widget = fields.Selection( widget = fields.Selection(
selection=lambda self:
self.env['kpi.kpi']._fields['widget'].selection,
required=True)
selection=lambda self: self.env["kpi.kpi"]._fields["widget"].selection,
required=True,
)
@api.depends('value')
@api.depends("value")
def _compute_raw_value(self): def _compute_raw_value(self):
for record in self: for record in self:
record.raw_value = json.dumps(record.value) record.raw_value = json.dumps(record.value)
def show_form(self): def show_form(self):
self.ensure_one() self.ensure_one()
action = self.env.ref('kpi_dashboard.kpi_kpi_history_act_window')
action = self.env.ref("kpi_dashboard.kpi_kpi_history_act_window")
result = action.read()[0] result = action.read()[0]
result.update({
'res_id': self.id,
'target': 'new',
'view_mode': 'form',
'views': [(self.env.context.get('form_id'), 'form')],
})
result.update(
{
"res_id": self.id,
"target": "new",
"view_mode": "form",
"views": [(self.env.context.get("form_id"), "form")],
}
)
return result return result

18
kpi_dashboard/security/security.xml

@ -1,22 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<record id="group_kpi_dashboard_manager" model="res.groups"> <record id="group_kpi_dashboard_manager" model="res.groups">
<field name="name">Manage KPI Dashboards</field> <field name="name">Manage KPI Dashboards</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="users" eval="[(4, ref('base.user_admin'))]"/>
<field name="category_id" ref="base.module_category_hidden" />
<field name="users" eval="[(4, ref('base.user_admin'))]" />
</record> </record>
<data noupdate="1"> <data noupdate="1">
<record id="rule_kpi_dashboard" model="ir.rule"> <record id="rule_kpi_dashboard" model="ir.rule">
<field name="name">KPI Dashboard: User</field> <field name="name">KPI Dashboard: User</field>
<field name="model_id" ref="model_kpi_dashboard"/>
<field name="domain_force">['|', ('group_ids', '=', False), ('group_ids', 'in', user.groups_id.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="model_id" ref="model_kpi_dashboard" />
<field
name="domain_force"
>['|', ('group_ids', '=', False), ('group_ids', 'in', user.groups_id.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]" />
</record> </record>
<record id="rule_kpi_dashboard_all" model="ir.rule"> <record id="rule_kpi_dashboard_all" model="ir.rule">
<field name="name">KPI Dashboard: All</field> <field name="name">KPI Dashboard: All</field>
<field name="model_id" ref="model_kpi_dashboard"/>
<field name="model_id" ref="model_kpi_dashboard" />
<field name="domain_force">[(1, '=', 1)]</field> <field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_kpi_dashboard_manager'))]"/>
<field name="groups" eval="[(4, ref('group_kpi_dashboard_manager'))]" />
</record> </record>
</data> </data>
</odoo> </odoo>

147
kpi_dashboard/static/src/js/dashboard_controller.js

@ -1,150 +1,159 @@
odoo.define('kpi_dashboard.DashboardController', function (require) {
odoo.define("kpi_dashboard.DashboardController", function(require) {
"use strict"; "use strict";
var BasicController = require('web.BasicController');
var core = require('web.core');
var BasicController = require("web.BasicController");
var core = require("web.core");
var qweb = core.qweb; var qweb = core.qweb;
var _t = core._t; var _t = core._t;
var DashboardController = BasicController.extend({ var DashboardController = BasicController.extend({
init: function () {
init: function() {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.dashboard_context = {}; this.dashboard_context = {};
this.dashboard_color_data = []
this.dashboard_color_data = [];
}, },
custom_events: _.extend({}, BasicController.prototype.custom_events, { custom_events: _.extend({}, BasicController.prototype.custom_events, {
addDashboard: '_addDashboard',
refresh_on_fly: '_refreshOnFly',
modify_context: '_modifyContext',
add_modify_color: '_addModifyColor',
refresh_colors: '_refreshColors',
addDashboard: "_addDashboard",
refresh_on_fly: "_refreshOnFly",
modify_context: "_modifyContext",
add_modify_color: "_addModifyColor",
refresh_colors: "_refreshColors",
}), }),
_refreshOnFly: function (event) {
_refreshOnFly: function(event) {
var self = this; var self = this;
this._rpc({ this._rpc({
model: this.modelName, model: this.modelName,
method: 'read_dashboard_on_fly',
method: "read_dashboard_on_fly",
args: [[this.renderer.state.res_id]], args: [[this.renderer.state.res_id]],
context: this._getContext(), context: this._getContext(),
}).then(function (data) {
_.each(data, function (item) {
}).then(function(data) {
_.each(data, function(item) {
// We will follow the same logic used on Bus Notifications // We will follow the same logic used on Bus Notifications
self.renderer._onNotification([[
"kpi_dashboard_" + self.renderer.state.res_id,
item
]])
self.renderer._onNotification([
["kpi_dashboard_" + self.renderer.state.res_id, item],
]);
}); });
}); });
}, },
renderPager: function ($node, options) {
renderPager: function($node, options) {
options = _.extend({}, options, { options = _.extend({}, options, {
validate: this.canBeDiscarded.bind(this), validate: this.canBeDiscarded.bind(this),
}); });
this._super($node, options); this._super($node, options);
}, },
_pushState: function (state) {
_pushState: function(state) {
state = state || {}; state = state || {};
var env = this.model.get(this.handle, {env: true}); var env = this.model.get(this.handle, {env: true});
state.id = env.currentId; state.id = env.currentId;
this._super(state); this._super(state);
}, },
_addDashboard: function () {
_addDashboard: function() {
var self = this; var self = this;
var action = self.initialState.specialData.action_id; var action = self.initialState.specialData.action_id;
var name = self.initialState.specialData.name; var name = self.initialState.specialData.name;
if (! action) {
if (!action) {
self.do_warn(_t("First you must create the Menu")); self.do_warn(_t("First you must create the Menu"));
} }
return self._rpc({
route: '/board/add_to_dashboard',
params: {
action_id: action,
context_to_save: {'res_id': self.initialState.res_id},
domain: [('id', '=', self.initialState.res_id)],
view_mode: 'dashboard',
name: name,
},
})
.then(function (r) {
if (r) {
self.do_notify(
_.str.sprintf(_t("'%s' added to dashboard"), name),
_t('Please refresh your browser for the changes to take effect.')
);
} else {
self.do_warn(_t("Could not add KPI dashboard to dashboard"));
}
});
return self
._rpc({
route: "/board/add_to_dashboard",
params: {
action_id: action,
context_to_save: {res_id: self.initialState.res_id},
domain: [("id", "=", self.initialState.res_id)],
view_mode: "dashboard",
name: name,
},
})
.then(function(r) {
if (r) {
self.do_notify(
_.str.sprintf(_t("'%s' added to dashboard"), name),
_t(
"Please refresh your browser for the changes to take effect."
)
);
} else {
self.do_warn(_t("Could not add KPI dashboard to dashboard"));
}
});
}, },
_updateButtons: function () {
_updateButtons: function() {
// HOOK Function // HOOK Function
this.$buttons.on( this.$buttons.on(
'click', '.o_dashboard_button_add',
this._addDashboard.bind(this));
"click",
".o_dashboard_button_add",
this._addDashboard.bind(this)
);
}, },
renderButtons: function ($node) {
if (! $node) {
renderButtons: function($node) {
if (!$node) {
return; return;
} }
this.$buttons = $('<div/>');
this.$buttons.append(qweb.render(
"kpi_dashboard.buttons", {widget: this}));
this.$buttons = $("<div/>");
this.$buttons.append(qweb.render("kpi_dashboard.buttons", {widget: this}));
this._updateButtons(); this._updateButtons();
this.$buttons.appendTo($node); this.$buttons.appendTo($node);
}, },
_getContext: function () {
_getContext: function() {
return _.extend( return _.extend(
{}, {},
this.model.get(this.handle, {raw: true}).getContext(), this.model.get(this.handle, {raw: true}).getContext(),
{bin_size: true}, {bin_size: true},
this.dashboard_context,
)
this.dashboard_context
);
}, },
_modifyContext: function (event) {
_modifyContext: function(event) {
var ctx = this._getContext(); var ctx = this._getContext();
this.dashboard_context = _.extend( this.dashboard_context = _.extend(
this.dashboard_context, this.dashboard_context,
py.eval(event.data.context, {context: _.extend(
ctx,
{__getattr__: function() {return false}}
// We need to add this in order to allow to use undefined
// context items
)}),
py.eval(event.data.context, {
context: _.extend(
ctx,
{
__getattr__: function() {
return false;
},
}
// We need to add this in order to allow to use undefined
// context items
),
})
); );
this._refreshOnFly(event); this._refreshOnFly(event);
this._refreshColors(); this._refreshColors();
}, },
_addModifyColor: function (event) {
_addModifyColor: function(event) {
this.dashboard_color_data.push([ this.dashboard_color_data.push([
event.data.element_id, event.data.element_id,
event.data.expression, event.data.expression,
]); ]);
}, },
_refreshColors: function () {
_refreshColors: function() {
var self = this; var self = this;
var ctx = this._getContext(); var ctx = this._getContext();
_.each(this.dashboard_color_data, function (data) {
_.each(this.dashboard_color_data, function(data) {
var color = py.eval(data[1], { var color = py.eval(data[1], {
context: _.extend(ctx, { context: _.extend(ctx, {
__getattr__: function() {return false},
__getattr__: function() {
return false;
},
}), }),
check_if: function(args) { check_if: function(args) {
if (args[0].toJSON()) { if (args[0].toJSON()) {
return args[1]; return args[1];
} }
return args[2]; return args[2];
}
},
}); });
var $element = self.renderer.$el.find('#' + data[0]);
$element.css('background-color', color);
var $element = self.renderer.$el.find("#" + data[0]);
$element.css("background-color", color);
}); });
}, },
}); });
return DashboardController; return DashboardController;
}); });

18
kpi_dashboard/static/src/js/dashboard_model.js

@ -1,23 +1,21 @@
odoo.define('kpi_dashboard.DashboardModel', function (require) {
odoo.define("kpi_dashboard.DashboardModel", function(require) {
"use strict"; "use strict";
var BasicModel = require('web.BasicModel');
var BasicModel = require("web.BasicModel");
var DashboardModel = BasicModel.extend({ var DashboardModel = BasicModel.extend({
_fetchRecord: function (record, options) {
_fetchRecord: function(record, options) {
return this._rpc({ return this._rpc({
model: record.model, model: record.model,
method: 'read_dashboard',
method: "read_dashboard",
args: [[record.res_id]], args: [[record.res_id]],
context: _.extend({}, record.getContext(), {bin_size: true}), context: _.extend({}, record.getContext(), {bin_size: true}),
})
.then(function (result) {
}).then(function(result) {
record.specialData = result; record.specialData = result;
return result
})
}
return result;
});
},
}); });
return DashboardModel; return DashboardModel;
}); });

117
kpi_dashboard/static/src/js/dashboard_renderer.js

@ -1,99 +1,98 @@
odoo.define('kpi_dashboard.DashboardRenderer', function (require) {
odoo.define("kpi_dashboard.DashboardRenderer", function(require) {
"use strict"; "use strict";
var BasicRenderer = require('web.BasicRenderer');
var core = require('web.core');
var registry = require('kpi_dashboard.widget_registry');
var BusService = require('bus.BusService');
var BasicRenderer = require("web.BasicRenderer");
var core = require("web.core");
var registry = require("kpi_dashboard.widget_registry");
var BusService = require("bus.BusService");
var qweb = core.qweb; var qweb = core.qweb;
var DashboardRenderer= BasicRenderer.extend({
var DashboardRenderer = BasicRenderer.extend({
className: "o_dashboard_view", className: "o_dashboard_view",
_getDashboardWidget: function (kpi) {
var Widget = registry.getAny([
kpi.widget, 'abstract',
]);
_getDashboardWidget: function(kpi) {
var Widget = registry.getAny([kpi.widget, "abstract"]);
var widget = new Widget(this, kpi); var widget = new Widget(this, kpi);
return widget; return widget;
}, },
_onClickModifyContext: function (modify_context_expression, event) {
this.trigger_up('modify_context', {
_onClickModifyContext: function(modify_context_expression, event) {
this.trigger_up("modify_context", {
context: modify_context_expression, context: modify_context_expression,
event: event, event: event,
})
});
}, },
_renderView: function () {
this.$el.html($(qweb.render('dashboard_kpi.dashboard')));
this.$el.css(
'background-color', this.state.specialData.background_color);
this.$el.find('.gridster')
.css('width', this.state.specialData.width);
this.$grid = this.$el.find('.gridster ul');
_renderView: function() {
this.$el.html($(qweb.render("dashboard_kpi.dashboard")));
this.$el.css("background-color", this.state.specialData.background_color);
this.$el.find(".gridster").css("width", this.state.specialData.width);
this.$grid = this.$el.find(".gridster ul");
var self = this; var self = this;
this.kpi_widget = {}; this.kpi_widget = {};
_.each(this.state.specialData.item_ids, function (kpi) {
var element = $(qweb.render(
'kpi_dashboard.kpi', {widget: kpi}));
element.css('background-color', kpi.color);
element.css('color', kpi.font_color);
element.attr('id', _.uniqueId('kpi_'));
_.each(this.state.specialData.item_ids, function(kpi) {
var element = $(qweb.render("kpi_dashboard.kpi", {widget: kpi}));
element.css("background-color", kpi.color);
element.css("color", kpi.font_color);
element.attr("id", _.uniqueId("kpi_"));
self.$grid.append(element); self.$grid.append(element);
if (kpi.modify_color) { if (kpi.modify_color) {
self.trigger_up("add_modify_color", { self.trigger_up("add_modify_color", {
element_id: element.attr("id"), element_id: element.attr("id"),
expression: kpi.modify_color_expression, expression: kpi.modify_color_expression,
})
});
} }
if (kpi.modify_context) { if (kpi.modify_context) {
element.on("click", self._onClickModifyContext.bind(
self, kpi.modify_context_expression));
element.css('cursor', 'pointer');
element.on(
"click",
self._onClickModifyContext.bind(
self,
kpi.modify_context_expression
)
);
element.css("cursor", "pointer");
// We want to set it show as clickable // We want to set it show as clickable
} }
self.kpi_widget[kpi.id] = self._getDashboardWidget(kpi); self.kpi_widget[kpi.id] = self._getDashboardWidget(kpi);
self.kpi_widget[kpi.id].appendTo(element); self.kpi_widget[kpi.id].appendTo(element);
}); });
this.$grid.gridster({
widget_margins: [
this.state.specialData.margin_x,
this.state.specialData.margin_y,
],
widget_base_dimensions: [
this.state.specialData.widget_dimension_x,
this.state.specialData.widget_dimension_y,
],
cols: this.state.specialData.max_cols,
}).data('gridster').disable();
this.channel = 'kpi_dashboard_' + this.state.res_id;
this.call(
'bus_service', 'addChannel', this.channel);
this.call('bus_service', 'startPolling');
this.call(
'bus_service', 'onNotification',
this, this._onNotification
);
this.$grid
.gridster({
widget_margins: [
this.state.specialData.margin_x,
this.state.specialData.margin_y,
],
widget_base_dimensions: [
this.state.specialData.widget_dimension_x,
this.state.specialData.widget_dimension_y,
],
cols: this.state.specialData.max_cols,
})
.data("gridster")
.disable();
this.channel = "kpi_dashboard_" + this.state.res_id;
this.call("bus_service", "addChannel", this.channel);
this.call("bus_service", "startPolling");
this.call("bus_service", "onNotification", this, this._onNotification);
if (this.state.specialData.compute_on_fly_refresh > 0) { if (this.state.specialData.compute_on_fly_refresh > 0) {
// Setting the refresh interval // Setting the refresh interval
this.on_fly_interval = setInterval(function () {
self.trigger_up('refresh_on_fly');
}, this.state.specialData.compute_on_fly_refresh *1000);
};
this.trigger_up('refresh_colors');
this.trigger_up('refresh_on_fly');
this.on_fly_interval = setInterval(function() {
self.trigger_up("refresh_on_fly");
}, this.state.specialData.compute_on_fly_refresh * 1000);
}
this.trigger_up("refresh_colors");
this.trigger_up("refresh_on_fly");
// We need to refreshs data in order compute with the current // We need to refreshs data in order compute with the current
// context // context
return $.when(); return $.when();
}, },
on_detach_callback: function () {
on_detach_callback: function() {
// We want to clear the refresh interval once we exit the view // We want to clear the refresh interval once we exit the view
if (this.on_fly_interval) { if (this.on_fly_interval) {
clearInterval(this.on_fly_interval)
clearInterval(this.on_fly_interval);
} }
this._super.apply(this, arguments); this._super.apply(this, arguments);
}, },
_onNotification: function (notifications) {
_onNotification: function(notifications) {
var self = this; var self = this;
_.each(notifications, function (notification) {
_.each(notifications, function(notification) {
var channel = notification[0]; var channel = notification[0];
var message = notification[1]; var message = notification[1];
if (channel === self.channel && message) { if (channel === self.channel && message) {

36
kpi_dashboard/static/src/js/dashboard_view.js

@ -1,26 +1,22 @@
odoo.define('kpi_dashboard.DashboardView', function (require) {
odoo.define("kpi_dashboard.DashboardView", function(require) {
"use strict"; "use strict";
var BasicView = require('web.BasicView');
var DashboardController = require('kpi_dashboard.DashboardController');
var DashboardModel = require('kpi_dashboard.DashboardModel');
var DashboardRenderer = require('kpi_dashboard.DashboardRenderer');
var view_registry = require('web.view_registry');
var core = require('web.core');
var BasicView = require("web.BasicView");
var DashboardController = require("kpi_dashboard.DashboardController");
var DashboardModel = require("kpi_dashboard.DashboardModel");
var DashboardRenderer = require("kpi_dashboard.DashboardRenderer");
var view_registry = require("web.view_registry");
var core = require("web.core");
var _lt = core._lt; var _lt = core._lt;
var DashboardView = BasicView.extend({ var DashboardView = BasicView.extend({
jsLibs: [
'/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js',
],
cssLibs: [
'/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css',
],
jsLibs: ["/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js"],
cssLibs: ["/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css"],
accesskey: "d", accesskey: "d",
display_name: _lt("Dashboard"), display_name: _lt("Dashboard"),
icon: 'fa-tachometer',
viewType: 'dashboard',
icon: "fa-tachometer",
viewType: "dashboard",
config: _.extend({}, BasicView.prototype.config, { config: _.extend({}, BasicView.prototype.config, {
Controller: DashboardController, Controller: DashboardController,
Renderer: DashboardRenderer, Renderer: DashboardRenderer,
@ -28,17 +24,17 @@ odoo.define('kpi_dashboard.DashboardView', function (require) {
}), }),
multi_record: false, multi_record: false,
searchable: false, searchable: false,
init: function () {
init: function() {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.controllerParams.mode = 'readonly';
this.loadParams.type = 'record';
if (! this.loadParams.res_id && this.loadParams.context.res_id) {
this.controllerParams.mode = "readonly";
this.loadParams.type = "record";
if (!this.loadParams.res_id && this.loadParams.context.res_id) {
this.loadParams.res_id = this.loadParams.context.res_id; this.loadParams.res_id = this.loadParams.context.res_id;
} }
}, },
}); });
view_registry.add('dashboard', DashboardView);
view_registry.add("dashboard", DashboardView);
return DashboardView; return DashboardView;
}); });

69
kpi_dashboard/static/src/js/field_widget.js

@ -1,28 +1,24 @@
odoo.define('kpi_dashboard.KpiFieldWidget', function(require) {
odoo.define("kpi_dashboard.KpiFieldWidget", function(require) {
"use strict"; "use strict";
var basic_fields = require('web.basic_fields');
var field_registry = require('web.field_registry');
var core = require('web.core');
var basic_fields = require("web.basic_fields");
var field_registry = require("web.field_registry");
var core = require("web.core");
var qweb = core.qweb; var qweb = core.qweb;
var registry = require('kpi_dashboard.widget_registry');
var registry = require("kpi_dashboard.widget_registry");
var KpiFieldWidget = basic_fields.FieldChar.extend({ var KpiFieldWidget = basic_fields.FieldChar.extend({
jsLibs: [
'/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js',
],
cssLibs: [
'/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css',
],
className: 'o_dashboard_view',
_renderReadonly: function () {
this.$el.html($(qweb.render('dashboard_kpi.dashboard')));
jsLibs: ["/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js"],
cssLibs: ["/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css"],
className: "o_dashboard_view",
_renderReadonly: function() {
this.$el.html($(qweb.render("dashboard_kpi.dashboard")));
var marginx = 0; var marginx = 0;
var marginy = 0; var marginy = 0;
var widgetx = 400; var widgetx = 400;
var widgety = 400; var widgety = 400;
this.$el.find('.gridster').css('width', widgety);
this.$grid = this.$el.find('.gridster ul');
this.$el.find(".gridster").css("width", widgety);
this.$grid = this.$el.find(".gridster ul");
var widgetVals = { var widgetVals = {
value: this.value, value: this.value,
col: 1, col: 1,
@ -30,10 +26,11 @@ odoo.define('kpi_dashboard.KpiFieldWidget', function(require) {
sizex: 1, sizex: 1,
sizey: 1, sizey: 1,
name: this.recordData[this.nodeOptions.name], name: this.recordData[this.nodeOptions.name],
value_last_update: this.recordData[this.nodeOptions.date]
}
value_last_update: this.recordData[this.nodeOptions.date],
};
var Widget = registry.getAny([ var Widget = registry.getAny([
this.recordData[this.nodeOptions.widget], 'abstract',
this.recordData[this.nodeOptions.widget],
"abstract",
]); ]);
this.state = { this.state = {
specialData: { specialData: {
@ -41,28 +38,24 @@ odoo.define('kpi_dashboard.KpiFieldWidget', function(require) {
margin_y: marginy, margin_y: marginy,
widget_dimension_x: widgetx, widget_dimension_x: widgetx,
widget_dimension_y: widgety, widget_dimension_y: widgety,
}
}
},
};
var widget = new Widget(this, widgetVals); var widget = new Widget(this, widgetVals);
var element = $(qweb.render(
'kpi_dashboard.kpi', {widget: widgetVals}));
element.css('background-color', 'white');
element.css('color', 'black');
var element = $(qweb.render("kpi_dashboard.kpi", {widget: widgetVals}));
element.css("background-color", "white");
element.css("color", "black");
this.$grid.append(element); this.$grid.append(element);
widget.appendTo(element)
this.$grid.gridster({
widget_margins: [
marginx,
marginy,
],
widget_base_dimensions: [
widgetx,
widgety,
],
cols: 1,
}).data('gridster').disable();
widget.appendTo(element);
this.$grid
.gridster({
widget_margins: [marginx, marginy],
widget_base_dimensions: [widgetx, widgety],
cols: 1,
})
.data("gridster")
.disable();
}, },
}); });
field_registry.add('kpi', KpiFieldWidget);
field_registry.add("kpi", KpiFieldWidget);
return KpiFieldWidget; return KpiFieldWidget;
}); });

88
kpi_dashboard/static/src/js/widget/abstract_widget.js

@ -1,20 +1,20 @@
odoo.define('kpi_dashboard.AbstractWidget', function (require) {
odoo.define("kpi_dashboard.AbstractWidget", function(require) {
"use strict"; "use strict";
var Widget = require('web.Widget');
var field_utils = require('web.field_utils');
var time = require('web.time');
var ajax = require('web.ajax');
var registry = require('kpi_dashboard.widget_registry');
var Widget = require("web.Widget");
var field_utils = require("web.field_utils");
var time = require("web.time");
var ajax = require("web.ajax");
var registry = require("kpi_dashboard.widget_registry");
var AbstractWidget = Widget.extend({ var AbstractWidget = Widget.extend({
template: 'kpi_dashboard.base_widget', // Template used by the widget
template: "kpi_dashboard.base_widget", // Template used by the widget
cssLibs: [], // Specific css of the widget cssLibs: [], // Specific css of the widget
jsLibs: [], // Specific Javascript libraries of the widget jsLibs: [], // Specific Javascript libraries of the widget
events: { events: {
'click .o_kpi_dashboard_toggle_button': '_onClickToggleButton',
'click .direct_action': '_onClickDirectAction',
"click .o_kpi_dashboard_toggle_button": "_onClickToggleButton",
"click .direct_action": "_onClickDirectAction",
}, },
init: function (parent, kpi_values) {
init: function(parent, kpi_values) {
this._super(parent); this._super(parent);
this.col = kpi_values.col; this.col = kpi_values.col;
this.row = kpi_values.row; this.row = kpi_values.row;
@ -29,66 +29,70 @@ odoo.define('kpi_dashboard.AbstractWidget', function (require) {
this.prefix = kpi_values.prefix; this.prefix = kpi_values.prefix;
this.suffix = kpi_values.suffix; this.suffix = kpi_values.suffix;
this.actions = kpi_values.actions; this.actions = kpi_values.actions;
this.widget_size_x = this.widget_dimension_x * this.sizex +
(this.sizex - 1) * this.margin_x;
this.widget_size_y = this.widget_dimension_y * this.sizey +
(this.sizey - 1) * this.margin_y;
this.widget_size_x =
this.widget_dimension_x * this.sizex + (this.sizex - 1) * this.margin_x;
this.widget_size_y =
this.widget_dimension_y * this.sizey + (this.sizey - 1) * this.margin_y;
}, },
willStart: function () {
willStart: function() {
// We need to load the libraries before the start // We need to load the libraries before the start
return $.when(ajax.loadLibs(this), this._super.apply(this, arguments)); return $.when(ajax.loadLibs(this), this._super.apply(this, arguments));
}, },
start: function () {
start: function() {
var self = this; var self = this;
return this._super.apply(this, arguments).then(function () {
return this._super.apply(this, arguments).then(function() {
self._fillWidget(self.values); self._fillWidget(self.values);
}); });
}, },
_onClickToggleButton: function (event) {
_onClickToggleButton: function(event) {
event.preventDefault(); event.preventDefault();
this.$el.toggleClass('o_dropdown_open');
this.$el.toggleClass("o_dropdown_open");
}, },
_fillWidget: function (values) {
_fillWidget: function(values) {
// This function fills the widget values // This function fills the widget values
if (this.$el === undefined)
return;
if (this.$el === undefined) return;
this.fillWidget(values); this.fillWidget(values);
var item = this.$el.find('[data-bind="value_last_update_display"]'); var item = this.$el.find('[data-bind="value_last_update_display"]');
if (item && ! values.compute_on_fly && values.value_last_update !== undefined) {
if (
item &&
!values.compute_on_fly &&
values.value_last_update !== undefined
) {
var value = field_utils.parse.datetime(values.value_last_update); var value = field_utils.parse.datetime(values.value_last_update);
item.text(value.clone().add(
this.getSession().getTZOffset(value), 'minutes').format(
time.getLangDatetimeFormat()
));
item.text(
value
.clone()
.add(this.getSession().getTZOffset(value), "minutes")
.format(time.getLangDatetimeFormat())
);
} }
var $manage = this.$el.find('.o_kpi_dashboard_manage');
var $manage = this.$el.find(".o_kpi_dashboard_manage");
if ($manage && this.showManagePanel(values)) if ($manage && this.showManagePanel(values))
$manage.toggleClass('hidden', false);
$manage.toggleClass("hidden", false);
}, },
showManagePanel: function (values) {
showManagePanel: function(values) {
// Hook for extensions // Hook for extensions
return (values.actions !== undefined);
return values.actions !== undefined;
}, },
fillWidget: function (values) {
fillWidget: function(values) {
// Specific function that will be changed by specific widget // Specific function that will be changed by specific widget
var value = values.value; var value = values.value;
var self = this; var self = this;
_.each(value, function (val, key) {
var item = self.$el.find('[data-bind=' + key + ']')
if (item)
item.text(val);
})
_.each(value, function(val, key) {
var item = self.$el.find("[data-bind=" + key + "]");
if (item) item.text(val);
});
}, },
_onClickDirectAction: function(event) { _onClickDirectAction: function(event) {
event.preventDefault(); event.preventDefault();
var $data = $(event.currentTarget).closest('a');
var action = this.actions[$($data).data('id')];
var $data = $(event.currentTarget).closest("a");
var action = this.actions[$($data).data("id")];
return this.do_action(action.id, { return this.do_action(action.id, {
additional_context: action.context
additional_context: action.context,
}); });
}
},
}); });
registry.add('abstract', AbstractWidget);
registry.add("abstract", AbstractWidget);
return AbstractWidget; return AbstractWidget;
}); });

10
kpi_dashboard/static/src/js/widget/counter_widget.js

@ -1,14 +1,14 @@
odoo.define('kpi_dashboard.CounterWidget', function (require) {
odoo.define("kpi_dashboard.CounterWidget", function(require) {
"use strict"; "use strict";
var IntegerWidget = require('kpi_dashboard.IntegerWidget');
var registry = require('kpi_dashboard.widget_registry');
var field_utils = require('web.field_utils');
var IntegerWidget = require("kpi_dashboard.IntegerWidget");
var registry = require("kpi_dashboard.widget_registry");
var field_utils = require("web.field_utils");
var CounterWidget = IntegerWidget.extend({ var CounterWidget = IntegerWidget.extend({
shortList: [], shortList: [],
}); });
registry.add('counter', CounterWidget);
registry.add("counter", CounterWidget);
return CounterWidget; return CounterWidget;
}); });

91
kpi_dashboard/static/src/js/widget/graph_widget.js

@ -1,40 +1,39 @@
odoo.define('kpi_dashboard.GraphWidget', function (require) {
odoo.define("kpi_dashboard.GraphWidget", function(require) {
"use strict"; "use strict";
var AbstractWidget = require('kpi_dashboard.AbstractWidget');
var registry = require('kpi_dashboard.widget_registry');
var core = require('web.core');
var AbstractWidget = require("kpi_dashboard.AbstractWidget");
var registry = require("kpi_dashboard.widget_registry");
var core = require("web.core");
var qweb = core.qweb; var qweb = core.qweb;
var GraphWidget = AbstractWidget.extend({ var GraphWidget = AbstractWidget.extend({
template: 'kpi_dashboard.graph',
template: "kpi_dashboard.graph",
jsLibs: [ jsLibs: [
'/web/static/lib/nvd3/d3.v3.js',
'/web/static/lib/nvd3/nv.d3.js',
'/web/static/src/js/libs/nvd3.js',
],
cssLibs: [
'/web/static/lib/nvd3/nv.d3.css',
"/web/static/lib/nvd3/d3.v3.js",
"/web/static/lib/nvd3/nv.d3.js",
"/web/static/src/js/libs/nvd3.js",
], ],
start: function () {
cssLibs: ["/web/static/lib/nvd3/nv.d3.css"],
start: function() {
this._onResize = this._onResize.bind(this); this._onResize = this._onResize.bind(this);
nv.utils.windowResize(this._onResize); nv.utils.windowResize(this._onResize);
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
destroy: function () {
if ('nv' in window && nv.utils && nv.utils.offWindowResize) {
// if the widget is destroyed before the lazy loaded libs (nv) are
destroy: function() {
if ("nv" in window && nv.utils && nv.utils.offWindowResize) {
// If the widget is destroyed before the lazy loaded libs (nv) are
// actually loaded (i.e. after the widget has actually started), // actually loaded (i.e. after the widget has actually started),
// nv is undefined, but the handler isn't bound yet anyway // nv is undefined, but the handler isn't bound yet anyway
nv.utils.offWindowResize(this._onResize); nv.utils.offWindowResize(this._onResize);
} }
this._super.apply(this, arguments); this._super.apply(this, arguments);
}, },
_getChartOptions: function (values) {
_getChartOptions: function(values) {
return { return {
x: function (d, u) { return u; },
margin: {'left': 0, 'right': 0, 'top': 5, 'bottom': 0},
x: function(d, u) {
return u;
},
margin: {left: 0, right: 0, top: 5, bottom: 0},
showYAxis: false, showYAxis: false,
showXAxis: false, showXAxis: false,
showLegend: false, showLegend: false,
@ -42,60 +41,58 @@ odoo.define('kpi_dashboard.GraphWidget', function (require) {
width: this.widget_size_x - 20, width: this.widget_size_x - 20,
}; };
}, },
_chartConfiguration: function (values) {
_chartConfiguration: function(values) {
this.chart.forceY([0]); this.chart.forceY([0]);
this.chart.xAxis.tickFormat(function (d) {
var label = '';
_.each(values.value.graphs, function (v) {
this.chart.xAxis.tickFormat(function(d) {
var label = "";
_.each(values.value.graphs, function(v) {
if (v.values[d] && v.values[d].x) { if (v.values[d] && v.values[d].x) {
label = v.values[d].x; label = v.values[d].x;
} }
}); });
return label; return label;
}); });
this.chart.yAxis.tickFormat(d3.format(',.2f'));
this.chart.yAxis.tickFormat(d3.format(",.2f"));
this.chart.tooltip.contentGenerator(function (key) {
return qweb.render('GraphCustomTooltip', {
'color': key.point.color,
'key': key.series[0].title,
'value': d3.format(',.2f')(key.point.y)
this.chart.tooltip.contentGenerator(function(key) {
return qweb.render("GraphCustomTooltip", {
color: key.point.color,
key: key.series[0].title,
value: d3.format(",.2f")(key.point.y),
}); });
}); });
}, },
_addGraph: function (values) {
_addGraph: function(values) {
var data = values.value.graphs; var data = values.value.graphs;
this.$svg.addClass('o_graph_linechart');
this.$svg.addClass("o_graph_linechart");
this.chart = nv.models.lineChart(); this.chart = nv.models.lineChart();
this.chart.options(
this._getChartOptions(values)
);
this.chart.options(this._getChartOptions(values));
this._chartConfiguration(values); this._chartConfiguration(values);
d3.select(this.$('svg')[0])
d3.select(this.$("svg")[0])
.datum(data) .datum(data)
.transition().duration(600)
.transition()
.duration(600)
.call(this.chart); .call(this.chart);
this.$('svg').css('height', this.widget_size_y - 90);
this.$("svg").css("height", this.widget_size_y - 90);
this._customizeChart(); this._customizeChart();
}, },
fillWidget: function (values) {
fillWidget: function(values) {
var self = this; var self = this;
var element = this.$el.find('[data-bind="value"]'); var element = this.$el.find('[data-bind="value"]');
element.empty(); element.empty();
element.css('padding-left', 10).css('padding-right', 10);
element.css("padding-left", 10).css("padding-right", 10);
this.chart = null; this.chart = null;
nv.addGraph(function () {
self.$svg = self.$el.find(
'[data-bind="value"]'
).append('<svg width=' + (self.widget_size_x - 20) + '>');
nv.addGraph(function() {
self.$svg = self.$el
.find('[data-bind="value"]')
.append("<svg width=" + (self.widget_size_x - 20) + ">");
self._addGraph(values); self._addGraph(values);
}); });
}, },
_customizeChart: function () {
_customizeChart: function() {
// Hook function // Hook function
}, },
_onResize: function () {
_onResize: function() {
if (this.chart) { if (this.chart) {
this.chart.update(); this.chart.update();
this._customizeChart(); this._customizeChart();
@ -103,6 +100,6 @@ odoo.define('kpi_dashboard.GraphWidget', function (require) {
}, },
}); });
registry.add('graph', GraphWidget);
registry.add("graph", GraphWidget);
return GraphWidget; return GraphWidget;
}); });

57
kpi_dashboard/static/src/js/widget/integer_widget.js

@ -1,36 +1,38 @@
odoo.define('kpi_dashboard.IntegerWidget', function (require) {
odoo.define("kpi_dashboard.IntegerWidget", function(require) {
"use strict"; "use strict";
var AbstractWidget = require('kpi_dashboard.AbstractWidget');
var registry = require('kpi_dashboard.widget_registry');
var field_utils = require('web.field_utils');
var AbstractWidget = require("kpi_dashboard.AbstractWidget");
var registry = require("kpi_dashboard.widget_registry");
var field_utils = require("web.field_utils");
var IntegerWidget = AbstractWidget.extend({ var IntegerWidget = AbstractWidget.extend({
template: 'kpi_dashboard.number',
template: "kpi_dashboard.number",
digits: [3, 0], digits: [3, 0],
shortList: [ shortList: [
[1000000000000, 'T', [3, 1]],
[1000000000, 'G', [3, 1]],
[1000000, 'M', [3, 1]],
[1000, 'K', [3, 1]]
[1000000000000, "T", [3, 1]],
[1000000000, "G", [3, 1]],
[1000000, "M", [3, 1]],
[1000, "K", [3, 1]],
], ],
shortNumber: function (num) {
var suffix = '';
shortNumber: function(num) {
var suffix = "";
var shortened = false; var shortened = false;
var digits = this.digits; var digits = this.digits;
_.each(this.shortList, function (shortItem) {
_.each(this.shortList, function(shortItem) {
if (!shortened && Math.abs(num) >= shortItem[0]) { if (!shortened && Math.abs(num) >= shortItem[0]) {
shortened = true; shortened = true;
suffix = shortItem[1]; suffix = shortItem[1];
num = num / shortItem[0];
num /= shortItem[0];
digits = shortItem[2]; digits = shortItem[2];
} }
}); });
return field_utils.format.float(num, false, {
digits: digits}) + suffix;
return (
field_utils.format.float(num, false, {
digits: digits,
}) + suffix
);
}, },
fillWidget: function (values) {
fillWidget: function(values) {
var widget = this.$el; var widget = this.$el;
var value = values.value.value; var value = values.value.value;
if (value === undefined) { if (value === undefined) {
@ -42,30 +44,31 @@ odoo.define('kpi_dashboard.IntegerWidget', function (require) {
} }
var previous = values.value.previous; var previous = values.value.previous;
var $change_rate = widget.find('.change-rate');
var $change_rate = widget.find(".change-rate");
if (previous === undefined) { if (previous === undefined) {
$change_rate.toggleClass('active', false);
$change_rate.toggleClass("active", false);
} else { } else {
var difference = 0; var difference = 0;
if (previous !== 0) { if (previous !== 0) {
difference = field_utils.format.integer(
(100 * value / previous) - 100) + '%';
difference =
field_utils.format.integer((100 * value) / previous - 100) +
"%";
} }
$change_rate.toggleClass('active', true);
$change_rate.toggleClass("active", true);
var $difference = widget.find('[data-bind="difference"]'); var $difference = widget.find('[data-bind="difference"]');
$difference.text(difference); $difference.text(difference);
var $arrow = widget.find('[data-bind="arrow"]'); var $arrow = widget.find('[data-bind="arrow"]');
if (value < previous) { if (value < previous) {
$arrow.toggleClass('fa-arrow-up', false);
$arrow.toggleClass('fa-arrow-down', true);
$arrow.toggleClass("fa-arrow-up", false);
$arrow.toggleClass("fa-arrow-down", true);
} else { } else {
$arrow.toggleClass('fa-arrow-up', true);
$arrow.toggleClass('fa-arrow-down', false);
$arrow.toggleClass("fa-arrow-up", true);
$arrow.toggleClass("fa-arrow-down", false);
} }
} }
}, },
}); });
registry.add('integer', IntegerWidget);
registry.add("integer", IntegerWidget);
return IntegerWidget; return IntegerWidget;
}); });

33
kpi_dashboard/static/src/js/widget/meter_widget.js

@ -1,39 +1,34 @@
odoo.define('kpi_dashboard.MeterWidget', function (require) {
odoo.define("kpi_dashboard.MeterWidget", function(require) {
"use strict"; "use strict";
var AbstractWidget = require('kpi_dashboard.AbstractWidget');
var registry = require('kpi_dashboard.widget_registry');
var AbstractWidget = require("kpi_dashboard.AbstractWidget");
var registry = require("kpi_dashboard.widget_registry");
var MeterWidget = AbstractWidget.extend({ var MeterWidget = AbstractWidget.extend({
template: 'kpi_dashboard.meter',
jsLibs: [
'/kpi_dashboard/static/lib/gauge/GaugeMeter.js',
],
fillWidget: function (values) {
template: "kpi_dashboard.meter",
jsLibs: ["/kpi_dashboard/static/lib/gauge/GaugeMeter.js"],
fillWidget: function(values) {
var input = this.$el.find('[data-bind="value"]'); var input = this.$el.find('[data-bind="value"]');
var options = this._getMeterOptions(values); var options = this._getMeterOptions(values);
var margin = (this.widget_dimension_x - options.size)/2;
var margin = (this.widget_dimension_x - options.size) / 2;
input.gaugeMeter(options); input.gaugeMeter(options);
input.parent().css('padding-left', margin);
input.parent().css("padding-left", margin);
}, },
_getMeterOptions: function (values) {
var size = Math.min(
this.widget_size_x,
this.widget_size_y - 40) - 10;
_getMeterOptions: function(values) {
var size = Math.min(this.widget_size_x, this.widget_size_y - 40) - 10;
return { return {
percent: values.value.value, percent: values.value.value,
style: 'Arch',
style: "Arch",
width: 10, width: 10,
size: size, size: size,
prepend: values.prefix !== undefined ? values.prefix : '',
append: values.suffix !== undefined ? values.suffix : '',
prepend: values.prefix !== undefined ? values.prefix : "",
append: values.suffix !== undefined ? values.suffix : "",
color: values.font_color, color: values.font_color,
animate_text_colors: true, animate_text_colors: true,
}; };
}, },
}); });
registry.add('meter', MeterWidget);
registry.add("meter", MeterWidget);
return MeterWidget; return MeterWidget;
}); });

17
kpi_dashboard/static/src/js/widget/number_widget.js

@ -1,21 +1,22 @@
odoo.define('kpi_dashboard.NumberWidget', function (require) {
odoo.define("kpi_dashboard.NumberWidget", function(require) {
"use strict"; "use strict";
var IntegerWidget = require('kpi_dashboard.IntegerWidget');
var registry = require('kpi_dashboard.widget_registry');
var field_utils = require('web.field_utils');
var IntegerWidget = require("kpi_dashboard.IntegerWidget");
var registry = require("kpi_dashboard.widget_registry");
var field_utils = require("web.field_utils");
var NumberWidget = IntegerWidget.extend({ var NumberWidget = IntegerWidget.extend({
digits: [3, 1], digits: [3, 1],
shortNumber: function (num) {
shortNumber: function(num) {
if (Math.abs(num) < 10) { if (Math.abs(num) < 10) {
return field_utils.format.float(num, false, { return field_utils.format.float(num, false, {
digits: [3, 2]});
digits: [3, 2],
});
} }
return this._super.apply(this, arguments)
return this._super.apply(this, arguments);
}, },
}); });
registry.add('number', NumberWidget);
registry.add("number", NumberWidget);
return NumberWidget; return NumberWidget;
}); });

13
kpi_dashboard/static/src/js/widget/text_widget.js

@ -1,17 +1,16 @@
odoo.define('kpi_dashboard.TextWidget', function (require) {
odoo.define("kpi_dashboard.TextWidget", function(require) {
"use strict"; "use strict";
var AbstractWidget = require('kpi_dashboard.AbstractWidget');
var registry = require('kpi_dashboard.widget_registry');
var AbstractWidget = require("kpi_dashboard.AbstractWidget");
var registry = require("kpi_dashboard.widget_registry");
var TextWidget = AbstractWidget.extend({ var TextWidget = AbstractWidget.extend({
template: 'kpi_dashboard.base_text',
fillWidget: function () {
template: "kpi_dashboard.base_text",
fillWidget: function() {
return; return;
}, },
}); });
registry.add('base_text', TextWidget);
registry.add("base_text", TextWidget);
return TextWidget; return TextWidget;
}); });

4
kpi_dashboard/static/src/js/widget_registry.js

@ -1,7 +1,7 @@
odoo.define('kpi_dashboard.widget_registry', function (require) {
odoo.define("kpi_dashboard.widget_registry", function(require) {
"use strict"; "use strict";
var Registry = require('web.Registry');
var Registry = require("web.Registry");
return new Registry(); return new Registry();
}); });

42
kpi_dashboard/static/src/scss/kpi_dashboard.scss

@ -1,11 +1,14 @@
.o_dashboard_view { .o_dashboard_view {
height: 100%; height: 100%;
@include o-webclient-padding($top: $o-horizontal-padding/2, $bottom: $o-horizontal-padding/2);
@include o-webclient-padding(
$top: $o-horizontal-padding/2,
$bottom: $o-horizontal-padding/2
);
display: flex; display: flex;
>.gridster {
> .gridster {
margin: 0 auto; margin: 0 auto;
>ul {
>li {
> ul {
> li {
text-align: center; text-align: center;
list-style: none outside none; list-style: none outside none;
} }
@ -56,7 +59,7 @@
max-width: 400px; max-width: 400px;
} }
.o_kpi_dashboard_manage_section { .o_kpi_dashboard_manage_section {
border-bottom: 1px solid gray('300');
border-bottom: 1px solid gray("300");
margin-bottom: 10px; margin-bottom: 10px;
} }
> div { > div {
@ -71,7 +74,7 @@
} }
.o_kpi_dashboard_toggle_button { .o_kpi_dashboard_toggle_button {
background: white; background: white;
border-color: gray('400');
border-color: gray("400");
z-index: $zindex-dropdown + 1; z-index: $zindex-dropdown + 1;
} }
} }
@ -82,8 +85,9 @@
right: 0; right: 0;
overflow: hidden; overflow: hidden;
cursor: default; cursor: default;
span, b{
margin: 0 23%;
span,
b {
margin: 0 23%;
width: 54%; width: 54%;
position: absolute; position: absolute;
text-align: center; text-align: center;
@ -93,20 +97,20 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
[data-style="Semi"] B{
Margin: 0 10%;
Width: 80%;
[data-style="Semi"] b {
margin: 0 10%;
width: 80%;
} }
S, U{
Text-Decoration:None;
s,
u {
text-decoration: None;
font-height: 100; font-height: 100;
} }
B{
Color: Black;
Font-Weight: 200;
Font-Size: 0.85em;
Opacity: .8;
b {
color: Black;
font-weight: 200;
font-size: 0.85em;
opacity: 0.8;
} }
} }
} }

51
kpi_dashboard/static/src/xml/dashboard.xml

@ -2,11 +2,12 @@
<template> <template>
<t t-name="dashboard_kpi.dashboard"> <t t-name="dashboard_kpi.dashboard">
<div class="gridster kpi_dashboard"> <div class="gridster kpi_dashboard">
<ul/>
<ul />
</div> </div>
</t> </t>
<t t-name="kpi_dashboard.kpi"> <t t-name="kpi_dashboard.kpi">
<li t-att-data-row="widget.row"
<li
t-att-data-row="widget.row"
t-att-data-col="widget.col" t-att-data-col="widget.col"
t-att-data-sizex="widget.sizex" t-att-data-sizex="widget.sizex"
t-att-data-sizey="widget.sizey" t-att-data-sizey="widget.sizey"
@ -14,15 +15,21 @@
</t> </t>
<t t-name="kpi_dashboard.base_text"> <t t-name="kpi_dashboard.base_text">
<div class="kpi"> <div class="kpi">
<h1 class="title" t-esc="widget.values.name"/>
<h1 class="title" t-esc="widget.values.name" />
</div> </div>
</t> </t>
<t t-name="kpi_dashboard.ManagePanel"> <t t-name="kpi_dashboard.ManagePanel">
<t t-if="widget.actions" >
<t t-if="widget.actions">
<t t-foreach="widget.actions" t-as="action_id"> <t t-foreach="widget.actions" t-as="action_id">
<t t-set="action" t-value="widget.actions[action_id]"/>
<t t-set="action" t-value="widget.actions[action_id]" />
<div role="menuitem" class=""> <div role="menuitem" class="">
<a role="menuitem" href="#" class="direct_action" t-att-data-id="action_id" t-att-data-type="action.type">Go to <t t-esc="action.name"/></a>
<a
role="menuitem"
href="#"
class="direct_action"
t-att-data-id="action_id"
t-att-data-type="action.type"
>Go to <t t-esc="action.name" /></a>
</div> </div>
</t> </t>
</t> </t>
@ -31,45 +38,55 @@
<div class="kpi"> <div class="kpi">
<div class="o_kpi_dashboard_manage hidden"> <div class="o_kpi_dashboard_manage hidden">
<a class="o_kpi_dashboard_toggle_button" href="#"> <a class="o_kpi_dashboard_toggle_button" href="#">
<i class="fa fa-ellipsis-v" aria-label="Selection" role="img" title="Selection"/>
<i
class="fa fa-ellipsis-v"
aria-label="Selection"
role="img"
title="Selection"
/>
</a> </a>
</div> </div>
<h1 class="title" t-esc="widget.values.name"/>
<p class="updated_at" data-bind="value_last_update_display"/>
<h1 class="title" t-esc="widget.values.name" />
<p class="updated_at" data-bind="value_last_update_display" />
<div class="container o_kpi_dashboard_manage_panel dropdown-menu"> <div class="container o_kpi_dashboard_manage_panel dropdown-menu">
<t t-call="kpi_dashboard.ManagePanel"/>
<t t-call="kpi_dashboard.ManagePanel" />
</div> </div>
</div> </div>
</t> </t>
<t t-name="kpi_dashboard.number" t-extend="kpi_dashboard.base_widget"> <t t-name="kpi_dashboard.number" t-extend="kpi_dashboard.base_widget">
<t t-jquery="h1" t-operation="after"> <t t-jquery="h1" t-operation="after">
<h2 class="numbervalue"> <h2 class="numbervalue">
<span t-esc="widget.prefix"/><span data-bind="value"/><span t-esc="widget.suffix"/>
<span t-esc="widget.prefix" />
<span data-bind="value" />
<span t-esc="widget.suffix" />
</h2> </h2>
<p class="change-rate"> <p class="change-rate">
<i class="fa" data-bind="arrow"/>
<span data-bind="difference"/>
<i class="fa" data-bind="arrow" />
<span data-bind="difference" />
</p> </p>
</t> </t>
</t> </t>
<t t-name="kpi_dashboard.meter" t-extend="kpi_dashboard.base_widget"> <t t-name="kpi_dashboard.meter" t-extend="kpi_dashboard.base_widget">
<t t-jquery="h1" t-operation="after"> <t t-jquery="h1" t-operation="after">
<div class="centered"> <div class="centered">
<div class="GaugeMeter" data-bind="value"/>
<div class="GaugeMeter" data-bind="value" />
</div> </div>
</t> </t>
</t> </t>
<t t-name="kpi_dashboard.graph" t-extend="kpi_dashboard.base_widget"> <t t-name="kpi_dashboard.graph" t-extend="kpi_dashboard.base_widget">
<t t-jquery="h1" t-operation="after"> <t t-jquery="h1" t-operation="after">
<div class="centered"> <div class="centered">
<div data-bind="value"/>
<div data-bind="value" />
</div> </div>
</t> </t>
</t> </t>
<t t-name="kpi_dashboard.buttons"> <t t-name="kpi_dashboard.buttons">
<div class="o_dashboard_buttons" role="toolbar" aria-label="Main actions"> <div class="o_dashboard_buttons" role="toolbar" aria-label="Main actions">
<button type="button"
class="btn btn-primary o_dashboard_button_add" accesskey="d">
<button
type="button"
class="btn btn-primary o_dashboard_button_add"
accesskey="d"
>
Add to Dashboard Add to Dashboard
</button> </button>
</div> </div>

86
kpi_dashboard/templates/assets.xml

@ -1,28 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<template id="assets_backend"
name="Backend Assets (used in backend interface)"
inherit_id="web.assets_backend">
<template
id="assets_backend"
name="Backend Assets (used in backend interface)"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside"> <xpath expr="." position="inside">
<script type="text/javascript" src="/kpi_dashboard/static/src/js/widget_registry.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/widget/abstract_widget.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/dashboard_renderer.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/dashboard_model.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/dashboard_controller.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/dashboard_view.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/widget/integer_widget.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/widget/number_widget.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/widget/counter_widget.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/widget/meter_widget.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/widget/graph_widget.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/widget/text_widget.js"/>
<script type="text/javascript" src="/kpi_dashboard/static/src/js/field_widget.js"/>
<link rel="stylesheet" type="text/scss" href="/kpi_dashboard/static/src/scss/kpi_dashboard.scss"/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/widget_registry.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/widget/abstract_widget.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/dashboard_renderer.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/dashboard_model.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/dashboard_controller.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/dashboard_view.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/widget/integer_widget.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/widget/number_widget.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/widget/counter_widget.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/widget/meter_widget.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/widget/graph_widget.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/widget/text_widget.js"
/>
<script
type="text/javascript"
src="/kpi_dashboard/static/src/js/field_widget.js"
/>
<link
rel="stylesheet"
type="text/scss"
href="/kpi_dashboard/static/src/scss/kpi_dashboard.scss"
/>
</xpath> </xpath>
</template> </template>
</odoo> </odoo>

24
kpi_dashboard/tests/test_formula.py

@ -1,19 +1,15 @@
# Copyright 2020 Creu Blanca # Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
class TestFormula(TransactionCase): class TestFormula(TransactionCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.kpi = self.env["kpi.kpi"].create( self.kpi = self.env["kpi.kpi"].create(
{
"name": "DEMO KPI",
"widget": "number",
"computation_method": "code",
}
{"name": "DEMO KPI", "widget": "number", "computation_method": "code",}
) )
def test_forbidden_words_01(self): def test_forbidden_words_01(self):
@ -45,10 +41,10 @@ self.env.cr.execute("CoMMiT")
self.kpi.compute() self.kpi.compute()
self.assertEqual(self.kpi.value, {}) self.assertEqual(self.kpi.value, {})
self.kpi.code = """ self.kpi.code = """
result = {}
result['value'] = len(model.search([('id', '=', %s)]))
result['previous'] = len(model.search([('id', '!=', %s)]))
""" % (
result = {{}}
result['value'] = len(model.search([('id', '=', {})]))
result['previous'] = len(model.search([('id', '!=', {})]))
""".format(
self.kpi.id, self.kpi.id,
self.kpi.id, self.kpi.id,
) )
@ -66,10 +62,10 @@ result['previous'] = len(model.search([('id', '!=', %s)]))
self.assertTrue(self.kpi.history_ids) self.assertTrue(self.kpi.history_ids)
self.assertEqual(self.kpi.value, {}) self.assertEqual(self.kpi.value, {})
self.kpi.code = """ self.kpi.code = """
result = {}
result['value'] = len(model.search([('id', '=', %s)]))
result['previous'] = len(model.search([('id', '!=', %s)]))
""" % (
result = {{}}
result['value'] = len(model.search([('id', '=', {})]))
result['previous'] = len(model.search([('id', '!=', {})]))
""".format(
self.kpi.id, self.kpi.id,
self.kpi.id, self.kpi.id,
) )

176
kpi_dashboard/tests/test_kpi_dashboard.py

@ -1,85 +1,92 @@
# Copyright 2020 Creu Blanca # Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError
from odoo.tests.common import Form
from mock import patch from mock import patch
from odoo.exceptions import ValidationError
from odoo.tests.common import Form, TransactionCase
class TestKpiDashboard(TransactionCase): class TestKpiDashboard(TransactionCase):
def setUp(self): def setUp(self):
super(TestKpiDashboard, self).setUp() super(TestKpiDashboard, self).setUp()
self.kpi_01 = self.env['kpi.kpi'].create({
'name': 'KPI 01',
'computation_method': 'function',
'widget': 'number',
'function': 'test_demo_number'
})
self.kpi_02 = self.env['kpi.kpi'].create({
'name': 'KPI 02',
'computation_method': 'function',
'widget': 'number',
'function': 'test_demo_number'
})
self.dashboard = self.env['kpi.dashboard'].create({
'name': 'Dashboard',
'number_of_columns': 4,
'widget_dimension_x': 250,
'widget_dimension_y': 250,
})
self.env['kpi.dashboard.item'].create({
'dashboard_id': self.dashboard.id,
'kpi_id': self.kpi_01.id,
'name': self.kpi_01.name,
'row': 1,
'column': 1,
})
self.env['kpi.dashboard.item'].create({
'dashboard_id': self.dashboard.id,
'name': self.kpi_02.name,
'kpi_id': self.kpi_02.id,
'row': 1,
'column': 2,
})
self.env['kpi.dashboard.item'].create({
'dashboard_id': self.dashboard.id,
'name': 'TITLE',
'row': 2,
'column': 1,
})
self.kpi_01 = self.env["kpi.kpi"].create(
{
"name": "KPI 01",
"computation_method": "function",
"widget": "number",
"function": "test_demo_number",
}
)
self.kpi_02 = self.env["kpi.kpi"].create(
{
"name": "KPI 02",
"computation_method": "function",
"widget": "number",
"function": "test_demo_number",
}
)
self.dashboard = self.env["kpi.dashboard"].create(
{
"name": "Dashboard",
"number_of_columns": 4,
"widget_dimension_x": 250,
"widget_dimension_y": 250,
}
)
self.env["kpi.dashboard.item"].create(
{
"dashboard_id": self.dashboard.id,
"kpi_id": self.kpi_01.id,
"name": self.kpi_01.name,
"row": 1,
"column": 1,
}
)
self.env["kpi.dashboard.item"].create(
{
"dashboard_id": self.dashboard.id,
"name": self.kpi_02.name,
"kpi_id": self.kpi_02.id,
"row": 1,
"column": 2,
}
)
self.env["kpi.dashboard.item"].create(
{"dashboard_id": self.dashboard.id, "name": "TITLE", "row": 2, "column": 1,}
)
def test_constrains_01(self): def test_constrains_01(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.kpi_01.dashboard_item_ids.write({'size_x': 2})
self.kpi_01.dashboard_item_ids.write({"size_x": 2})
def test_constrains_02(self): def test_constrains_02(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.kpi_02.dashboard_item_ids.write({'size_x': 4})
self.kpi_02.dashboard_item_ids.write({"size_x": 4})
def test_constrains_03(self): def test_constrains_03(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.kpi_01.dashboard_item_ids.write({'size_y': 11})
self.kpi_01.dashboard_item_ids.write({"size_y": 11})
def test_menu(self): def test_menu(self):
self.assertFalse(self.dashboard.menu_id) self.assertFalse(self.dashboard.menu_id)
wzd = self.env['kpi.dashboard.menu'].create({
'dashboard_id': self.dashboard.id,
'menu_id': self.env['ir.ui.menu'].search([], limit=1).id,
})
wzd = self.env["kpi.dashboard.menu"].create(
{
"dashboard_id": self.dashboard.id,
"menu_id": self.env["ir.ui.menu"].search([], limit=1).id,
}
)
wzd.generate_menu() wzd.generate_menu()
self.assertTrue(self.dashboard.menu_id) self.assertTrue(self.dashboard.menu_id)
self.assertFalse(self.dashboard.menu_id.groups_id) self.assertFalse(self.dashboard.menu_id.groups_id)
self.dashboard.write({
'group_ids': [
(6, 0, self.env['res.groups'].search([], limit=1).ids)]
})
self.dashboard.write(
{"group_ids": [(6, 0, self.env["res.groups"].search([], limit=1).ids)]}
)
self.assertTrue(self.dashboard.menu_id.groups_id) self.assertTrue(self.dashboard.menu_id.groups_id)
def test_onchange(self): def test_onchange(self):
with Form(self.env['kpi.dashboard']) as dashboard:
dashboard.name = 'New Dashboard'
with Form(self.env["kpi.dashboard"]) as dashboard:
dashboard.name = "New Dashboard"
with dashboard.item_ids.new() as item: with dashboard.item_ids.new() as item:
item.kpi_id = self.kpi_01 item.kpi_id = self.kpi_01
self.assertTrue(item.name) self.assertTrue(item.name)
@ -88,40 +95,38 @@ class TestKpiDashboard(TransactionCase):
data = self.dashboard.read_dashboard() data = self.dashboard.read_dashboard()
title_found = False title_found = False
actions = 0 actions = 0
for item in data['item_ids']:
if not item.get('kpi_id'):
for item in data["item_ids"]:
if not item.get("kpi_id"):
title_found = True title_found = True
if item.get('actions', False):
actions += len(item['actions'])
if item.get("actions", False):
actions += len(item["actions"])
self.assertTrue(title_found) self.assertTrue(title_found)
self.assertEqual(0, actions) self.assertEqual(0, actions)
act01 = self.env['ir.actions.act_window'].search(
[], limit=1)
self.env['kpi.kpi.action'].create({
'kpi_id': self.kpi_01.id,
'action': '%s,%s' % (act01._name, act01.id)
})
act02 = self.env['ir.actions.act_url'].search(
[], limit=1)
self.env['kpi.kpi.action'].create({
'kpi_id': self.kpi_01.id,
'action': '%s,%s' % (act02._name, act02.id)
})
act01 = self.env["ir.actions.act_window"].search([], limit=1)
self.env["kpi.kpi.action"].create(
{"kpi_id": self.kpi_01.id, "action": "{},{}".format(act01._name, act01.id)}
)
act02 = self.env["ir.actions.act_url"].search([], limit=1)
self.env["kpi.kpi.action"].create(
{"kpi_id": self.kpi_01.id, "action": "{},{}".format(act02._name, act02.id)}
)
data = self.dashboard.read_dashboard() data = self.dashboard.read_dashboard()
title_found = False title_found = False
actions = 0 actions = 0
for item in data['item_ids']:
if not item.get('kpi_id'):
for item in data["item_ids"]:
if not item.get("kpi_id"):
title_found = True title_found = True
if item.get('actions', False):
actions += len(item['actions'])
if item.get("actions", False):
actions += len(item["actions"])
self.assertTrue(title_found) self.assertTrue(title_found)
self.assertEqual(2, actions) self.assertEqual(2, actions)
self.assertFalse(data.get("action_id", False)) self.assertFalse(data.get("action_id", False))
wzd = self.env['kpi.dashboard.menu'].create({
'dashboard_id': self.dashboard.id,
'menu_id': self.env['ir.ui.menu'].search([], limit=1).id,
})
wzd = self.env["kpi.dashboard.menu"].create(
{
"dashboard_id": self.dashboard.id,
"menu_id": self.env["ir.ui.menu"].search([], limit=1).id,
}
)
wzd.generate_menu() wzd.generate_menu()
data = self.dashboard.read_dashboard() data = self.dashboard.read_dashboard()
self.assertTrue(data.get("action_id", False)) self.assertTrue(data.get("action_id", False))
@ -129,8 +134,8 @@ class TestKpiDashboard(TransactionCase):
def test_compute(self): def test_compute(self):
self.assertFalse(self.kpi_01.value_last_update) self.assertFalse(self.kpi_01.value_last_update)
with patch( with patch(
"odoo.addons.kpi_dashboard.models.kpi_kpi."
"KpiKpi.test_demo_number", create=True
"odoo.addons.kpi_dashboard.models.kpi_kpi." "KpiKpi.test_demo_number",
create=True,
) as f: ) as f:
f.return_value = {"value": 0} f.return_value = {"value": 0}
self.kpi_01.compute() self.kpi_01.compute()
@ -138,10 +143,9 @@ class TestKpiDashboard(TransactionCase):
def test_compute_model(self): def test_compute_model(self):
self.assertFalse(self.kpi_01.value_last_update) self.assertFalse(self.kpi_01.value_last_update)
self.kpi_01.model_id = self.env.ref('base.model_res_partner')
self.kpi_01.model_id = self.env.ref("base.model_res_partner")
with patch( with patch(
"odoo.addons.base.models.res_partner.Partner.test_demo_number",
create=True
"odoo.addons.base.models.res_partner.Partner.test_demo_number", create=True
) as f: ) as f:
f.return_value = {"value": 0} f.return_value = {"value": 0}
self.kpi_01.compute() self.kpi_01.compute()
@ -153,8 +157,8 @@ class TestKpiDashboard(TransactionCase):
self.assertTrue(self.kpi_01.cron_id) self.assertTrue(self.kpi_01.cron_id)
self.assertFalse(self.kpi_01.value_last_update) self.assertFalse(self.kpi_01.value_last_update)
with patch( with patch(
"odoo.addons.kpi_dashboard.models.kpi_kpi."
"KpiKpi.test_demo_number", create=True
"odoo.addons.kpi_dashboard.models.kpi_kpi." "KpiKpi.test_demo_number",
create=True,
) as f: ) as f:
f.return_value = {"value": 0} f.return_value = {"value": 0}
self.kpi_01.cron_id.method_direct_trigger() self.kpi_01.cron_id.method_direct_trigger()

155
kpi_dashboard/views/kpi_dashboard.xml

@ -1,170 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Creu Blanca <!-- Copyright 2020 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo> <odoo>
<record model="ir.ui.view" id="kpi_dashboard_form_view"> <record model="ir.ui.view" id="kpi_dashboard_form_view">
<field name="name">kpi.dashboard.form (in kpi_dashboard)</field> <field name="name">kpi.dashboard.form (in kpi_dashboard)</field>
<field name="model">kpi.dashboard</field> <field name="model">kpi.dashboard</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<header/>
<header />
<sheet> <sheet>
<div name="button_box" class="oe_button_box"> <div name="button_box" class="oe_button_box">
<button name="%(kpi_dashboard.kpi_dashboard_menu_act_window)d"
type="action"
string="Generate menu"
icon="fa-folder-open-o"
context="{'default_dashboard_id': active_id}"
attrs="{'invisible': [('menu_id', '!=', False)]}"
/>
<button
name="%(kpi_dashboard.kpi_dashboard_menu_act_window)d"
type="action"
string="Generate menu"
icon="fa-folder-open-o"
context="{'default_dashboard_id': active_id}"
attrs="{'invisible': [('menu_id', '!=', False)]}"
/>
</div> </div>
<group> <group>
<field name="name"/>
<field name="menu_id" attrs="{'invisible': [('menu_id', '=', False)]}"/>
<field name="name" />
<field
name="menu_id"
attrs="{'invisible': [('menu_id', '=', False)]}"
/>
</group> </group>
<notebook> <notebook>
<page name="item" string="KPIs"> <page name="item" string="KPIs">
<field name="item_ids"> <field name="item_ids">
<tree editable="bottom"> <tree editable="bottom">
<field name="name"/>
<field name="kpi_id"/>
<field name="column"/>
<field name="row"/>
<field name="size_x"/>
<field name="size_y"/>
<field name="color" widget="color"/>
<field name="font_color" widget="color"/>
<button name="technical_config" string=""
type="object" icon="fa-edit"
groups="base.group_no_one"/>
<field name="name" />
<field name="kpi_id" />
<field name="column" />
<field name="row" />
<field name="size_x" />
<field name="size_y" />
<field name="color" widget="color" />
<field name="font_color" widget="color" />
<button
name="technical_config"
string=""
type="object"
icon="fa-edit"
groups="base.group_no_one"
/>
</tree> </tree>
</field> </field>
</page> </page>
<page name="widget" string="Widget configuration"> <page name="widget" string="Widget configuration">
<group> <group>
<group name="margin"> <group name="margin">
<field name="margin_x"/>
<field name="margin_y"/>
<field name="margin_x" />
<field name="margin_y" />
</group> </group>
<group name="dimension"> <group name="dimension">
<field name="widget_dimension_x"/>
<field name="widget_dimension_y"/>
<field name="number_of_columns"/>
<field name="width"/>
<field name="widget_dimension_x" />
<field name="widget_dimension_y" />
<field name="number_of_columns" />
<field name="width" />
</group> </group>
<group name="color"> <group name="color">
<field name="background_color" widget="color"/>
<field name="background_color" widget="color" />
</group> </group>
<group name="config"> <group name="config">
<field name="compute_on_fly_refresh"/>
<field name="compute_on_fly_refresh" />
</group> </group>
</group> </group>
</page> </page>
<page name="group" string="Groups"> <page name="group" string="Groups">
<field name="group_ids"/>
<field name="group_ids" />
</page> </page>
</notebook> </notebook>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_dashboard_search_view"> <record model="ir.ui.view" id="kpi_dashboard_search_view">
<field name="name">kpi.dashboard.search (in kpi_dashboard)</field> <field name="name">kpi.dashboard.search (in kpi_dashboard)</field>
<field name="model">kpi.dashboard</field> <field name="model">kpi.dashboard</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search> <search>
<field name="name"/>
<field name="name" />
</search> </search>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_dashboard_tree_view"> <record model="ir.ui.view" id="kpi_dashboard_tree_view">
<field name="name">kpi.dashboard.tree (in kpi_dashboard)</field> <field name="name">kpi.dashboard.tree (in kpi_dashboard)</field>
<field name="model">kpi.dashboard</field> <field name="model">kpi.dashboard</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree> <tree>
<field name="name"/>
<field name="name" />
</tree> </tree>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_dashboard_dashboard_view"> <record model="ir.ui.view" id="kpi_dashboard_dashboard_view">
<field name="name">kpi.dashboard.dashboard (in kpi_dashboard)</field> <field name="name">kpi.dashboard.dashboard (in kpi_dashboard)</field>
<field name="model">kpi.dashboard</field> <field name="model">kpi.dashboard</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<dashboard/>
<dashboard />
</field> </field>
</record> </record>
<record model="ir.actions.act_window" id="kpi_dashboard_act_window"> <record model="ir.actions.act_window" id="kpi_dashboard_act_window">
<field name="name">Kpi Dashboard</field> <!-- TODO -->
<field name="name">Kpi Dashboard</field>
<!-- TODO -->
<field name="res_model">kpi.dashboard</field> <field name="res_model">kpi.dashboard</field>
<field name="view_mode">tree,form,dashboard</field> <field name="view_mode">tree,form,dashboard</field>
<field name="domain">[]</field> <field name="domain">[]</field>
<field name="context">{}</field> <field name="context">{}</field>
</record> </record>
<record model="ir.ui.menu" id="kpi_dashboard_menu"> <record model="ir.ui.menu" id="kpi_dashboard_menu">
<field name="name">Configure Dashboard</field> <field name="name">Configure Dashboard</field>
<field name="parent_id" ref="menu_configuration_kpi_dashboards"/> <!-- TODO -->
<field name="action" ref="kpi_dashboard_act_window"/>
<field name="sequence" eval="16"/> <!-- TODO -->
<field name="parent_id" ref="menu_configuration_kpi_dashboards" />
<!-- TODO -->
<field name="action" ref="kpi_dashboard_act_window" />
<field name="sequence" eval="16" />
<!-- TODO -->
</record> </record>
<record model="ir.ui.view" id="kpi_dashboard_item_form_view"> <record model="ir.ui.view" id="kpi_dashboard_item_form_view">
<field name="name">kpi.dashboard.item.form (in kpi_dashboard)</field> <field name="name">kpi.dashboard.item.form (in kpi_dashboard)</field>
<field name="model">kpi.dashboard.item</field> <field name="model">kpi.dashboard.item</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<header/>
<header />
<sheet> <sheet>
<div name="button_box" class="oe_button_box"/>
<div name="button_box" class="oe_button_box" />
<group> <group>
<field name="name"/>
<field name="dashboard_id"/>
<field name="kpi_id"/>
<field name="column"/>
<field name="row"/>
<field name="size_x"/>
<field name="size_y"/>
<field name="color" widget="color"/>
<field name="font_color" widget="color"/>
<field name="name" />
<field name="dashboard_id" />
<field name="kpi_id" />
<field name="column" />
<field name="row" />
<field name="size_x" />
<field name="size_y" />
<field name="color" widget="color" />
<field name="font_color" widget="color" />
</group> </group>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_dashboard_item_config_form_view"> <record model="ir.ui.view" id="kpi_dashboard_item_config_form_view">
<field name="name">kpi.dashboard.item.form (in kpi_dashboard)</field> <field name="name">kpi.dashboard.item.form (in kpi_dashboard)</field>
<field name="model">kpi.dashboard.item</field> <field name="model">kpi.dashboard.item</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<header/>
<header />
<sheet> <sheet>
<group> <group>
<field name="modify_context"/>
<field name="modify_context_expression"
attrs="{'invisible': [('modify_context', '=', False)]}"
widget="ace" options="{'mode': 'python'}"/>
<field name="modify_color"/>
<field name="modify_color_expression"
attrs="{'invisible': [('modify_color', '=', False)]}"
widget="ace" options="{'mode': 'python'}"/>
<field name="modify_context" />
<field
name="modify_context_expression"
attrs="{'invisible': [('modify_context', '=', False)]}"
widget="ace"
options="{'mode': 'python'}"
/>
<field name="modify_color" />
<field
name="modify_color_expression"
attrs="{'invisible': [('modify_color', '=', False)]}"
widget="ace"
options="{'mode': 'python'}"
/>
</group> </group>
</sheet> </sheet>
<footer> <footer>
<button name="write"
string="Save" type="object"
class="oe_highlight"/>
<button special="cancel" string="Cancel"
class="oe_link"/>
<button
name="write"
string="Save"
type="object"
class="oe_highlight"
/>
<button special="cancel" string="Cancel" class="oe_link" />
</footer> </footer>
</form> </form>
</field> </field>
</record> </record>
</odoo> </odoo>

180
kpi_dashboard/views/kpi_kpi.xml

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Creu Blanca <!-- Copyright 2020 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo> <odoo>
<record model="ir.actions.act_window" id="kpi_kpi_history_act_window"> <record model="ir.actions.act_window" id="kpi_kpi_history_act_window">
<field name="name">Kpi History</field> <field name="name">Kpi History</field>
<field name="res_model">kpi.kpi.history</field> <field name="res_model">kpi.kpi.history</field>
@ -11,149 +9,199 @@
<field name="domain">[('kpi_id', '=', active_id)]</field> <field name="domain">[('kpi_id', '=', active_id)]</field>
<field name="context">{}</field> <field name="context">{}</field>
</record> </record>
<record model="ir.ui.view" id="kpi_kpi_history_widget_form_view"> <record model="ir.ui.view" id="kpi_kpi_history_widget_form_view">
<field name="name">kpi.kpi.history.raw.form (in kpi_dashboard)</field> <field name="name">kpi.kpi.history.raw.form (in kpi_dashboard)</field>
<field name="model">kpi.kpi.history</field> <field name="model">kpi.kpi.history</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<field name="create_date" invisible="1"/>
<field name="widget" invisible="1"/>
<field name="name" invisible="1"/>
<field name="value"
widget="kpi"
options="{'date': 'create_date', 'widget': 'widget', 'name': 'name'}"/>
<field name="create_date" invisible="1" />
<field name="widget" invisible="1" />
<field name="name" invisible="1" />
<field
name="value"
widget="kpi"
options="{'date': 'create_date', 'widget': 'widget', 'name': 'name'}"
/>
</form> </form>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_kpi_history_raw_form_view"> <record model="ir.ui.view" id="kpi_kpi_history_raw_form_view">
<field name="name">kpi.kpi.history.raw.form (in kpi_dashboard)</field> <field name="name">kpi.kpi.history.raw.form (in kpi_dashboard)</field>
<field name="model">kpi.kpi.history</field> <field name="model">kpi.kpi.history</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<field name="raw_value"/>
<field name="raw_value" />
</form> </form>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_kpi_history_tree_view"> <record model="ir.ui.view" id="kpi_kpi_history_tree_view">
<field name="name">kpi.kpi.history.tree (in kpi_dashboard)</field> <field name="name">kpi.kpi.history.tree (in kpi_dashboard)</field>
<field name="model">kpi.kpi.history</field> <field name="model">kpi.kpi.history</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree create="0" delete="0"> <tree create="0" delete="0">
<field name="create_date"/>
<button name="show_form" string="Show value" type="object"
context="{'form_id': %(kpi_dashboard.kpi_kpi_history_widget_form_view)d}"
<field name="create_date" />
<button
name="show_form"
string="Show value"
type="object"
context="{'form_id': %(kpi_dashboard.kpi_kpi_history_widget_form_view)d}"
/> />
<button name="show_form" string="Raw value" type="object"
context="{'form_id': %(kpi_dashboard.kpi_kpi_history_raw_form_view)d}"
<button
name="show_form"
string="Raw value"
type="object"
context="{'form_id': %(kpi_dashboard.kpi_kpi_history_raw_form_view)d}"
/> />
</tree> </tree>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_kpi_form_view"> <record model="ir.ui.view" id="kpi_kpi_form_view">
<field name="name">kpi.kpi.form (in kpi_dashboard)</field> <field name="name">kpi.kpi.form (in kpi_dashboard)</field>
<field name="model">kpi.kpi</field> <field name="model">kpi.kpi</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<header> <header>
<button name="generate_cron" string="Generate cron" type="object"
attrs="{'invisible': ['|', ('cron_id', '!=',False), ('compute_on_fly', '=', True)]}"/>
<button name="compute" string="Compute now" type="object" attrs="{'invisible': [('compute_on_fly', '=', True)]}"/>
<button
name="generate_cron"
string="Generate cron"
type="object"
attrs="{'invisible': ['|', ('cron_id', '!=',False), ('compute_on_fly', '=', True)]}"
/>
<button
name="compute"
string="Compute now"
type="object"
attrs="{'invisible': [('compute_on_fly', '=', True)]}"
/>
</header> </header>
<sheet> <sheet>
<div class="oe_button_box" name="button_box"> <div class="oe_button_box" name="button_box">
<button name="%(kpi_dashboard.kpi_kpi_history_act_window)d"
string="Show history"
type="action"
attrs="{'invisible': ['|', ('store_history', '=', False), ('compute_on_fly', '=', True)]}"
icon="fa-history"/>
<button string="Show value" type="object" name="show_value"
icon="fa-paint-brush"
<button
name="%(kpi_dashboard.kpi_kpi_history_act_window)d"
string="Show history"
type="action"
attrs="{'invisible': ['|', ('store_history', '=', False), ('compute_on_fly', '=', True)]}"
icon="fa-history"
/>
<button
string="Show value"
type="object"
name="show_value"
icon="fa-paint-brush"
/> />
</div> </div>
<h2> <h2>
<field name="name"/>
<field name="name" />
</h2> </h2>
<group> <group>
<group> <group>
<field name="computation_method"/>
<field name="widget"/>
<field name="store_history" attrs="{'invisible': [('compute_on_fly', '=', True)]}"/>
<field name="store_history_interval" attrs="{'invisible': [('store_history', '=', False)]}"/>
<field name="store_history_interval_number" attrs="{'invisible': [('store_history', '=', False)]}"/>
<field name="compute_on_fly" attrs="{'invisible': [('store_history', '=', True)]}"/>
<field name="model_id" attrs="{'invisible': [('computation_method', '!=', 'function')]}"/>
<field name="function" attrs="{'required': [('computation_method', '=', 'function')], 'invisible': [('computation_method', '!=', 'function')]}"/>
<field name="args" attrs="{'invisible': [('computation_method', '!=', 'function')]}"/>
<field name="kwargs" attrs="{'invisible': [('computation_method', '!=', 'function')]}"/>
<field name="computation_method" />
<field name="widget" />
<field
name="store_history"
attrs="{'invisible': [('compute_on_fly', '=', True)]}"
/>
<field
name="store_history_interval"
attrs="{'invisible': [('store_history', '=', False)]}"
/>
<field
name="store_history_interval_number"
attrs="{'invisible': [('store_history', '=', False)]}"
/>
<field
name="compute_on_fly"
attrs="{'invisible': [('store_history', '=', True)]}"
/>
<field
name="model_id"
attrs="{'invisible': [('computation_method', '!=', 'function')]}"
/>
<field
name="function"
attrs="{'required': [('computation_method', '=', 'function')], 'invisible': [('computation_method', '!=', 'function')]}"
/>
<field
name="args"
attrs="{'invisible': [('computation_method', '!=', 'function')]}"
/>
<field
name="kwargs"
attrs="{'invisible': [('computation_method', '!=', 'function')]}"
/>
</group> </group>
<group> <group>
<field name="cron_id" attrs="{'invisible': [('cron_id', '=',False)]}" readonly="True"/>
<field
name="cron_id"
attrs="{'invisible': [('cron_id', '=',False)]}"
readonly="True"
/>
</group> </group>
<group> <group>
<field name="suffix"/>
<field name="prefix"/>
<field name="suffix" />
<field name="prefix" />
</group> </group>
</group> </group>
<notebook> <notebook>
<page name="action" string="Actions"> <page name="action" string="Actions">
<field name="action_ids"> <field name="action_ids">
<tree editable="bottom"> <tree editable="bottom">
<field name="action"/>
<field name="context"/>
<field name="action" />
<field name="context" />
</tree> </tree>
</field> </field>
</page> </page>
<page name="code" string="Code" attrs="{'invisible': [('computation_method', '!=', 'code')]}">
<field name="code" widget="ace"
options="{'mode': 'python'}"
placeholder="Enter Python code here."/>
<page
name="code"
string="Code"
attrs="{'invisible': [('computation_method', '!=', 'code')]}"
>
<field
name="code"
widget="ace"
options="{'mode': 'python'}"
placeholder="Enter Python code here."
/>
</page> </page>
</notebook> </notebook>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_kpi_widget_form_view"> <record model="ir.ui.view" id="kpi_kpi_widget_form_view">
<field name="name">kpi.kpi.raw.form (in kpi_dashboard)</field> <field name="name">kpi.kpi.raw.form (in kpi_dashboard)</field>
<field name="model">kpi.kpi</field> <field name="model">kpi.kpi</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<field name="computed_date" invisible="1"/>
<field name="widget" invisible="1"/>
<field name="name" invisible="1"/>
<field name="computed_value"
widget="kpi"
options="{'date': 'computed_date', 'widget': 'widget', 'name': 'name'}"/>
<field name="computed_date" invisible="1" />
<field name="widget" invisible="1" />
<field name="name" invisible="1" />
<field
name="computed_value"
widget="kpi"
options="{'date': 'computed_date', 'widget': 'widget', 'name': 'name'}"
/>
</form> </form>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_kpi_search_view"> <record model="ir.ui.view" id="kpi_kpi_search_view">
<field name="name">kpi.kpi.search (in kpi_dashboard)</field> <field name="name">kpi.kpi.search (in kpi_dashboard)</field>
<field name="model">kpi.kpi</field> <field name="model">kpi.kpi</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search> <search>
<field name="name"/>
<field name="name" />
</search> </search>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="kpi_kpi_tree_view"> <record model="ir.ui.view" id="kpi_kpi_tree_view">
<field name="name">kpi.kpi.tree (in kpi_dashboard)</field> <field name="name">kpi.kpi.tree (in kpi_dashboard)</field>
<field name="model">kpi.kpi</field> <field name="model">kpi.kpi</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree> <tree>
<field name="name"/>
<field name="name" />
</tree> </tree>
</field> </field>
</record> </record>
<record model="ir.actions.act_window" id="kpi_kpi_act_window"> <record model="ir.actions.act_window" id="kpi_kpi_act_window">
<field name="name">Kpi</field> <field name="name">Kpi</field>
<field name="res_model">kpi.kpi</field> <field name="res_model">kpi.kpi</field>
@ -161,12 +209,10 @@
<field name="domain">[]</field> <field name="domain">[]</field>
<field name="context">{}</field> <field name="context">{}</field>
</record> </record>
<record model="ir.ui.menu" id="kpi_kpi_menu"> <record model="ir.ui.menu" id="kpi_kpi_menu">
<field name="name">Configure Kpi</field> <field name="name">Configure Kpi</field>
<field name="parent_id" ref="menu_configuration_kpi_dashboards"/>
<field name="action" ref="kpi_kpi_act_window"/>
<field name="sequence" eval="20"/>
<field name="parent_id" ref="menu_configuration_kpi_dashboards" />
<field name="action" ref="kpi_kpi_act_window" />
<field name="sequence" eval="20" />
</record> </record>
</odoo> </odoo>

15
kpi_dashboard/views/kpi_menu.xml

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Creu Blanca <!-- Copyright 2020 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo> <odoo>
<!-- CONFIGURATION --> <!-- CONFIGURATION -->
<menuitem id="menu_configuration_kpi_dashboards"
name="KPI Dashboards"
parent="base.menu_reporting_config"
groups="kpi_dashboard.group_kpi_dashboard_manager"
sequence="10"/>
<menuitem
id="menu_configuration_kpi_dashboards"
name="KPI Dashboards"
parent="base.menu_reporting_config"
groups="kpi_dashboard.group_kpi_dashboard_manager"
sequence="10"
/>
</odoo> </odoo>

25
kpi_dashboard/wizards/kpi_dashboard_menu.xml

@ -1,31 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Creu Blanca <!-- Copyright 2020 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo> <odoo>
<record model="ir.ui.view" id="kpi_dashboard_menu_form_view"> <record model="ir.ui.view" id="kpi_dashboard_menu_form_view">
<field name="name">kpi.dashboard.menu.form (in kpi_dashboard)</field> <field name="name">kpi.dashboard.menu.form (in kpi_dashboard)</field>
<field name="model">kpi.dashboard.menu</field> <field name="model">kpi.dashboard.menu</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Generate Menu"> <form string="Generate Menu">
<group> <group>
<field name="dashboard_id" invisible="1"/>
<field name="menu_id"/>
<field name="dashboard_id" invisible="1" />
<field name="menu_id" />
</group> </group>
<footer> <footer>
<button name="generate_menu"
string="Generate"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
<button
name="generate_menu"
string="Generate"
class="btn-primary"
type="object"
/>
<button string="Cancel" class="btn-default" special="cancel" />
</footer> </footer>
</form> </form>
</field> </field>
</record> </record>
<record model="ir.actions.act_window" id="kpi_dashboard_menu_act_window"> <record model="ir.actions.act_window" id="kpi_dashboard_menu_act_window">
<field name="name">Kpi Dashboard Menu</field> <field name="name">Kpi Dashboard Menu</field>
<field name="res_model">kpi.dashboard.menu</field> <field name="res_model">kpi.dashboard.menu</field>
@ -33,6 +30,4 @@
<field name="context">{}</field> <field name="context">{}</field>
<field name="target">new</field> <field name="target">new</field>
</record> </record>
</odoo> </odoo>
Loading…
Cancel
Save