Browse Source

[12.0][ADD] kpi_dashboard

12.0
Enric Tobella 4 years ago
parent
commit
17f5daefc8
  1. 96
      kpi_dashboard/README.rst
  2. 2
      kpi_dashboard/__init__.py
  3. 23
      kpi_dashboard/__manifest__.py
  4. 4
      kpi_dashboard/models/__init__.py
  5. 10
      kpi_dashboard/models/ir_actions_act_window_view.py
  6. 10
      kpi_dashboard/models/ir_ui_view.py
  7. 182
      kpi_dashboard/models/kpi_dashboard.py
  8. 110
      kpi_dashboard/models/kpi_kpi.py
  9. 19
      kpi_dashboard/readme/CONFIGURE.rst
  10. 1
      kpi_dashboard/readme/CONTRIBUTORS.rst
  11. 1
      kpi_dashboard/readme/DESCRIPTION.rst
  12. 9
      kpi_dashboard/security/ir.model.access.csv
  13. 22
      kpi_dashboard/security/security.xml
  14. BIN
      kpi_dashboard/static/description/icon.png
  15. 449
      kpi_dashboard/static/description/index.html
  16. 276
      kpi_dashboard/static/lib/gauge/GaugeMeter.js
  17. 2
      kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css
  18. 2
      kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js
  19. 76
      kpi_dashboard/static/src/js/dashboard_controller.js
  20. 23
      kpi_dashboard/static/src/js/dashboard_model.js
  21. 74
      kpi_dashboard/static/src/js/dashboard_renderer.js
  22. 44
      kpi_dashboard/static/src/js/dashboard_view.js
  23. 91
      kpi_dashboard/static/src/js/widget/abstract_widget.js
  24. 108
      kpi_dashboard/static/src/js/widget/graph_widget.js
  25. 39
      kpi_dashboard/static/src/js/widget/meter_widget.js
  26. 72
      kpi_dashboard/static/src/js/widget/number_widget.js
  27. 17
      kpi_dashboard/static/src/js/widget/text_widget.js
  28. 7
      kpi_dashboard/static/src/js/widget_registry.js
  29. 112
      kpi_dashboard/static/src/scss/kpi_dashboard.scss
  30. 76
      kpi_dashboard/static/src/xml/dashboard.xml
  31. 111
      kpi_dashboard/views/kpi_dashboard.xml
  32. 89
      kpi_dashboard/views/kpi_kpi.xml
  33. 12
      kpi_dashboard/views/kpi_menu.xml
  34. 25
      kpi_dashboard/views/webclient_templates.xml
  35. 1
      kpi_dashboard/wizards/__init__.py
  36. 17
      kpi_dashboard/wizards/kpi_dashboard_menu.py
  37. 38
      kpi_dashboard/wizards/kpi_dashboard_menu.xml

96
kpi_dashboard/README.rst

@ -0,0 +1,96 @@
=============
Kpi Dashboard
=============
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Freporting--engine-lightgray.png?logo=github
:target: https://github.com/OCA/reporting-engine/tree/12.0/kpi_dashboard
:alt: OCA/reporting-engine
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/reporting-engine-12-0/reporting-engine-12-0-kpi_dashboard
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/143/12.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module adds new kinds of dashboards on a specific new type of view.
**Table of contents**
.. contents::
:local:
Configuration
=============
Configure KPIs
~~~~~~~~~~~~~~
#. Access `Dashboards > Configuration > KPI Dashboards > Configure KPI`
#. Create a new KPI specifying the computation method and the kpi type
#. Number: result must contain a `value` and, if needed, a `previous`
#. Meter: result must contain `value`, `min` and `max`
#. Graph: result must contain a list on `graphs` containing `values`, `title` and `key`
Configure dashboards
~~~~~~~~~~~~~~~~~~~~
#. Access `Dashboards > Configuration > KPI Dashboards > Configure Dashboards`
#. Create a new dashboard and specify all the standard parameters on `Widget configuration`
#. Append elements on KPIs
#. You can preview the element using the dashboard view
#. You can create the menu entry directly using the `Generate menu` button
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/reporting-engine/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/reporting-engine/issues/new?body=module:%20kpi_dashboard%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Creu Blanca
Contributors
~~~~~~~~~~~~
* Enric Tobella <etobella@creublanca.es>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/reporting-engine <https://github.com/OCA/reporting-engine/tree/12.0/kpi_dashboard>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

2
kpi_dashboard/__init__.py

@ -0,0 +1,2 @@
from . import models
from . import wizards

23
kpi_dashboard/__manifest__.py

@ -0,0 +1,23 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Kpi Dashboard",
"summary": """
Create Dashboards using kpis""",
"version": "12.0.1.0.0",
"license": "AGPL-3",
"author": "Creu Blanca,Odoo Community Association (OCA)",
"website": "https://github.com/reporting-engine",
"depends": ["bus", "board", "base_sparse_field", "web_widget_color"],
"qweb": ["static/src/xml/dashboard.xml"],
"data": [
"wizards/kpi_dashboard_menu.xml",
"security/security.xml",
"security/ir.model.access.csv",
"views/kpi_menu.xml",
"views/webclient_templates.xml",
"views/kpi_kpi.xml",
"views/kpi_dashboard.xml",
],
}

4
kpi_dashboard/models/__init__.py

@ -0,0 +1,4 @@
from . import kpi_dashboard
from . import kpi_kpi
from . import ir_actions_act_window_view
from . import ir_ui_view

10
kpi_dashboard/models/ir_actions_act_window_view.py

@ -0,0 +1,10 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class IrActionsActWindowView(models.Model):
_inherit = "ir.actions.act_window.view"
view_mode = fields.Selection(selection_add=[("dashboard", "Dashboard")])

10
kpi_dashboard/models/ir_ui_view.py

@ -0,0 +1,10 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class IrUiView(models.Model):
_inherit = "ir.ui.view"
type = fields.Selection(selection_add=[("dashboard", "Dashboard")])

182
kpi_dashboard/models/kpi_dashboard.py

@ -0,0 +1,182 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class KpiDashboard(models.Model):
_name = "kpi.dashboard"
_description = "Dashboard"
name = fields.Char(required=True)
active = fields.Boolean(default=True,)
item_ids = fields.One2many(
"kpi.dashboard.item", inverse_name="dashboard_id", copy=True,
)
number_of_columns = fields.Integer(default=5, required=True)
width = fields.Integer(compute="_compute_width")
margin_y = fields.Integer(default=10, required=True)
margin_x = fields.Integer(default=10, required=True)
widget_dimension_x = fields.Integer(default=250, required=True)
widget_dimension_y = fields.Integer(default=250, required=True)
background_color = fields.Char(required=True, default="#f9f9f9")
group_ids = fields.Many2many("res.groups",)
menu_id = fields.Many2one("ir.ui.menu", copy=False)
def write(self, vals):
res = super().write(vals)
if "group_ids" in vals:
for rec in self:
if rec.menu_id:
rec.menu_id.write(
{"groups_id": [(6, 0, rec.group_ids.ids)]}
)
return res
@api.depends("widget_dimension_x", "margin_x", "number_of_columns")
def _compute_width(self):
for rec in self:
rec.width = (
rec.margin_x * (rec.number_of_columns + 1)
+ rec.widget_dimension_x * rec.number_of_columns
)
def read_dashboard(self):
self.ensure_one()
result = {
"name": self.name,
"width": self.width,
"item_ids": self.item_ids.read_dashboard(),
"max_cols": self.number_of_columns,
"margin_x": self.margin_x,
"margin_y": self.margin_y,
"widget_dimension_x": self.widget_dimension_x,
"widget_dimension_y": self.widget_dimension_y,
"background_color": self.background_color,
}
if self.menu_id:
result["action_id"] = self.menu_id.action.id
return result
def _generate_menu_vals(self, menu, action):
return {
"parent_id": menu.id or False,
"name": self.name,
"action": "%s,%s" % (action._name, action.id),
"groups_id": [(6, 0, self.group_ids.ids)],
}
def _generate_action_vals(self, menu):
return {
"name": self.name,
"res_model": self._name,
"view_mode": "dashboard",
"res_id": self.id,
}
def _generate_menu(self, menu):
action = self.env["ir.actions.act_window"].create(
self._generate_action_vals(menu)
)
self.menu_id = self.env["ir.ui.menu"].create(
self._generate_menu_vals(menu, action)
)
class KpiDashboardItem(models.Model):
_name = "kpi.dashboard.item"
_description = "Dashboard Items"
_order = "row asc, column asc"
name = fields.Char(required=True)
kpi_id = fields.Many2one("kpi.kpi")
dashboard_id = fields.Many2one("kpi.dashboard", required=True,)
column = 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')
size_x = fields.Integer(required=True, default=1)
size_y = fields.Integer(required=True, default=1)
color = fields.Char()
font_color = fields.Char()
@api.depends('row', 'size_y')
def _compute_end_row(self):
for r in self:
r.end_row = r.row + r.size_y - 1
@api.depends('column', 'size_x')
def _compute_end_column(self):
for r in self:
r.end_column = r.column + r.size_x - 1
@api.constrains('size_y')
def _check_size_y(self):
for rec in self:
if rec.size_y > 10:
raise ValidationError(_(
'Size Y of the widget cannot be bigger than 10'))
def _check_size_domain(self):
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),
]
@api.constrains('end_row', 'end_column', 'row', 'column')
def _check_size(self):
for r in self:
if self.search(r._check_size_domain(), limit=1):
raise ValidationError(_(
'Widgets cannot be crossed by other widgets'
))
if r.end_column > r.dashboard_id.number_of_columns:
raise ValidationError(_(
'Widget %s is bigger than expected'
) % r.display_name)
@api.onchange("kpi_id")
def _onchange_kpi(self):
for rec in self:
if not rec.name and rec.kpi_id:
rec.name = rec.kpi_id.name
def _read_dashboard(self):
vals = {
"id": self.id,
"name": self.name,
"col": self.column,
"row": self.row,
"sizex": self.size_x,
"sizey": self.size_y,
"color": self.color,
"font_color": self.font_color or "000000",
}
if self.kpi_id:
vals.update(
{
"widget": self.kpi_id.widget,
"kpi_id": self.kpi_id.id,
"suffix": self.kpi_id.suffix or "",
"prefix": self.kpi_id.prefix or "",
"value": self.kpi_id.value,
"value_last_update": self.kpi_id.value_last_update,
}
)
if self.kpi_id.action_ids:
vals["actions"] = self.kpi_id.action_ids.read_dashboard()
else:
vals["widget"] = "base_text"
return vals
def read_dashboard(self):
result = []
for kpi in self:
result.append(kpi._read_dashboard())
return result

110
kpi_dashboard/models/kpi_kpi.py

@ -0,0 +1,110 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
import ast
class KpiKpi(models.Model):
_name = "kpi.kpi"
_description = "Kpi Kpi"
name = fields.Char(required=True)
active = fields.Boolean(default=True)
cron_id = fields.Many2one("ir.cron", readonly=True, copy=False)
computation_method = fields.Selection(
[("function", "Function")], required=True
)
value = fields.Serialized()
dashboard_item_ids = fields.One2many("kpi.dashboard.item", inverse_name="kpi_id")
model_id = fields.Many2one("ir.model",)
function = fields.Char()
args = fields.Char()
kwargs = fields.Char()
widget = fields.Selection(
[("number", "Number"), ("meter", "Meter"), ("graph", "Graph")],
required=True,
default="number",
)
value_last_update = fields.Datetime(readonly=True)
prefix = fields.Char()
suffix = fields.Char()
action_ids = fields.One2many(
"kpi.kpi.action",
inverse_name='kpi_id',
help="Actions that can be opened from the KPI"
)
def _cron_vals(self):
return {
"name": self.name,
"model_id": self.env.ref("kpi_dashboard.model_kpi_kpi").id,
"interval_number": 1,
"interval_type": "hours",
"state": "code",
"code": "model.browse(%s).compute()" % self.id,
"active": True,
}
def compute(self):
for record in self:
record._compute()
return True
def _compute(self):
self.write(
{
"value": getattr(
self, "_compute_value_%s" % self.computation_method
)()
}
)
notifications = []
for dashboard_item in self.dashboard_item_ids:
channel = "kpi_dashboard_%s" % dashboard_item.dashboard_id.id
notifications.append([channel, dashboard_item._read_dashboard()])
if notifications:
self.env["bus.bus"].sendmany(notifications)
def _compute_value_function(self):
obj = self
if self.model_id:
obj = self.env[self.model_id.model]
args = ast.literal_eval(self.args or "[]")
kwargs = ast.literal_eval(self.kwargs or "{}")
return getattr(obj, self.function)(*args, **kwargs)
def generate_cron(self):
self.ensure_one()
self.cron_id = self.env["ir.cron"].create(self._cron_vals())
@api.multi
def write(self, vals):
if "value" in vals:
vals["value_last_update"] = fields.Datetime.now()
return super().write(vals)
class KpiKpiAction(models.Model):
_name = 'kpi.kpi.action'
_description = 'KPI action'
kpi_id = fields.Many2one('kpi.kpi', required=True, ondelete='cascade')
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')],
required=True,
)
def read_dashboard(self):
result = []
for r in self:
result.append({
'id': r.action.id,
'type': r.action._name,
'name': r.action.name
})
return result

19
kpi_dashboard/readme/CONFIGURE.rst

@ -0,0 +1,19 @@
Configure KPIs
~~~~~~~~~~~~~~
#. Access `Dashboards > Configuration > KPI Dashboards > Configure KPI`
#. Create a new KPI specifying the computation method and the kpi type
#. Number: result must contain a `value` and, if needed, a `previous`
#. Meter: result must contain `value`, `min` and `max`
#. Graph: result must contain a list on `graphs` containing `values`, `title` and `key`
Configure dashboards
~~~~~~~~~~~~~~~~~~~~
#. Access `Dashboards > Configuration > KPI Dashboards > Configure Dashboards`
#. Create a new dashboard and specify all the standard parameters on `Widget configuration`
#. Append elements on KPIs
#. You can preview the element using the dashboard view
#. You can create the menu entry directly using the `Generate menu` button

1
kpi_dashboard/readme/CONTRIBUTORS.rst

@ -0,0 +1 @@
* Enric Tobella <etobella@creublanca.es>

1
kpi_dashboard/readme/DESCRIPTION.rst

@ -0,0 +1 @@
This module adds new kinds of dashboards on a specific new type of view.

9
kpi_dashboard/security/ir.model.access.csv

@ -0,0 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_kpi_dashboard,access_kpi_dashboard,model_kpi_dashboard,base.group_user,1,0,0,0
access_kpi_dashboard_kpi,access_kpi_dashboard_kpi,model_kpi_dashboard_item,base.group_user,1,0,0,0
access_kpi_kpi,access_kpi_kpi,model_kpi_kpi,base.group_user,1,0,0,0
access_kpi_kpi_action,access_kpi_kpi_action,model_kpi_kpi_action,base.group_user,1,0,0,0
manage_kpi_dashboard,manage_kpi_dashboard,model_kpi_dashboard,group_kpi_dashboard_manager,1,1,1,1
manage_kpi_dashboard_kpi,manage_kpi_dashboard_kpi,model_kpi_dashboard_item,group_kpi_dashboard_manager,1,1,1,1
manage_kpi_kpi,manage_kpi_kpi,model_kpi_kpi,group_kpi_dashboard_manager,1,1,1,1
manage_kpi_kpi_action,manage_kpi_kpi_action,model_kpi_kpi_action,group_kpi_dashboard_manager,1,1,1,1

22
kpi_dashboard/security/security.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="group_kpi_dashboard_manager" model="res.groups">
<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'))]"/>
</record>
<data noupdate="1">
<record id="rule_kpi_dashboard" model="ir.rule">
<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'))]"/>
</record>
<record id="rule_kpi_dashboard_all" model="ir.rule">
<field name="name">KPI Dashboard: All</field>
<field name="model_id" ref="model_kpi_dashboard"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_kpi_dashboard_manager'))]"/>
</record>
</data>
</odoo>

BIN
kpi_dashboard/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

449
kpi_dashboard/static/description/index.html

@ -0,0 +1,449 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" />
<title>Kpi Dashboard</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="kpi-dashboard">
<h1 class="title">Kpi Dashboard</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/reporting-engine/tree/12.0/kpi_dashboard"><img alt="OCA/reporting-engine" src="https://img.shields.io/badge/github-OCA%2Freporting--engine-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/reporting-engine-12-0/reporting-engine-12-0-kpi_dashboard"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/143/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module adds new kinds of dashboards on a specific new type of view.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a><ul>
<li><a class="reference internal" href="#configure-kpis" id="id2">Configure KPIs</a></li>
<li><a class="reference internal" href="#configure-dashboards" id="id3">Configure dashboards</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="id4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
<div class="section" id="configure-kpis">
<h2><a class="toc-backref" href="#id2">Configure KPIs</a></h2>
<ol class="arabic simple">
<li>Access <cite>Dashboards &gt; Configuration &gt; KPI Dashboards &gt; Configure KPI</cite></li>
<li>Create a new KPI specifying the computation method and the kpi type<ol class="arabic">
<li>Number: result must contain a <cite>value</cite> and, if needed, a <cite>previous</cite></li>
<li>Meter: result must contain <cite>value</cite>, <cite>min</cite> and <cite>max</cite></li>
<li>Graph: result must contain a list on <cite>graphs</cite> containing <cite>values</cite>, <cite>title</cite> and <cite>key</cite></li>
</ol>
</li>
</ol>
</div>
<div class="section" id="configure-dashboards">
<h2><a class="toc-backref" href="#id3">Configure dashboards</a></h2>
<ol class="arabic simple">
<li>Access <cite>Dashboards &gt; Configuration &gt; KPI Dashboards &gt; Configure Dashboards</cite></li>
<li>Create a new dashboard and specify all the standard parameters on <cite>Widget configuration</cite></li>
<li>Append elements on KPIs</li>
<li>You can preview the element using the dashboard view</li>
<li>You can create the menu entry directly using the <cite>Generate menu</cite> button</li>
</ol>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id4">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/reporting-engine/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/reporting-engine/issues/new?body=module:%20kpi_dashboard%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id5">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
<ul class="simple">
<li>Creu Blanca</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id7">Contributors</a></h2>
<ul class="simple">
<li>Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id8">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/reporting-engine/tree/12.0/kpi_dashboard">OCA/reporting-engine</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

276
kpi_dashboard/static/lib/gauge/GaugeMeter.js

@ -0,0 +1,276 @@
;
/*
* AshAlom Gauge Meter. Version 2.0.0
* Copyright AshAlom.com All rights reserved.
* https://github.com/AshAlom/GaugeMeter <- Deleted!
* https://github.com/githubsrinath/GaugeMeter <- Backup original.
*
* Original created by Dr Ash Alom
*
* This is a bug fixed and modified version of the AshAlom Gauge Meter.
* Copyright 2018 Michael Wolf (Mictronics)
* https://github.com/mictronics/GaugeMeter
*
*/
!function ($) {
$.fn.gaugeMeter = function (t) {
var defaults = $.extend({
id: "",
percent: 0,
used: null,
min: null,
total: null,
size: 100,
prepend: "",
append: "",
theme: "Red-Gold-Green",
color: "",
back: "RGBa(0,0,0,.06)",
width: 3,
style: "Full",
stripe: "0",
animationstep: 1,
animate_gauge_colors: false,
animate_text_colors: false,
label: "",
label_color: "Black",
text: "",
text_size: 0.22,
fill: "",
showvalue: false
}, t);
return this.each(function () {
function getThemeColor(e) {
var t = "#2C94E0";
return e || (e = 1e-14),
"Red-Gold-Green" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#e32100"), e > 20 && (t = "#f35100"), e > 30 && (t = "#ff8700"), e > 40 && (t = "#ffb800"), e > 50 && (t = "#ffd900"), e > 60 && (t = "#dcd800"), e > 70 && (t = "#a6d900"), e > 80 && (t = "#69d900"), e > 90 && (t = "#32d900")),
"Green-Gold-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#69d900"), e > 20 && (t = "#a6d900"), e > 30 && (t = "#dcd800"), e > 40 && (t = "#ffd900"), e > 50 && (t = "#ffb800"), e > 60 && (t = "#ff8700"), e > 70 && (t = "#f35100"), e > 80 && (t = "#e32100"), e > 90 && (t = "#d90000")),
"Green-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#41c900"), e > 20 && (t = "#56b300"), e > 30 && (t = "#6f9900"), e > 40 && (t = "#8a7b00"), e > 50 && (t = "#a75e00"), e > 60 && (t = "#c24000"), e > 70 && (t = "#db2600"), e > 80 && (t = "#f01000"), e > 90 && (t = "#ff0000")),
"Red-Green" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#f01000"), e > 20 && (t = "#db2600"), e > 30 && (t = "#c24000"), e > 40 && (t = "#a75e00"), e > 50 && (t = "#8a7b00"), e > 60 && (t = "#6f9900"), e > 70 && (t = "#56b300"), e > 80 && (t = "#41c900"), e > 90 && (t = "#32d900")),
"DarkBlue-LightBlue" === option.theme && (e > 0 && (t = "#2c94e0"), e > 10 && (t = "#2b96e1"), e > 20 && (t = "#2b99e4"), e > 30 && (t = "#2a9ce7"), e > 40 && (t = "#28a0e9"), e > 50 && (t = "#26a4ed"), e > 60 && (t = "#25a8f0"), e > 70 && (t = "#24acf3"), e > 80 && (t = "#23aff5"), e > 90 && (t = "#21b2f7")),
"LightBlue-DarkBlue" === option.theme && (e > 0 && (t = "#21b2f7"), e > 10 && (t = "#23aff5"), e > 20 && (t = "#24acf3"), e > 30 && (t = "#25a8f0"), e > 40 && (t = "#26a4ed"), e > 50 && (t = "#28a0e9"), e > 60 && (t = "#2a9ce7"), e > 70 && (t = "#2b99e4"), e > 80 && (t = "#2b96e1"), e > 90 && (t = "#2c94e0")),
"DarkRed-LightRed" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#dc0000"), e > 20 && (t = "#e00000"), e > 30 && (t = "#e40000"), e > 40 && (t = "#ea0000"), e > 50 && (t = "#ee0000"), e > 60 && (t = "#f30000"), e > 70 && (t = "#f90000"), e > 80 && (t = "#fc0000"), e > 90 && (t = "#ff0000")),
"LightRed-DarkRed" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#fc0000"), e > 20 && (t = "#f90000"), e > 30 && (t = "#f30000"), e > 40 && (t = "#ee0000"), e > 50 && (t = "#ea0000"), e > 60 && (t = "#e40000"), e > 70 && (t = "#e00000"), e > 80 && (t = "#dc0000"), e > 90 && (t = "#d90000")),
"DarkGreen-LightGreen" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#33db00"), e > 20 && (t = "#34df00"), e > 30 && (t = "#34e200"), e > 40 && (t = "#36e700"), e > 50 && (t = "#37ec00"), e > 60 && (t = "#38f100"), e > 70 && (t = "#38f600"), e > 80 && (t = "#39f900"), e > 90 && (t = "#3afc00")),
"LightGreen-DarkGreen" === option.theme && (e > 0 && (t = "#3afc00"), e > 10 && (t = "#39f900"), e > 20 && (t = "#38f600"), e > 30 && (t = "#38f100"), e > 40 && (t = "#37ec00"), e > 50 && (t = "#36e700"), e > 60 && (t = "#34e200"), e > 70 && (t = "#34df00"), e > 80 && (t = "#33db00"), e > 90 && (t = "#32d900")),
"DarkGold-LightGold" === option.theme && (e > 0 && (t = "#ffb800"), e > 10 && (t = "#ffba00"), e > 20 && (t = "#ffbd00"), e > 30 && (t = "#ffc200"), e > 40 && (t = "#ffc600"), e > 50 && (t = "#ffcb00"), e > 60 && (t = "#ffcf00"), e > 70 && (t = "#ffd400"), e > 80 && (t = "#ffd600"), e > 90 && (t = "#ffd900")),
"LightGold-DarkGold" === option.theme && (e > 0 && (t = "#ffd900"), e > 10 && (t = "#ffd600"), e > 20 && (t = "#ffd400"), e > 30 && (t = "#ffcf00"), e > 40 && (t = "#ffcb00"), e > 50 && (t = "#ffc600"), e > 60 && (t = "#ffc200"), e > 70 && (t = "#ffbd00"), e > 80 && (t = "#ffba00"), e > 90 && (t = "#ffb800")),
"White" === option.theme && (t = "#fff"),
"Black" === option.theme && (t = "#000"),
t;
}
/* The label below gauge. */
function createLabel(t, a) {
if(t.children("b").length === 0){
$("<b></b>").appendTo(t).html(option.label).css({
"line-height": option.size + 5 * a + "px",
color: option.label_color
});
}
}
/* Prepend and append text, the gauge text or percentage value. */
function createSpanTag(t) {
var fgcolor = "";
if (option.animate_text_colors === true){
fgcolor = option.fgcolor;
}
var child = t.children("span");
if(child.length !== 0){
child.html(r).css({color: fgcolor});
return;
}
if(option.text_size <= 0.0 || Number.isNaN(option.text_size)){
option.text_size = 0.22;
}
if(option.text_size > 0.5){
option.text_size = 0.5;
}
$("<span></span>").appendTo(t).html(r).css({
"line-height": option.size + "px",
"font-size": option.text_size * option.size + "px",
color: fgcolor
});
}
/* Get data attributes as options from div tag. Fall back to defaults when not exists. */
function getDataAttr(t) {
$.each(dataAttr, function (index, element) {
if(t.data(element) !== undefined && t.data(element) !== null){
option[element] = t.data(element);
} else {
option[element] = $(defaults).attr(element);
}
if(element === "fill"){
s = option[element];
}
if((element === "size" ||
element === "width" ||
element === "animationstep" ||
element === "stripe"
) && !Number.isInteger(option[element])){
option[element] = parseInt(option[element]);
}
if(element === "text_size"){
option[element] = parseFloat(option[element]);
}
});
}
/* Draws the gauge. */
function drawGauge(a) {
if(M < 0) M = 0;
if(M > 100) M = 100;
var lw = option.width < 1 || isNaN(option.width) ? option.size / 20 : option.width;
g.clearRect(0, 0, b.width, b.height);
g.beginPath();
g.arc(m, v, x, G, k, !1);
if(s){
g.fillStyle = option.fill;
g.fill();
}
g.lineWidth = lw;
g.strokeStyle = option.back;
option.stripe > parseInt(0) ? g.setLineDash([option.stripe], 1) : g.lineCap = "round";
g.stroke();
g.beginPath();
g.arc(m, v, x, -I, P * a - I, !1);
g.lineWidth = lw;
g.strokeStyle = option.fgcolor;
g.stroke();
c > M && (M += z, requestAnimationFrame(function(){
drawGauge(Math.min(M, c) / 100);
}, p));
}
$(this).attr("data-id", $(this).attr("id"));
var r,
dataAttr = ["percent",
"used",
"min",
"total",
"size",
"prepend",
"append",
"theme",
"color",
"back",
"width",
"style",
"stripe",
"animationstep",
"animate_gauge_colors",
"animate_text_colors",
"label",
"label_color",
"text",
"text_size",
"fill",
"showvalue"],
option = {},
c = 0,
p = $(this),
s = false;
p.addClass("gaugeMeter");
getDataAttr(p);
if(Number.isInteger(option.used) && Number.isInteger(option.total)){
var u = option.used;
var t = option.total;
if(Number.isInteger(option.min)) {
if(option.min < 0) {
t -= option.min;
u -= option.min;
}
}
c = u / (t / 100);
} else {
if(Number.isInteger(option.percent)){
c = option.percent;
} else {
c = parseInt(defaults.percent);
}
}
if(c < 0) c = 0;
if(c > 100) c = 100;
if( option.text !== "" && option.text !== null && option.text !== undefined){
if(option.append !== "" && option.append !== null && option.append !== undefined){
r = option.text + "<u>" + option.append + "</u>";
} else {
r = option.text;
}
if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){
r = "<s>" + option.prepend + "</s>" + r;
}
} else {
if(defaults.showvalue === true || option.showvalue === true){
r = option.used;
} else {
r = c.toString();
}
if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){
r = "<s>" + option.prepend + "</s>" + r;
}
if(option.append !== "" && option.append !== null && option.append !== undefined){
r = r + "<u>" + option.append + "</u>";
}
}
option.fgcolor = getThemeColor(c);
if(option.color !== "" && option.color !== null && option.color !== undefined){
option.fgcolor = option.color;
}
if(option.animate_gauge_colors === true){
option.fgcolor = getThemeColor(c);
}
createSpanTag(p);
if(option.style !== "" && option.style !== null && option.style !== undefined){
createLabel(p, option.size / 13);
}
$(this).width(option.size + "px");
var b = $("<canvas></canvas>").attr({width: option.size, height: option.size}).get(0),
g = b.getContext("2d"),
m = b.width / 2,
v = b.height / 2,
_ = 360 * option.percent,
x = (_ * (Math.PI / 180), b.width / 2.5),
k = 2.3 * Math.PI,
G = 0,
M = 0 === option.animationstep ? c : 0,
z = Math.max(option.animationstep, 0),
P = 2 * Math.PI,
I = Math.PI / 2,
R = option.style;
var child = $(this).children("canvas");
if(child.length !== 0){
/* Replace existing canvas when new percentage was written. */
child.replaceWith(b);
} else {
/* Initially create canvas. */
$(b).appendTo($(this));
}
if ("Semi" === R){
k = 2 * Math.PI;
G = 3.13;
P = 1 * Math.PI;
I = Math.PI / .996;
}
if ("Arch" === R){
k = 2.195 * Math.PI;
G = 1, G = 655.99999;
P = 1.4 * Math.PI;
I = Math.PI / .8335;
}
drawGauge(M / 100);
});
};
}
(jQuery);

2
kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css

@ -0,0 +1,2 @@
/*! gridster.js - v0.8.0 - 2019-01-10 - * https://dsmorse.github.io/gridster.js/ - Copyright (c) 2019 ducksboard; Licensed MIT */
.gridster{position:relative}.gridster>*{-webkit-transition:height .4s,width .4s;-moz-transition:height .4s,width .4s;-o-transition:height .4s,width .4s;-ms-transition:height .4s,width .4s;transition:height .4s,width .4s}.gridster .gs-w{z-index:2;position:absolute}.gridster .preview-holder{z-index:1;position:absolute;background-color:#fff;border-color:#fff;opacity:.3}.gridster .player-revert{z-index:10!important;-webkit-transition:left .3s,top .3s!important;-moz-transition:left .3s,top .3s!important;-o-transition:left .3s,top .3s!important;transition:left .3s,top .3s!important}.gridster.collapsed{height:auto!important}.gridster.collapsed .gs-w{position:static!important}.ready .gs-w:not(.preview-holder),.ready .resize-preview-holder{-webkit-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-moz-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-o-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;transition:opacity .3s,left .3s,top .3s,width .3s,height .3s}.gridster .dragging,.gridster .resizing{z-index:10!important;-webkit-transition:all 0s!important;-moz-transition:all 0s!important;-o-transition:all 0s!important;transition:all 0s!important}.gs-resize-handle{position:absolute;z-index:1}.gs-resize-handle-both{width:20px;height:20px;bottom:-8px;right:-8px;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=);background-position:top left;background-repeat:no-repeat;cursor:se-resize;z-index:20}.gs-resize-handle-x{top:0;bottom:13px;right:-5px;width:10px;cursor:e-resize}.gs-resize-handle-y{left:0;right:13px;bottom:-5px;height:10px;cursor:s-resize}.gs-w:hover .gs-resize-handle,.resizing .gs-resize-handle{opacity:1}.gs-resize-handle,.gs-w.dragging .gs-resize-handle{opacity:0}.gs-resize-disabled .gs-resize-handle,[data-max-sizex="1"] .gs-resize-handle-x,[data-max-sizey="1"] .gs-resize-handle-y,[data-max-sizey="1"][data-max-sizex="1"] .gs-resize-handle{display:none!important}

2
kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js
File diff suppressed because it is too large
View File

76
kpi_dashboard/static/src/js/dashboard_controller.js

@ -0,0 +1,76 @@
odoo.define('kpi_dashboard.DashboardController', function (require) {
"use strict";
var BasicController = require('web.BasicController');
var core = require('web.core');
var qweb = core.qweb;
var _t = core._t;
var DashboardController = BasicController.extend({
custom_events: _.extend({}, BasicController.prototype.custom_events, {
addDashboard: '_addDashboard',
}),
renderPager: function ($node, options) {
options = _.extend({}, options, {
validate: this.canBeDiscarded.bind(this),
});
this._super($node, options);
},
_pushState: function (state) {
state = state || {};
var env = this.model.get(this.handle, {env: true});
state.id = env.currentId;
this._super(state);
},
_addDashboard: function () {
var self = this;
var action = self.initialState.specialData.action_id;
var name = self.initialState.specialData.name;
if (! action) {
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"));
}
});
},
_updateButtons: function () {
// HOOK Function
this.$buttons.on(
'click', '.o_dashboard_button_add',
this._addDashboard.bind(this));
},
renderButtons: function ($node) {
if (! $node) {
return;
}
this.$buttons = $('<div/>');
this.$buttons.append(qweb.render(
"kpi_dashboard.buttons", {widget: this}));
this._updateButtons();
this.$buttons.appendTo($node);
},
});
return DashboardController;
});

23
kpi_dashboard/static/src/js/dashboard_model.js

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

74
kpi_dashboard/static/src/js/dashboard_renderer.js

@ -0,0 +1,74 @@
odoo.define('kpi_dashboard.DashboardRenderer', function (require) {
"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 qweb = core.qweb;
var DashboardRenderer= BasicRenderer.extend({
className: "o_dashboard_view",
_getDashboardWidget: function (kpi) {
var Widget = registry.getAny([
kpi.widget, 'abstract',
]);
var widget = new Widget(this, kpi);
return widget;
},
_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;
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);
self.$grid.append(element);
self.kpi_widget[kpi.id] = self._getDashboardWidget(kpi);
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
);
return $.when();
},
_onNotification: function (notifications) {
var self = this;
_.each(notifications, function (notification) {
var channel = notification[0];
var message = notification[1];
if (channel === self.channel && message) {
var widget = self.kpi_widget[message.id];
if (widget !== undefined) {
widget._fillWidget(message);
}
}
});
},
});
return DashboardRenderer;
});

44
kpi_dashboard/static/src/js/dashboard_view.js

@ -0,0 +1,44 @@
odoo.define('kpi_dashboard.DashboardView', function (require) {
"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 _lt = core._lt;
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',
],
accesskey: "d",
display_name: _lt("Dashboard"),
icon: 'fa-tachometer',
viewType: 'dashboard',
config: _.extend({}, BasicView.prototype.config, {
Controller: DashboardController,
Renderer: DashboardRenderer,
Model: DashboardModel,
}),
multi_record: false,
searchable: false,
init: function () {
this._super.apply(this, arguments);
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;
}
},
});
view_registry.add('dashboard', DashboardView);
return DashboardView;
});

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

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

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

@ -0,0 +1,108 @@
odoo.define('kpi_dashboard.GraphWidget', function (require) {
"use strict";
var AbstractWidget = require('kpi_dashboard.AbstractWidget');
var registry = require('kpi_dashboard.widget_registry');
var core = require('web.core');
var qweb = core.qweb;
var GraphWidget = AbstractWidget.extend({
template: 'kpi_dashboard.graph',
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',
],
start: function () {
this._onResize = this._onResize.bind(this);
nv.utils.windowResize(this._onResize);
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
// actually loaded (i.e. after the widget has actually started),
// nv is undefined, but the handler isn't bound yet anyway
nv.utils.offWindowResize(this._onResize);
}
this._super.apply(this, arguments);
},
_getChartOptions: function (values) {
return {
x: function (d, u) { return u; },
margin: {'left': 0, 'right': 0, 'top': 5, 'bottom': 0},
showYAxis: false,
showXAxis: false,
showLegend: false,
height: this.widget_size_y - 90,
width: this.widget_size_x - 20,
};
},
_chartConfiguration: function (values) {
this.chart.forceY([0]);
this.chart.xAxis.tickFormat(function (d) {
var label = '';
_.each(values.value.graphs, function (v) {
if (v.values[d] && v.values[d].x) {
label = v.values[d].x;
}
});
return label;
});
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)
});
});
},
_addGraph: function (values) {
var data = values.value.graphs;
this.$svg.addClass('o_graph_linechart');
this.chart = nv.models.lineChart();
this.chart.options(
this._getChartOptions(values)
);
this._chartConfiguration(values);
d3.select(this.$('svg')[0])
.datum(data)
.transition().duration(600)
.call(this.chart);
this.$('svg').css('height', this.widget_size_y - 90);
this._customizeChart();
},
fillWidget: function (values) {
var self = this;
var element = this.$el.find('[data-bind="value"]');
element.empty();
element.css('padding-left', 10).css('padding-right', 10);
this.chart = null;
nv.addGraph(function () {
self.$svg = self.$el.find(
'[data-bind="value"]'
).append('<svg width=' + (self.widget_size_x - 20) + '>');
self._addGraph(values);
});
},
_customizeChart: function () {
// Hook function
},
_onResize: function () {
if (this.chart) {
this.chart.update();
this._customizeChart();
}
},
});
registry.add('graph', GraphWidget);
return GraphWidget;
});

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

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

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

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

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

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

7
kpi_dashboard/static/src/js/widget_registry.js

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

112
kpi_dashboard/static/src/scss/kpi_dashboard.scss

@ -0,0 +1,112 @@
.o_dashboard_view {
height: 100%;
@include o-webclient-padding($top: $o-horizontal-padding/2, $bottom: $o-horizontal-padding/2);
display: flex;
>.gridster {
margin: 0 auto;
>ul {
>li {
text-align: center;
list-style: none outside none;
}
}
}
.updated_at {
font-size: 15px;
position: absolute;
bottom: 0px;
left: 0;
right: 0;
}
.gs-w {
padding: 10px;
}
.centered {
position: absolute;
left: 0;
right: 0;
}
.numbervalue {
text-transform: uppercase;
font-size: 54px;
font-weight: 700;
}
.change-rate {
font-weight: 500;
font-size: 30px;
}
.hidden {
display: none;
}
.o_kpi_dashboard_toggle_button {
position: absolute;
right: 0px;
top: 0px;
margin: -1px -1px auto auto;
padding: 8px 16px;
border: 1px solid transparent;
border-bottom: none;
height: 35px;
}
.o_kpi_dashboard_manage_panel {
@include o-position-absolute($right: -1px, $top: 34px);
margin-top: -1px;
&.container {
width: 95%;
max-width: 400px;
}
.o_kpi_dashboard_manage_section {
border-bottom: 1px solid gray('300');
margin-bottom: 10px;
}
> div {
padding: 3px 0 3px 20px;
visibility: visible;
margin-bottom: 5px;
}
}
.o_dropdown_open {
.o_kpi_dashboard_manage_panel {
display: block;
}
.o_kpi_dashboard_toggle_button {
background: white;
border-color: gray('400');
z-index: $zindex-dropdown + 1;
}
}
.GaugeMeter {
position: relative;
text-align: center;
left: 0;
right: 0;
overflow: hidden;
cursor: default;
span, b{
margin: 0 23%;
width: 54%;
position: absolute;
text-align: center;
display: inline-block;
font-height: 100;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
[data-style="Semi"] B{
Margin: 0 10%;
Width: 80%;
}
S, U{
Text-Decoration:None;
font-height: 100;
}
B{
Color: Black;
Font-Weight: 200;
Font-Size: 0.85em;
Opacity: .8;
}
}
}

76
kpi_dashboard/static/src/xml/dashboard.xml

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" ?>
<template>
<t t-name="dashboard_kpi.dashboard">
<div class="gridster kpi_dashboard">
<ul/>
</div>
</t>
<t t-name="kpi_dashboard.kpi">
<li t-att-data-row="widget.row"
t-att-data-col="widget.col"
t-att-data-sizex="widget.sizex"
t-att-data-sizey="widget.sizey"
/>
</t>
<t t-name="kpi_dashboard.base_text">
<div class="kpi">
<h1 class="title" t-esc="widget.values.name"/>
</div>
</t>
<t t-name="kpi_dashboard.ManagePanel">
<t t-if="widget.actions" >
<t t-foreach="widget.actions" t-as="action">
<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>
</div>
</t>
</t>
</t>
<t t-name="kpi_dashboard.base_widget">
<div class="kpi">
<div class="o_kpi_dashboard_manage hidden">
<a class="o_kpi_dashboard_toggle_button" href="#">
<i class="fa fa-ellipsis-v" aria-label="Selection" role="img" title="Selection"/>
</a>
</div>
<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">
<t t-call="kpi_dashboard.ManagePanel"/>
</div>
</div>
</t>
<t t-name="kpi_dashboard.number" t-extend="kpi_dashboard.base_widget">
<t t-jquery="h1" t-operation="after">
<h2 class="numbervalue">
<span t-esc="widget.prefix"/><span data-bind="value"/><span t-esc="widget.suffix"/>
</h2>
<p class="change-rate">
<i class="fa" data-bind="arrow"/>
<span data-bind="difference"/>
</p>
</t>
</t>
<t t-name="kpi_dashboard.meter" t-extend="kpi_dashboard.base_widget">
<t t-jquery="h1" t-operation="after">
<div class="centered">
<div class="GaugeMeter" data-bind="value"/>
</div>
</t>
</t>
<t t-name="kpi_dashboard.graph" t-extend="kpi_dashboard.base_widget">
<t t-jquery="h1" t-operation="after">
<div class="centered">
<div data-bind="value"/>
</div>
</t>
</t>
<t t-name="kpi_dashboard.buttons">
<div class="o_dashboard_buttons" role="toolbar" aria-label="Main actions">
<button type="button"
class="btn btn-primary o_dashboard_button_add" accesskey="d">
Add to Dashboard
</button>
</div>
</t>
</template>

111
kpi_dashboard/views/kpi_dashboard.xml

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="kpi_dashboard_form_view">
<field name="name">kpi.dashboard.form (in kpi_dashboard)</field>
<field name="model">kpi.dashboard</field>
<field name="arch" type="xml">
<form>
<header/>
<sheet>
<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)]}"
/>
</div>
<group>
<field name="name"/>
<field name="menu_id" attrs="{'invisible': [('menu_id', '=', False)]}"/>
</group>
<notebook>
<page name="item" string="KPIs">
<field name="item_ids">
<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"/>
</tree>
</field>
</page>
<page name="widget" string="Widget configuration">
<group>
<group name="margin">
<field name="margin_x"/>
<field name="margin_y"/>
</group>
<group name="dimension">
<field name="widget_dimension_x"/>
<field name="widget_dimension_y"/>
<field name="number_of_columns"/>
<field name="width"/>
</group>
<group name="color">
<field name="background_color" widget="color"/>
</group>
</group>
</page>
<page name="group" string="Groups">
<field name="group_ids"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="kpi_dashboard_search_view">
<field name="name">kpi.dashboard.search (in kpi_dashboard)</field>
<field name="model">kpi.dashboard</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="kpi_dashboard_tree_view">
<field name="name">kpi.dashboard.tree (in kpi_dashboard)</field>
<field name="model">kpi.dashboard</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="kpi_dashboard_dashboard_view">
<field name="name">kpi.dashboard.dashboard (in kpi_dashboard)</field>
<field name="model">kpi.dashboard</field>
<field name="arch" type="xml">
<dashboard/>
</field>
</record>
<record model="ir.actions.act_window" id="kpi_dashboard_act_window">
<field name="name">Kpi Dashboard</field> <!-- TODO -->
<field name="res_model">kpi.dashboard</field>
<field name="view_mode">tree,form,dashboard</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="kpi_dashboard_menu">
<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 -->
</record>
</odoo>

89
kpi_dashboard/views/kpi_kpi.xml

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="kpi_kpi_form_view">
<field name="name">kpi.kpi.form (in kpi_dashboard)</field>
<field name="model">kpi.kpi</field>
<field name="arch" type="xml">
<form>
<header>
<button name="generate_cron" string="Generate cron" type="object"
attrs="{'invisible': [('cron_id', '!=',False)]}"/>
<button name="compute" string="Compute now" type="object"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box"/>
<h2>
<field name="name"/>
</h2>
<group>
<group>
<field name="computation_method"/>
<field name="widget"/>
<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>
<field name="cron_id" attrs="{'invisible': [('cron_id', '=',False)]}" readonly="True"/>
</group>
<group>
<field name="suffix"/>
<field name="prefix"/>
</group>
</group>
<notebook>
<page name="action" string="Actions">
<field name="action_ids">
<tree editable="bottom">
<field name="action"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="kpi_kpi_search_view">
<field name="name">kpi.kpi.search (in kpi_dashboard)</field>
<field name="model">kpi.kpi</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="kpi_kpi_tree_view">
<field name="name">kpi.kpi.tree (in kpi_dashboard)</field>
<field name="model">kpi.kpi</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="kpi_kpi_act_window">
<field name="name">Kpi</field>
<field name="res_model">kpi.kpi</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="kpi_kpi_menu">
<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"/>
</record>
</odoo>

12
kpi_dashboard/views/kpi_menu.xml

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

25
kpi_dashboard/views/webclient_templates.xml

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend"
name="Backend Assets (used in backend interface)"
inherit_id="web.assets_backend">
<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/number_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"/>
<link rel="stylesheet" type="text/scss" href="/kpi_dashboard/static/src/scss/kpi_dashboard.scss"/>
</xpath>
</template>
</odoo>

1
kpi_dashboard/wizards/__init__.py

@ -0,0 +1 @@
from . import kpi_dashboard_menu

17
kpi_dashboard/wizards/kpi_dashboard_menu.py

@ -0,0 +1,17 @@
# Copyright 2020 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class KpiDashboardMenu(models.TransientModel):
_name = "kpi.dashboard.menu"
_description = "Create a Menu for a Dashboard"
dashboard_id = fields.Many2one("kpi.dashboard", required=True)
menu_id = fields.Many2one("ir.ui.menu")
@api.multi
def generate_menu(self):
self.dashboard_id._generate_menu(self.menu_id)

38
kpi_dashboard/wizards/kpi_dashboard_menu.xml

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