OCA-git-bot
4 years ago
49 changed files with 20136 additions and 0 deletions
-
118kpi_dashboard/README.rst
-
2kpi_dashboard/__init__.py
-
25kpi_dashboard/__manifest__.py
-
201kpi_dashboard/demo/demo_dashboard.xml
-
724kpi_dashboard/i18n/kpi_dashboard.pot
-
4kpi_dashboard/models/__init__.py
-
12kpi_dashboard/models/ir_actions_act_window_view.py
-
12kpi_dashboard/models/ir_ui_view.py
-
238kpi_dashboard/models/kpi_dashboard.py
-
260kpi_dashboard/models/kpi_kpi.py
-
33kpi_dashboard/readme/CONFIGURE.rst
-
1kpi_dashboard/readme/CONTRIBUTORS.rst
-
1kpi_dashboard/readme/DESCRIPTION.rst
-
12kpi_dashboard/security/ir.model.access.csv
-
25kpi_dashboard/security/security.xml
-
BINkpi_dashboard/static/description/icon.png
-
466kpi_dashboard/static/description/index.html
-
276kpi_dashboard/static/lib/gauge/GaugeMeter.js
-
2kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css
-
2kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js
-
9503kpi_dashboard/static/lib/nvd3/d3.v3.js
-
427kpi_dashboard/static/lib/nvd3/nv.d3.css
-
5985kpi_dashboard/static/lib/nvd3/nv.d3.js
-
164kpi_dashboard/static/src/js/dashboard_controller.js
-
21kpi_dashboard/static/src/js/dashboard_model.js
-
108kpi_dashboard/static/src/js/dashboard_renderer.js
-
40kpi_dashboard/static/src/js/dashboard_view.js
-
61kpi_dashboard/static/src/js/field_widget.js
-
50kpi_dashboard/static/src/js/lib/nvd3.js
-
101kpi_dashboard/static/src/js/widget/abstract_widget.js
-
13kpi_dashboard/static/src/js/widget/counter_widget.js
-
108kpi_dashboard/static/src/js/widget/graph_widget.js
-
75kpi_dashboard/static/src/js/widget/integer_widget.js
-
34kpi_dashboard/static/src/js/widget/meter_widget.js
-
22kpi_dashboard/static/src/js/widget/number_widget.js
-
16kpi_dashboard/static/src/js/widget/text_widget.js
-
7kpi_dashboard/static/src/js/widget_registry.js
-
116kpi_dashboard/static/src/scss/kpi_dashboard.scss
-
97kpi_dashboard/static/src/xml/dashboard.xml
-
68kpi_dashboard/templates/assets.xml
-
2kpi_dashboard/tests/__init__.py
-
77kpi_dashboard/tests/test_formula.py
-
167kpi_dashboard/tests/test_kpi_dashboard.py
-
179kpi_dashboard/views/kpi_dashboard.xml
-
218kpi_dashboard/views/kpi_kpi.xml
-
13kpi_dashboard/views/kpi_menu.xml
-
1kpi_dashboard/wizards/__init__.py
-
16kpi_dashboard/wizards/kpi_dashboard_menu.py
-
33kpi_dashboard/wizards/kpi_dashboard_menu.xml
@ -0,0 +1,118 @@ |
|||||
|
============= |
||||
|
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` |
||||
|
|
||||
|
#. In order to compute the KPI you can use a predefined function from a model or |
||||
|
use the code to directly compute it. |
||||
|
|
||||
|
Using KPI with code |
||||
|
~~~~~~~~~~~~~~~~~~~ |
||||
|
|
||||
|
Define the code directly on the code field. You can use `self` and `model` as the kpi element |
||||
|
The script should create a variable called `result` as a dictionary that |
||||
|
will be stored as the value. |
||||
|
For example, we can use:: |
||||
|
|
||||
|
result = {} |
||||
|
result['value'] = len(model.search([('id', '=', %s)])) |
||||
|
result['previous'] = len(model.search([('id', '!=', %s)])) |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
.. |maintainer-etobella| image:: https://github.com/etobella.png?size=40px |
||||
|
:target: https://github.com/etobella |
||||
|
:alt: etobella |
||||
|
|
||||
|
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__: |
||||
|
|
||||
|
|maintainer-etobella| |
||||
|
|
||||
|
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. |
@ -0,0 +1,2 @@ |
|||||
|
from . import models |
||||
|
from . import wizards |
@ -0,0 +1,25 @@ |
|||||
|
# 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": "14.0.1.0.0", |
||||
|
"license": "AGPL-3", |
||||
|
"author": "Creu Blanca,Odoo Community Association (OCA)", |
||||
|
"website": "https://github.com/OCA/reporting-engine", |
||||
|
"depends": ["bus", "board", "base_sparse_field"], |
||||
|
"qweb": ["static/src/xml/dashboard.xml"], |
||||
|
"data": [ |
||||
|
"wizards/kpi_dashboard_menu.xml", |
||||
|
"security/security.xml", |
||||
|
"security/ir.model.access.csv", |
||||
|
"templates/assets.xml", |
||||
|
"views/kpi_menu.xml", |
||||
|
"views/kpi_kpi.xml", |
||||
|
"views/kpi_dashboard.xml", |
||||
|
], |
||||
|
"demo": ["demo/demo_dashboard.xml"], |
||||
|
"maintainers": ["etobella"], |
||||
|
} |
@ -0,0 +1,201 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8" ?> |
||||
|
<odoo> |
||||
|
<record id="demo_dashboard" model="kpi.dashboard"> |
||||
|
<field name="name">Dashboard</field> |
||||
|
<field name="number_of_columns">4</field> |
||||
|
<field name="widget_dimension_y">50</field> |
||||
|
<field name="widget_dimension_x">250</field> |
||||
|
<field name="background_color">#020202</field> |
||||
|
<field name="compute_on_fly_refresh">30</field> |
||||
|
</record> |
||||
|
<record id="widget_number_01" model="kpi.kpi"> |
||||
|
<field name="name">Number 01</field> |
||||
|
<field name="prefix">$</field> |
||||
|
<field name="computation_method">code</field> |
||||
|
<field name="widget">number</field> |
||||
|
<field name="code">result = {"value": 10000,"previous": 12000}</field> |
||||
|
</record> |
||||
|
<record id="widget_number_02" model="kpi.kpi"> |
||||
|
<field name="name">Number 02</field> |
||||
|
<field name="suffix">€</field> |
||||
|
<field name="computation_method">code</field> |
||||
|
<field name="widget">number</field> |
||||
|
<field name="code">result = {"value": 12000,"previous": 10000}</field> |
||||
|
</record> |
||||
|
<function |
||||
|
model="kpi.kpi" |
||||
|
name="compute" |
||||
|
eval="[[ref('widget_number_01'), ref('widget_number_02')]]" |
||||
|
/> |
||||
|
<record id="widget_meter_01" model="kpi.kpi"> |
||||
|
<field name="name">Meter 01</field> |
||||
|
<field name="suffix">€</field> |
||||
|
<field name="computation_method">code</field> |
||||
|
<field name="widget">meter</field> |
||||
|
<field name="code">result = {"min": 0, "max": 100, "value": 90}</field> |
||||
|
</record> |
||||
|
<record id="widget_meter_02" model="kpi.kpi"> |
||||
|
<field name="name">Meter 02</field> |
||||
|
<field name="prefix">$</field> |
||||
|
<field name="computation_method">code</field> |
||||
|
<field name="widget">meter</field> |
||||
|
<field name="code">result = {"min": 0, "max": 100, "value": 40}</field> |
||||
|
</record> |
||||
|
<function |
||||
|
model="kpi.kpi" |
||||
|
name="compute" |
||||
|
eval="[[ref('widget_meter_01'), ref('widget_meter_02')]]" |
||||
|
/> |
||||
|
<record id="widget_graph" model="kpi.kpi"> |
||||
|
<field name="name">Graph</field> |
||||
|
<field name="computation_method">code</field> |
||||
|
<field name="widget">graph</field> |
||||
|
<field |
||||
|
name="code" |
||||
|
><![CDATA[ |
||||
|
result = {"graphs": [ |
||||
|
{ |
||||
|
"values": [ |
||||
|
{"x": i, "y": i * 1000} |
||||
|
for i in range(1, 12) |
||||
|
], |
||||
|
"title": "Current Year", |
||||
|
"key": "current", |
||||
|
"area": True, |
||||
|
"color": "ffffff", |
||||
|
}, |
||||
|
{ |
||||
|
"values": [ |
||||
|
{"x": i, "y": 1000 * (12-i)} |
||||
|
for i in range(1, 12) |
||||
|
], |
||||
|
"title": "Previous Year", |
||||
|
"key": "previous", |
||||
|
"area": True, |
||||
|
"color": "000000", |
||||
|
}, |
||||
|
]} |
||||
|
]]></field> |
||||
|
</record> |
||||
|
<function model="kpi.kpi" name="compute" eval="[[ref('widget_graph')]]" /> |
||||
|
<record id="widget_integer" model="kpi.kpi"> |
||||
|
<field name="name">Integer counter</field> |
||||
|
<field name="computation_method">code</field> |
||||
|
<field name="widget">integer</field> |
||||
|
<field name="compute_on_fly" eval="True" /> |
||||
|
<field |
||||
|
name="code" |
||||
|
><![CDATA[ |
||||
|
result = {"value": self.env.context.get('counter', 990)} |
||||
|
]]></field> |
||||
|
</record> |
||||
|
<record id="widget_counter" model="kpi.kpi"> |
||||
|
<field name="name">Counter</field> |
||||
|
<field name="computation_method">code</field> |
||||
|
<field name="widget">counter</field> |
||||
|
<field name="compute_on_fly" eval="True" /> |
||||
|
<field |
||||
|
name="code" |
||||
|
><![CDATA[ |
||||
|
result = {"value": self.env.context.get('counter', 990)} |
||||
|
]]></field> |
||||
|
</record> |
||||
|
<record id="dashboard_widget_text" model="kpi.dashboard.item"> |
||||
|
<field name="name">Dashboard title</field> |
||||
|
<field name="dashboard_id" ref="demo_dashboard" /> |
||||
|
<field name="column">1</field> |
||||
|
<field name="row">1</field> |
||||
|
<field name="size_x">4</field> |
||||
|
<field name="color">#707070</field> |
||||
|
<field name="font_color">#000000</field> |
||||
|
</record> |
||||
|
<record id="dashboard_widget_number_01" model="kpi.dashboard.item"> |
||||
|
<field name="name">Number 01</field> |
||||
|
<field name="dashboard_id" ref="demo_dashboard" /> |
||||
|
<field name="kpi_id" ref="widget_number_01" /> |
||||
|
<field name="column">1</field> |
||||
|
<field name="row">2</field> |
||||
|
<field name="size_y">4</field> |
||||
|
<field name="color">#47bbb3</field> |
||||
|
<field name="font_color">#ffffff</field> |
||||
|
</record> |
||||
|
<record id="dashboard_widget_number_02" model="kpi.dashboard.item"> |
||||
|
<field name="name">Number 02</field> |
||||
|
<field name="dashboard_id" ref="demo_dashboard" /> |
||||
|
<field name="kpi_id" ref="widget_number_02" /> |
||||
|
<field name="column">1</field> |
||||
|
<field name="row">6</field> |
||||
|
<field name="size_y">4</field> |
||||
|
<field name="color">#ec663c</field> |
||||
|
<field name="font_color">#ffffff</field> |
||||
|
</record> |
||||
|
<record id="dashboard_widget_meter_01" model="kpi.dashboard.item"> |
||||
|
<field name="name">Meter 01</field> |
||||
|
<field name="dashboard_id" ref="demo_dashboard" /> |
||||
|
<field name="kpi_id" ref="widget_meter_01" /> |
||||
|
<field name="column">2</field> |
||||
|
<field name="row">2</field> |
||||
|
<field name="size_y">4</field> |
||||
|
<field name="color">#9c4274</field> |
||||
|
<field name="font_color">#ffffff</field> |
||||
|
</record> |
||||
|
<record id="dashboard_widget_meter_02" model="kpi.dashboard.item"> |
||||
|
<field name="name">Meter 02</field> |
||||
|
<field name="dashboard_id" ref="demo_dashboard" /> |
||||
|
<field name="kpi_id" ref="widget_meter_02" /> |
||||
|
<field name="column">2</field> |
||||
|
<field name="row">6</field> |
||||
|
<field name="size_y">4</field> |
||||
|
<field name="color">#12b0c5</field> |
||||
|
<field name="font_color">#ffffff</field> |
||||
|
</record> |
||||
|
<record id="dashboard_widget_add_counter" model="kpi.dashboard.item"> |
||||
|
<field name="name">+1 to Counter</field> |
||||
|
<field name="dashboard_id" ref="demo_dashboard" /> |
||||
|
<field name="column">3</field> |
||||
|
<field name="row">10</field> |
||||
|
<field name="size_y">1</field> |
||||
|
<field name="size_x">2</field> |
||||
|
<field name="color">#B41F1F</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> |
||||
|
</record> |
||||
|
<record id="dashboard_widget_counter" model="kpi.dashboard.item"> |
||||
|
<field name="name">Counter</field> |
||||
|
<field name="dashboard_id" ref="demo_dashboard" /> |
||||
|
<field name="kpi_id" ref="widget_counter" /> |
||||
|
<field name="column">3</field> |
||||
|
<field name="row">11</field> |
||||
|
<field name="size_y">3</field> |
||||
|
<field name="color">#4B0082</field> |
||||
|
<field name="font_color">#ffffff</field> |
||||
|
</record> |
||||
|
<record id="dashboard_widget_integer" model="kpi.dashboard.item"> |
||||
|
<field name="name">Integer</field> |
||||
|
<field name="dashboard_id" ref="demo_dashboard" /> |
||||
|
<field name="kpi_id" ref="widget_integer" /> |
||||
|
<field name="column">4</field> |
||||
|
<field name="row">11</field> |
||||
|
<field name="size_y">3</field> |
||||
|
<field name="color">#ffffff</field> |
||||
|
<field name="font_color">#4B0082</field> |
||||
|
</record> |
||||
|
<record id="dashboard_widget_graph" model="kpi.dashboard.item"> |
||||
|
<field name="name">Graph</field> |
||||
|
<field name="dashboard_id" ref="demo_dashboard" /> |
||||
|
<field name="kpi_id" ref="widget_graph" /> |
||||
|
<field name="column">3</field> |
||||
|
<field name="row">2</field> |
||||
|
<field name="size_x">2</field> |
||||
|
<field name="size_y">8</field> |
||||
|
<field name="color">#ff9618</field> |
||||
|
<field name="font_color">#ffffff</field> |
||||
|
</record> |
||||
|
</odoo> |
@ -0,0 +1,724 @@ |
|||||
|
# Translation of Odoo Server. |
||||
|
# This file contains the translation of the following modules: |
||||
|
# * kpi_dashboard |
||||
|
# |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: Odoo Server 12.0\n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"Last-Translator: <>\n" |
||||
|
"Language-Team: \n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: \n" |
||||
|
"Plural-Forms: \n" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: code:addons/kpi_dashboard/models/kpi_kpi.py:153 |
||||
|
#, python-format |
||||
|
msgid " or " |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#. openerp-web |
||||
|
#: code:addons/kpi_dashboard/static/src/js/dashboard_controller.js:72 |
||||
|
#, python-format |
||||
|
msgid "'%s' added to dashboard" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__action_ids |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action__action |
||||
|
msgid "Action" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model,name:kpi_dashboard.model_ir_actions_act_window_view |
||||
|
msgid "Action Window View" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_kpi_form_view |
||||
|
msgid "Actions" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,help:kpi_dashboard.field_kpi_kpi__action_ids |
||||
|
msgid "Actions that can be opened from the KPI" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__active |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__active |
||||
|
msgid "Active" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.actions.act_window.view,view_mode:0 |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "Activity" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#. openerp-web |
||||
|
#: code:addons/kpi_dashboard/static/src/xml/dashboard.xml:72 |
||||
|
#, python-format |
||||
|
msgid "Add to Dashboard" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi,widget:0 |
||||
|
msgid "Altair" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__args |
||||
|
msgid "Args" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__background_color |
||||
|
msgid "Background Color" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi,widget:0 |
||||
|
msgid "Bokeh" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.actions.act_window.view,view_mode:0 |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "Calendar" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_dashboard_item_config_form_view |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_dashboard_menu_form_view |
||||
|
msgid "Cancel" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__code |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_kpi_form_view |
||||
|
#: selection:kpi.kpi,computation_method:0 |
||||
|
msgid "Code" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__color |
||||
|
msgid "Color" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__column |
||||
|
msgid "Column" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__computation_method |
||||
|
msgid "Computation Method" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__compute_on_fly |
||||
|
msgid "Compute On Fly" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__compute_on_fly_refresh |
||||
|
msgid "Compute On Fly Refresh" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_kpi_form_view |
||||
|
msgid "Compute now" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__computed_date |
||||
|
msgid "Computed Date" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__computed_value |
||||
|
msgid "Computed Value" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.ui.menu,name:kpi_dashboard.kpi_dashboard_menu |
||||
|
msgid "Configure Dashboard" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.ui.menu,name:kpi_dashboard.kpi_kpi_menu |
||||
|
msgid "Configure Kpi" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action__context |
||||
|
msgid "Context" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#. openerp-web |
||||
|
#: code:addons/kpi_dashboard/static/src/js/dashboard_controller.js:76 |
||||
|
#, python-format |
||||
|
msgid "Could not add KPI dashboard to dashboard" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi,widget:0 |
||||
|
msgid "Counter" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model,name:kpi_dashboard.model_kpi_dashboard_menu |
||||
|
msgid "Create a Menu for a Dashboard" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__create_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__create_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_menu__create_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__create_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action__create_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__create_uid |
||||
|
msgid "Created by" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__create_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__create_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_menu__create_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__create_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action__create_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__create_date |
||||
|
msgid "Created on" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__cron_id |
||||
|
msgid "Cron" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#. openerp-web |
||||
|
#: code:addons/kpi_dashboard/static/src/js/dashboard_view.js:21 |
||||
|
#: selection:ir.actions.act_window.view,view_mode:0 |
||||
|
#: model:ir.model,name:kpi_dashboard.model_kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__dashboard_id |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_menu__dashboard_id |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
#, python-format |
||||
|
msgid "Dashboard" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__dashboard_item_ids |
||||
|
msgid "Dashboard Item" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model,name:kpi_dashboard.model_kpi_dashboard_item |
||||
|
msgid "Dashboard Items" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "Diagram" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__display_name |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__display_name |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_menu__display_name |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__display_name |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action__display_name |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__display_name |
||||
|
msgid "Display Name" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__end_column |
||||
|
msgid "End Column" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__end_row |
||||
|
msgid "End Row" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_kpi_form_view |
||||
|
msgid "Enter Python code here." |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#. openerp-web |
||||
|
#: code:addons/kpi_dashboard/static/src/js/dashboard_controller.js:57 |
||||
|
#, python-format |
||||
|
msgid "First you must create the Menu" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__font_color |
||||
|
msgid "Font Color" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.actions.act_window.view,view_mode:0 |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "Form" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__function |
||||
|
#: selection:kpi.kpi,computation_method:0 |
||||
|
msgid "Function" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.actions.act_window.view,view_mode:0 |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "Gantt" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_dashboard_menu_form_view |
||||
|
msgid "Generate" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_dashboard_menu_form_view |
||||
|
msgid "Generate Menu" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_kpi_form_view |
||||
|
msgid "Generate cron" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_dashboard_form_view |
||||
|
msgid "Generate menu" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#. openerp-web |
||||
|
#: code:addons/kpi_dashboard/static/src/xml/dashboard.xml:25 |
||||
|
#, python-format |
||||
|
msgid "Go to" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.actions.act_window.view,view_mode:0 |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
#: selection:kpi.kpi,widget:0 |
||||
|
msgid "Graph" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__group_ids |
||||
|
msgid "Group" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_dashboard_form_view |
||||
|
msgid "Groups" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__history_ids |
||||
|
msgid "History" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__id |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__id |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_menu__id |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__id |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action__id |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__id |
||||
|
msgid "ID" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi,widget:0 |
||||
|
msgid "Integer" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__item_ids |
||||
|
msgid "Item" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.ui.menu,name:kpi_dashboard.menu_configuration_kpi_dashboards |
||||
|
msgid "KPI Dashboards" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model,name:kpi_dashboard.model_kpi_kpi_action |
||||
|
msgid "KPI action" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model,name:kpi_dashboard.model_kpi_kpi_history |
||||
|
msgid "KPI history" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_dashboard_form_view |
||||
|
msgid "KPIs" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.actions.act_window.view,view_mode:0 |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "Kanban" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.actions.act_window,name:kpi_dashboard.kpi_kpi_act_window |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__kpi_id |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action__kpi_id |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__kpi_id |
||||
|
msgid "Kpi" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.actions.act_window,name:kpi_dashboard.kpi_dashboard_act_window |
||||
|
msgid "Kpi Dashboard" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.actions.act_window,name:kpi_dashboard.kpi_dashboard_menu_act_window |
||||
|
msgid "Kpi Dashboard Menu" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.actions.act_window,name:kpi_dashboard.kpi_kpi_history_act_window |
||||
|
msgid "Kpi History" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model,name:kpi_dashboard.model_kpi_kpi |
||||
|
msgid "Kpi Kpi" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__kwargs |
||||
|
msgid "Kwargs" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard____last_update |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item____last_update |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_menu____last_update |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi____last_update |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action____last_update |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history____last_update |
||||
|
msgid "Last Modified on" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__write_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__write_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_menu__write_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__write_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action__write_uid |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__write_uid |
||||
|
msgid "Last Updated by" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__write_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__write_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_menu__write_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__write_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_action__write_date |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__write_date |
||||
|
msgid "Last Updated on" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#. openerp-web |
||||
|
#: code:addons/kpi_dashboard/static/src/xml/dashboard.xml:70 |
||||
|
#, python-format |
||||
|
msgid "Main actions" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:res.groups,name:kpi_dashboard.group_kpi_dashboard_manager |
||||
|
msgid "Manage KPI Dashboards" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__margin_x |
||||
|
msgid "Margin X" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__margin_y |
||||
|
msgid "Margin Y" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__menu_id |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_menu__menu_id |
||||
|
msgid "Menu" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi,widget:0 |
||||
|
msgid "Meter" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__model_id |
||||
|
msgid "Model" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__modify_color |
||||
|
msgid "Modify Color" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__modify_color_expression |
||||
|
msgid "Modify Color Expression" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__modify_context |
||||
|
msgid "Modify Context" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__modify_context_expression |
||||
|
msgid "Modify Context Expression" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__name |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__name |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__name |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__name |
||||
|
msgid "Name" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi,widget:0 |
||||
|
msgid "Number" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__number_of_columns |
||||
|
msgid "Number Of Columns" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.actions.act_window.view,view_mode:0 |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "Pivot" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#. openerp-web |
||||
|
#: code:addons/kpi_dashboard/static/src/js/dashboard_controller.js:73 |
||||
|
#, python-format |
||||
|
msgid "Please refresh your browser for the changes to take effect." |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__prefix |
||||
|
msgid "Prefix" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "QWeb" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__raw_value |
||||
|
msgid "Raw Value" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_kpi_history_tree_view |
||||
|
msgid "Raw value" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__row |
||||
|
msgid "Row" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_dashboard_item_config_form_view |
||||
|
msgid "Save" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "Search" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,help:kpi_dashboard.field_kpi_dashboard__compute_on_fly_refresh |
||||
|
msgid "Seconds to refresh on fly elements" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#. openerp-web |
||||
|
#: code:addons/kpi_dashboard/static/src/xml/dashboard.xml:34 |
||||
|
#, python-format |
||||
|
msgid "Selection" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_kpi_form_view |
||||
|
msgid "Show history" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_kpi_form_view |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_kpi_history_tree_view |
||||
|
msgid "Show value" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__size_x |
||||
|
msgid "Size X" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard_item__size_y |
||||
|
msgid "Size Y" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: code:addons/kpi_dashboard/models/kpi_dashboard.py:139 |
||||
|
#, python-format |
||||
|
msgid "Size Y of the widget cannot be bigger than 10" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__store_history |
||||
|
msgid "Store History" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__store_history_interval |
||||
|
msgid "Store History Interval" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__store_history_interval_number |
||||
|
msgid "Store History Interval Number" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__suffix |
||||
|
msgid "Suffix" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: code:addons/kpi_dashboard/models/kpi_kpi.py:155 |
||||
|
#, python-format |
||||
|
msgid "The code cannot contain the following terms: %s." |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:ir.actions.act_window.view,view_mode:0 |
||||
|
#: selection:ir.ui.view,type:0 |
||||
|
msgid "Tree" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__value |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__value |
||||
|
msgid "Value" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__value_last_update |
||||
|
msgid "Value Last Update" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model,name:kpi_dashboard.model_ir_ui_view |
||||
|
msgid "View" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_ir_actions_act_window_view__view_mode |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_ir_ui_view__type |
||||
|
msgid "View Type" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi__widget |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_kpi_history__widget |
||||
|
msgid "Widget" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: code:addons/kpi_dashboard/models/kpi_dashboard.py:160 |
||||
|
#, python-format |
||||
|
msgid "Widget %s is bigger than expected" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__widget_dimension_x |
||||
|
msgid "Widget Dimension X" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__widget_dimension_y |
||||
|
msgid "Widget Dimension Y" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model_terms:ir.ui.view,arch_db:kpi_dashboard.kpi_dashboard_form_view |
||||
|
msgid "Widget configuration" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: code:addons/kpi_dashboard/models/kpi_dashboard.py:156 |
||||
|
#, python-format |
||||
|
msgid "Widgets cannot be crossed by other widgets" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: model:ir.model.fields,field_description:kpi_dashboard.field_kpi_dashboard__width |
||||
|
msgid "Width" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi.action,action:0 |
||||
|
msgid "ir.actions.act_url" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi.action,action:0 |
||||
|
msgid "ir.actions.act_window" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi.action,action:0 |
||||
|
msgid "ir.actions.client" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi.action,action:0 |
||||
|
msgid "ir.actions.report" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: kpi_dashboard |
||||
|
#: selection:kpi.kpi.action,action:0 |
||||
|
msgid "ir.actions.server" |
||||
|
msgstr "" |
||||
|
|
@ -0,0 +1,4 @@ |
|||||
|
from . import kpi_dashboard |
||||
|
from . import kpi_kpi |
||||
|
from . import ir_actions_act_window_view |
||||
|
from . import ir_ui_view |
@ -0,0 +1,12 @@ |
|||||
|
# 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")], ondelete={"dashboard": "cascade"} |
||||
|
) |
@ -0,0 +1,12 @@ |
|||||
|
# 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")], ondelete={"dashboard": "cascade"} |
||||
|
) |
@ -0,0 +1,238 @@ |
|||||
|
# 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) |
||||
|
compute_on_fly_refresh = fields.Integer( |
||||
|
default=0, help="Seconds to refresh on fly elements" |
||||
|
) |
||||
|
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_on_fly(self): |
||||
|
self.ensure_one() |
||||
|
result = [] |
||||
|
for item in self.item_ids: |
||||
|
if not item.kpi_id.compute_on_fly: |
||||
|
continue |
||||
|
result.append(item._read_dashboard()) |
||||
|
return result |
||||
|
|
||||
|
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, |
||||
|
"compute_on_fly_refresh": self.compute_on_fly_refresh, |
||||
|
"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": "{},{}".format(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, ondelete="cascade") |
||||
|
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() |
||||
|
modify_context = fields.Boolean() |
||||
|
modify_context_expression = fields.Char() |
||||
|
modify_color = fields.Boolean() |
||||
|
modify_color_expression = 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", |
||||
|
"modify_context": self.modify_context, |
||||
|
"modify_color": self.modify_color, |
||||
|
} |
||||
|
if self.modify_context: |
||||
|
vals["modify_context_expression"] = self.modify_context_expression |
||||
|
if self.modify_color: |
||||
|
vals["modify_color_expression"] = self.modify_color_expression |
||||
|
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 "", |
||||
|
"compute_on_fly": 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(), |
||||
|
} |
||||
|
) |
||||
|
else: |
||||
|
vals.update( |
||||
|
{ |
||||
|
"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 |
||||
|
|
||||
|
def technical_config(self): |
||||
|
self.ensure_one() |
||||
|
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, |
||||
|
} |
||||
|
|
||||
|
def store_data(self): |
||||
|
return {"type": "ir.actions.act_window_close"} |
@ -0,0 +1,260 @@ |
|||||
|
# Copyright 2020 Creu Blanca |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
import ast |
||||
|
import json |
||||
|
import re |
||||
|
from datetime import date, datetime, time |
||||
|
|
||||
|
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): |
||||
|
_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"), ("code", "Code")], |
||||
|
required=True, |
||||
|
default="code", |
||||
|
) |
||||
|
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( |
||||
|
[ |
||||
|
("integer", "Integer"), |
||||
|
("number", "Number"), |
||||
|
("meter", "Meter"), |
||||
|
("counter", "Counter"), |
||||
|
("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", |
||||
|
) |
||||
|
code = fields.Text("Code") |
||||
|
store_history = fields.Boolean() |
||||
|
store_history_interval = fields.Selection( |
||||
|
selection=lambda self: self.env["ir.cron"]._fields["interval_type"].selection, |
||||
|
) |
||||
|
store_history_interval_number = fields.Integer() |
||||
|
compute_on_fly = fields.Boolean() |
||||
|
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") |
||||
|
|
||||
|
@api.depends("value", "value_last_update", "compute_on_fly") |
||||
|
def _compute_computed_value(self): |
||||
|
for record in self: |
||||
|
if record.compute_on_fly: |
||||
|
record.computed_value = record._compute_value() |
||||
|
record.computed_date = fields.Datetime.now() |
||||
|
else: |
||||
|
record.computed_value = record.value |
||||
|
record.computed_date = record.value_last_update |
||||
|
|
||||
|
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 _generate_history_vals(self, value): |
||||
|
return { |
||||
|
"kpi_id": self.id, |
||||
|
"value": value, |
||||
|
"widget": self.widget, |
||||
|
} |
||||
|
|
||||
|
def _compute_value(self): |
||||
|
return getattr(self, "_compute_value_%s" % self.computation_method)() |
||||
|
|
||||
|
def _compute(self): |
||||
|
value = self._compute_value() |
||||
|
self.write({"value": value}) |
||||
|
if self.store_history: |
||||
|
last = self.env["kpi.kpi.history"].search( |
||||
|
[("kpi_id", "=", self.id)], limit=1 |
||||
|
) |
||||
|
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)) |
||||
|
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()) |
||||
|
|
||||
|
def write(self, vals): |
||||
|
if "value" in vals: |
||||
|
vals["value_last_update"] = fields.Datetime.now() |
||||
|
return super().write(vals) |
||||
|
|
||||
|
def _get_code_input_dict(self): |
||||
|
return { |
||||
|
"self": self, |
||||
|
"model": self.browse(), |
||||
|
"datetime": datetime, |
||||
|
"date": date, |
||||
|
"time": time, |
||||
|
"float_compare": float_compare, |
||||
|
"relativedelta": relativedelta.relativedelta, |
||||
|
} |
||||
|
|
||||
|
def _forbidden_code(self): |
||||
|
return ["commit", "rollback", "getattr", "execute"] |
||||
|
|
||||
|
def _compute_value_code(self): |
||||
|
forbidden = self._forbidden_code() |
||||
|
search_terms = "(" + ("|".join(forbidden)) + ")" |
||||
|
if re.search(search_terms, (self.code or "").lower()): |
||||
|
message = ", ".join(forbidden[:-1]) or "" |
||||
|
if len(message) > 0: |
||||
|
message += _(" or ") |
||||
|
message += forbidden[-1] |
||||
|
raise ValidationError( |
||||
|
_("The code cannot contain the following terms: %s.") % message |
||||
|
) |
||||
|
results = self._get_code_input_dict() |
||||
|
savepoint = "kpi_formula_%s" % self.id |
||||
|
# pylint: disable=E8103 |
||||
|
self.env.cr.execute("savepoint %s" % savepoint) |
||||
|
safe_eval(self.code or "", results, mode="exec", nocopy=True) |
||||
|
# pylint: disable=E8103 |
||||
|
self.env.cr.execute("rollback to %s" % savepoint) |
||||
|
return results.get("result", {}) |
||||
|
|
||||
|
def show_value(self): |
||||
|
self.ensure_one() |
||||
|
action = self.env.ref("kpi_dashboard.kpi_kpi_act_window") |
||||
|
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") |
||||
|
], |
||||
|
} |
||||
|
) |
||||
|
return result |
||||
|
|
||||
|
|
||||
|
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, |
||||
|
) |
||||
|
context = fields.Char() |
||||
|
|
||||
|
def read_dashboard(self): |
||||
|
result = {} |
||||
|
for r in self: |
||||
|
result[r.id] = { |
||||
|
"id": r.action.id, |
||||
|
"type": r.action._name, |
||||
|
"name": r.action.name, |
||||
|
"context": safe_eval(r.context or "{}"), |
||||
|
} |
||||
|
return result |
||||
|
|
||||
|
|
||||
|
class KpiKpiHistory(models.Model): |
||||
|
_name = "kpi.kpi.history" |
||||
|
_description = "KPI history" |
||||
|
_order = "create_date DESC" |
||||
|
|
||||
|
kpi_id = fields.Many2one( |
||||
|
"kpi.kpi", required=True, ondelete="cascade", readonly=True |
||||
|
) |
||||
|
value = fields.Serialized(readonly=True) |
||||
|
raw_value = fields.Char(compute="_compute_raw_value") |
||||
|
name = fields.Char(related="kpi_id.name") |
||||
|
widget = fields.Selection( |
||||
|
selection=lambda self: self.env["kpi.kpi"]._fields["widget"].selection, |
||||
|
required=True, |
||||
|
default="number", |
||||
|
) |
||||
|
|
||||
|
@api.depends("value") |
||||
|
def _compute_raw_value(self): |
||||
|
for record in self: |
||||
|
record.raw_value = json.dumps(record.value) |
||||
|
|
||||
|
def show_form(self): |
||||
|
self.ensure_one() |
||||
|
action = self.env.ref("kpi_dashboard.kpi_kpi_history_act_window") |
||||
|
result = action.read()[0] |
||||
|
result.update( |
||||
|
{ |
||||
|
"res_id": self.id, |
||||
|
"target": "new", |
||||
|
"view_mode": "form", |
||||
|
"views": [(self.env.context.get("form_id"), "form")], |
||||
|
} |
||||
|
) |
||||
|
return result |
@ -0,0 +1,33 @@ |
|||||
|
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` |
||||
|
|
||||
|
#. In order to compute the KPI you can use a predefined function from a model or |
||||
|
use the code to directly compute it. |
||||
|
|
||||
|
Using KPI with code |
||||
|
~~~~~~~~~~~~~~~~~~~ |
||||
|
|
||||
|
Define the code directly on the code field. You can use `self` and `model` as the kpi element |
||||
|
The script should create a variable called `result` as a dictionary that |
||||
|
will be stored as the value. |
||||
|
For example, we can use:: |
||||
|
|
||||
|
result = {} |
||||
|
result['value'] = len(model.search([('id', '=', %s)])) |
||||
|
result['previous'] = len(model.search([('id', '!=', %s)])) |
||||
|
|
||||
|
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 |
@ -0,0 +1 @@ |
|||||
|
* Enric Tobella <etobella@creublanca.es> |
@ -0,0 +1 @@ |
|||||
|
This module adds new kinds of dashboards on a specific new type of view. |
@ -0,0 +1,12 @@ |
|||||
|
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 |
||||
|
access_kpi_kpi_history,access_kpi_kpi_history,model_kpi_kpi_history,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 |
||||
|
manage_kpi_kpi_history,manage_kpi_kpi_history,model_kpi_kpi_history,group_kpi_dashboard_manager,1,1,1,1 |
||||
|
access_kpi_dashboard_menu,access_kpi_dashboard_menu,model_kpi_dashboard_menu,group_kpi_dashboard_manager,1,1,1,1 |
@ -0,0 +1,25 @@ |
|||||
|
<?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> |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,466 @@ |
|||||
|
<?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.15.1: 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="#using-kpi-with-code" id="id3">Using KPI with code</a></li> |
||||
|
<li><a class="reference internal" href="#configure-dashboards" id="id4">Configure dashboards</a></li> |
||||
|
</ul> |
||||
|
</li> |
||||
|
<li><a class="reference internal" href="#bug-tracker" id="id5">Bug Tracker</a></li> |
||||
|
<li><a class="reference internal" href="#credits" id="id6">Credits</a><ul> |
||||
|
<li><a class="reference internal" href="#authors" id="id7">Authors</a></li> |
||||
|
<li><a class="reference internal" href="#contributors" id="id8">Contributors</a></li> |
||||
|
<li><a class="reference internal" href="#maintainers" id="id9">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 > Configuration > KPI Dashboards > 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> |
||||
|
<li>In order to compute the KPI you can use a predefined function from a model or |
||||
|
use the code to directly compute it.</li> |
||||
|
</ol> |
||||
|
</div> |
||||
|
<div class="section" id="using-kpi-with-code"> |
||||
|
<h2><a class="toc-backref" href="#id3">Using KPI with code</a></h2> |
||||
|
<p>Define the code directly on the code field. You can use <cite>self</cite> and <cite>model</cite> as the kpi element |
||||
|
The script should create a variable called <cite>result</cite> as a dictionary that |
||||
|
will be stored as the value. |
||||
|
For example, we can use:</p> |
||||
|
<pre class="literal-block"> |
||||
|
result = {} |
||||
|
result['value'] = len(model.search([('id', '=', %s)])) |
||||
|
result['previous'] = len(model.search([('id', '!=', %s)])) |
||||
|
</pre> |
||||
|
</div> |
||||
|
<div class="section" id="configure-dashboards"> |
||||
|
<h2><a class="toc-backref" href="#id4">Configure dashboards</a></h2> |
||||
|
<ol class="arabic simple"> |
||||
|
<li>Access <cite>Dashboards > Configuration > KPI Dashboards > 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="#id5">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="#id6">Credits</a></h1> |
||||
|
<div class="section" id="authors"> |
||||
|
<h2><a class="toc-backref" href="#id7">Authors</a></h2> |
||||
|
<ul class="simple"> |
||||
|
<li>Creu Blanca</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<div class="section" id="contributors"> |
||||
|
<h2><a class="toc-backref" href="#id8">Contributors</a></h2> |
||||
|
<ul class="simple"> |
||||
|
<li>Enric Tobella <<a class="reference external" href="mailto:etobella@creublanca.es">etobella@creublanca.es</a>></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<div class="section" id="maintainers"> |
||||
|
<h2><a class="toc-backref" href="#id9">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>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p> |
||||
|
<p><a class="reference external" href="https://github.com/etobella"><img alt="etobella" src="https://github.com/etobella.png?size=40px" /></a></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> |
@ -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); |
@ -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
File diff suppressed because it is too large
View File
9503
kpi_dashboard/static/lib/nvd3/d3.v3.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,427 @@ |
|||||
|
/* nvd3 version 1.8.2-dev (https://github.com/novus/nvd3) 2016-02-08 */ |
||||
|
.nvd3 .nv-axis { |
||||
|
pointer-events:none; |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-axis path { |
||||
|
fill: none; |
||||
|
stroke: #000; |
||||
|
stroke-opacity: .75; |
||||
|
shape-rendering: crispEdges; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-axis path.domain { |
||||
|
stroke-opacity: .75; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-axis.nv-x path.domain { |
||||
|
stroke-opacity: 0; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-axis line { |
||||
|
fill: none; |
||||
|
stroke: #e5e5e5; |
||||
|
shape-rendering: crispEdges; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-axis .zero line, |
||||
|
/*this selector may not be necessary*/ .nvd3 .nv-axis line.zero { |
||||
|
stroke-opacity: .75; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-axis .nv-axisMaxMin text { |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .x .nv-axis .nv-axisMaxMin text, |
||||
|
.nvd3 .x2 .nv-axis .nv-axisMaxMin text, |
||||
|
.nvd3 .x3 .nv-axis .nv-axisMaxMin text { |
||||
|
text-anchor: middle |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-axis.nv-disabled { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
|
||||
|
/* scatter */ |
||||
|
.nvd3 .nv-groups .nv-point.hover { |
||||
|
stroke-width: 20px; |
||||
|
stroke-opacity: .5; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-scatter .nv-point.hover { |
||||
|
fill-opacity: 1; |
||||
|
} |
||||
|
.nv-noninteractive { |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
.nv-distx, .nv-disty { |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
.nvtooltip { |
||||
|
position: absolute; |
||||
|
background-color: rgba(255,255,255,1.0); |
||||
|
color: rgba(0,0,0,1.0); |
||||
|
padding: 1px; |
||||
|
border: 1px solid rgba(0,0,0,.2); |
||||
|
z-index: 10000; |
||||
|
display: block; |
||||
|
|
||||
|
font-family: Arial; |
||||
|
font-size: 13px; |
||||
|
text-align: left; |
||||
|
pointer-events: none; |
||||
|
|
||||
|
white-space: nowrap; |
||||
|
|
||||
|
-webkit-touch-callout: none; |
||||
|
-webkit-user-select: none; |
||||
|
-khtml-user-select: none; |
||||
|
-moz-user-select: none; |
||||
|
-ms-user-select: none; |
||||
|
user-select: none; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip { |
||||
|
background: rgba(255,255,255, 0.8); |
||||
|
border: 1px solid rgba(0,0,0,0.5); |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
/*Give tooltips that old fade in transition by |
||||
|
putting a "with-transitions" class on the container div. |
||||
|
*/ |
||||
|
.nvtooltip.with-transitions, .with-transitions .nvtooltip { |
||||
|
transition: opacity 50ms linear; |
||||
|
-moz-transition: opacity 50ms linear; |
||||
|
-webkit-transition: opacity 50ms linear; |
||||
|
|
||||
|
transition-delay: 200ms; |
||||
|
-moz-transition-delay: 200ms; |
||||
|
-webkit-transition-delay: 200ms; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip.x-nvtooltip, |
||||
|
.nvtooltip.y-nvtooltip { |
||||
|
padding: 8px; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip h3 { |
||||
|
margin: 0; |
||||
|
padding: 4px 14px; |
||||
|
line-height: 18px; |
||||
|
font-weight: normal; |
||||
|
background-color: rgba(247,247,247,0.75); |
||||
|
color: rgba(0,0,0,1.0); |
||||
|
text-align: center; |
||||
|
|
||||
|
border-bottom: 1px solid #ebebeb; |
||||
|
|
||||
|
-webkit-border-radius: 5px 5px 0 0; |
||||
|
-moz-border-radius: 5px 5px 0 0; |
||||
|
border-radius: 5px 5px 0 0; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip p { |
||||
|
margin: 0; |
||||
|
padding: 5px 14px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip span { |
||||
|
display: inline-block; |
||||
|
margin: 2px 0; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip table { |
||||
|
margin: 6px; |
||||
|
border-spacing:0; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
.nvtooltip table td { |
||||
|
padding: 2px 9px 2px 0; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip table td.key { |
||||
|
font-weight: normal; |
||||
|
} |
||||
|
.nvtooltip table td.key.total { |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
.nvtooltip table td.value { |
||||
|
text-align: right; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip table tr.highlight td { |
||||
|
padding: 1px 9px 1px 0; |
||||
|
border-bottom-style: solid; |
||||
|
border-bottom-width: 1px; |
||||
|
border-top-style: solid; |
||||
|
border-top-width: 1px; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip table td.legend-color-guide div { |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip table td.legend-color-guide div { |
||||
|
width: 12px; |
||||
|
height: 12px; |
||||
|
border: 1px solid #999; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip .footer { |
||||
|
padding: 3px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.nvtooltip-pending-removal { |
||||
|
pointer-events: none; |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/**** |
||||
|
Interactive Layer |
||||
|
*/ |
||||
|
.nvd3 .nv-interactiveGuideLine { |
||||
|
pointer-events:none; |
||||
|
} |
||||
|
.nvd3 line.nv-guideline { |
||||
|
stroke: #ccc; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-bars rect { |
||||
|
fill-opacity: .75; |
||||
|
|
||||
|
transition: fill-opacity 250ms linear; |
||||
|
-moz-transition: fill-opacity 250ms linear; |
||||
|
-webkit-transition: fill-opacity 250ms linear; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-bars rect.hover { |
||||
|
fill-opacity: 1; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-bars .hover rect { |
||||
|
fill: lightblue; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-bars text { |
||||
|
fill: rgba(0,0,0,0); |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-bars .hover text { |
||||
|
fill: rgba(0,0,0,1); |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-multibar .nv-groups rect, |
||||
|
.nvd3 .nv-multibarHorizontal .nv-groups rect, |
||||
|
.nvd3 .nv-discretebar .nv-groups rect { |
||||
|
stroke-opacity: 0; |
||||
|
|
||||
|
transition: fill-opacity 250ms linear; |
||||
|
-moz-transition: fill-opacity 250ms linear; |
||||
|
-webkit-transition: fill-opacity 250ms linear; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-multibar .nv-groups rect:hover, |
||||
|
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover, |
||||
|
.nvd3 .nv-candlestickBar .nv-ticks rect:hover, |
||||
|
.nvd3 .nv-discretebar .nv-groups rect:hover { |
||||
|
fill-opacity: 1; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-discretebar .nv-groups text, |
||||
|
.nvd3 .nv-multibarHorizontal .nv-groups text { |
||||
|
font-weight: bold; |
||||
|
fill: rgba(0,0,0,1); |
||||
|
stroke: rgba(0,0,0,0); |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-groups path.nv-line { |
||||
|
fill: none; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-groups path.nv-area { |
||||
|
stroke: none; |
||||
|
} |
||||
|
|
||||
|
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point { |
||||
|
fill-opacity: 0; |
||||
|
stroke-opacity: 0; |
||||
|
} |
||||
|
|
||||
|
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point { |
||||
|
fill-opacity: .5 !important; |
||||
|
stroke-opacity: .5 !important; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
.with-transitions .nvd3 .nv-groups .nv-point { |
||||
|
transition: stroke-width 250ms linear, stroke-opacity 250ms linear; |
||||
|
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; |
||||
|
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.nvd3.nv-scatter .nv-groups .nv-point.hover, |
||||
|
.nvd3 .nv-groups .nv-point.hover { |
||||
|
stroke-width: 7px; |
||||
|
fill-opacity: .95 !important; |
||||
|
stroke-opacity: .95 !important; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
.nvd3 .nv-point-paths path { |
||||
|
stroke: #aaa; |
||||
|
stroke-opacity: 0; |
||||
|
fill: #eee; |
||||
|
fill-opacity: 0; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
.nvd3 .nv-indexLine { |
||||
|
cursor: ew-resize; |
||||
|
} |
||||
|
|
||||
|
/******************** |
||||
|
* SVG CSS |
||||
|
*/ |
||||
|
|
||||
|
/******************** |
||||
|
Default CSS for an svg element nvd3 used |
||||
|
*/ |
||||
|
svg.nvd3-svg { |
||||
|
-webkit-touch-callout: none; |
||||
|
-webkit-user-select: none; |
||||
|
-khtml-user-select: none; |
||||
|
-ms-user-select: none; |
||||
|
-moz-user-select: none; |
||||
|
user-select: none; |
||||
|
display: block; |
||||
|
width:100%; |
||||
|
height:100%; |
||||
|
} |
||||
|
|
||||
|
/******************** |
||||
|
Box shadow and border radius styling |
||||
|
*/ |
||||
|
.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip { |
||||
|
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); |
||||
|
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); |
||||
|
box-shadow: 0 5px 10px rgba(0,0,0,.2); |
||||
|
|
||||
|
-webkit-border-radius: 5px; |
||||
|
-moz-border-radius: 5px; |
||||
|
border-radius: 5px; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
.nvd3 text { |
||||
|
font: normal 12px Arial; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .title { |
||||
|
font: bold 14px Arial; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-background { |
||||
|
fill: white; |
||||
|
fill-opacity: 0; |
||||
|
} |
||||
|
|
||||
|
.nvd3.nv-noData { |
||||
|
font-size: 18px; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/********** |
||||
|
* Brush |
||||
|
*/ |
||||
|
|
||||
|
.nv-brush .extent { |
||||
|
fill-opacity: .125; |
||||
|
shape-rendering: crispEdges; |
||||
|
} |
||||
|
|
||||
|
.nv-brush .resize path { |
||||
|
fill: #eee; |
||||
|
stroke: #666; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/********** |
||||
|
* Legend |
||||
|
*/ |
||||
|
|
||||
|
.nvd3 .nv-legend .nv-series { |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-legend .nv-disabled circle { |
||||
|
fill-opacity: 0; |
||||
|
} |
||||
|
|
||||
|
/* focus */ |
||||
|
.nvd3 .nv-brush .extent { |
||||
|
fill-opacity: 0 !important; |
||||
|
} |
||||
|
|
||||
|
.nvd3 .nv-brushBackground rect { |
||||
|
stroke: #000; |
||||
|
stroke-width: .4; |
||||
|
fill: #fff; |
||||
|
fill-opacity: .7; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
.nvd3.nv-pie path { |
||||
|
stroke-opacity: 0; |
||||
|
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; |
||||
|
-moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; |
||||
|
-webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.nvd3.nv-pie .nv-pie-title { |
||||
|
font-size: 24px; |
||||
|
fill: rgba(19, 196, 249, 0.59); |
||||
|
} |
||||
|
|
||||
|
.nvd3.nv-pie .nv-slice text { |
||||
|
stroke: #000; |
||||
|
stroke-width: 0; |
||||
|
} |
||||
|
|
||||
|
.nvd3.nv-pie path { |
||||
|
stroke: #fff; |
||||
|
stroke-width: 1px; |
||||
|
stroke-opacity: 1; |
||||
|
} |
||||
|
|
||||
|
.nvd3.nv-pie path { |
||||
|
fill-opacity: .7; |
||||
|
} |
||||
|
.nvd3.nv-pie .hover path { |
||||
|
fill-opacity: 1; |
||||
|
} |
||||
|
.nvd3.nv-pie .nv-label { |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
.nvd3.nv-pie .nv-label rect { |
||||
|
fill-opacity: 0; |
||||
|
stroke-opacity: 0; |
||||
|
} |
5985
kpi_dashboard/static/lib/nvd3/nv.d3.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,164 @@ |
|||||
|
/* |
||||
|
global py |
||||
|
*/ |
||||
|
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({ |
||||
|
init: function () { |
||||
|
this._super.apply(this, arguments); |
||||
|
this.dashboard_context = {}; |
||||
|
this.dashboard_color_data = []; |
||||
|
}, |
||||
|
custom_events: _.extend({}, BasicController.prototype.custom_events, { |
||||
|
addDashboard: "_addDashboard", |
||||
|
refresh_on_fly: "_refreshOnFly", |
||||
|
modify_context: "_modifyContext", |
||||
|
add_modify_color: "_addModifyColor", |
||||
|
refresh_colors: "_refreshColors", |
||||
|
}), |
||||
|
_refreshOnFly: function () { |
||||
|
var self = this; |
||||
|
this._rpc({ |
||||
|
model: this.modelName, |
||||
|
method: "read_dashboard_on_fly", |
||||
|
args: [[this.renderer.state.res_id]], |
||||
|
context: this._getContext(), |
||||
|
}).then(function (data) { |
||||
|
_.each(data, function (item) { |
||||
|
// We will follow the same logic used on Bus Notifications
|
||||
|
self.renderer._onNotification([ |
||||
|
["kpi_dashboard_" + self.renderer.state.res_id, item], |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
renderPager: function ($node, options) { |
||||
|
// eslint-disable-next-line no-param-reassign
|
||||
|
options = _.extend({}, options, { |
||||
|
validate: this.canBeDiscarded.bind(this), |
||||
|
}); |
||||
|
this._super($node, options); |
||||
|
}, |
||||
|
_pushState: function (state) { |
||||
|
// eslint-disable-next-line no-param-reassign
|
||||
|
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); |
||||
|
}, |
||||
|
_getContext: function () { |
||||
|
return _.extend( |
||||
|
{}, |
||||
|
this.model.get(this.handle, {raw: true}).getContext(), |
||||
|
{bin_size: true}, |
||||
|
this.dashboard_context |
||||
|
); |
||||
|
}, |
||||
|
_modifyContext: function (event) { |
||||
|
var ctx = this._getContext(); |
||||
|
this.dashboard_context = _.extend( |
||||
|
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
|
||||
|
), |
||||
|
}) |
||||
|
); |
||||
|
this._refreshOnFly(event); |
||||
|
this._refreshColors(); |
||||
|
}, |
||||
|
_addModifyColor: function (event) { |
||||
|
this.dashboard_color_data.push([ |
||||
|
event.data.element_id, |
||||
|
event.data.expression, |
||||
|
]); |
||||
|
}, |
||||
|
_refreshColors: function () { |
||||
|
var self = this; |
||||
|
var ctx = this._getContext(); |
||||
|
_.each(this.dashboard_color_data, function (data) { |
||||
|
var color = py.eval(data[1], { |
||||
|
context: _.extend(ctx, { |
||||
|
__getattr__: function () { |
||||
|
return false; |
||||
|
}, |
||||
|
}), |
||||
|
check_if: function (args) { |
||||
|
if (args[0].toJSON()) { |
||||
|
return args[1]; |
||||
|
} |
||||
|
return args[2]; |
||||
|
}, |
||||
|
}); |
||||
|
var $element = self.renderer.$el.find("#" + data[0]); |
||||
|
$element.css("background-color", color); |
||||
|
}); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
return DashboardController; |
||||
|
}); |
@ -0,0 +1,21 @@ |
|||||
|
odoo.define("kpi_dashboard.DashboardModel", function (require) { |
||||
|
"use strict"; |
||||
|
|
||||
|
var BasicModel = require("web.BasicModel"); |
||||
|
|
||||
|
var DashboardModel = BasicModel.extend({ |
||||
|
_fetchRecord: function (record) { |
||||
|
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; |
||||
|
}); |
@ -0,0 +1,108 @@ |
|||||
|
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 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; |
||||
|
}, |
||||
|
_onClickModifyContext: function (modify_context_expression, event) { |
||||
|
this.trigger_up("modify_context", { |
||||
|
context: modify_context_expression, |
||||
|
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"); |
||||
|
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); |
||||
|
element.attr("id", _.uniqueId("kpi_")); |
||||
|
self.$grid.append(element); |
||||
|
if (kpi.modify_color) { |
||||
|
self.trigger_up("add_modify_color", { |
||||
|
element_id: element.attr("id"), |
||||
|
expression: kpi.modify_color_expression, |
||||
|
}); |
||||
|
} |
||||
|
if (kpi.modify_context) { |
||||
|
element.on( |
||||
|
"click", |
||||
|
self._onClickModifyContext.bind( |
||||
|
self, |
||||
|
kpi.modify_context_expression |
||||
|
) |
||||
|
); |
||||
|
element.css("cursor", "pointer"); |
||||
|
// We want to set it show as clickable
|
||||
|
} |
||||
|
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); |
||||
|
if (this.state.specialData.compute_on_fly_refresh > 0) { |
||||
|
// 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"); |
||||
|
// We need to refreshs data in order compute with the current
|
||||
|
// context
|
||||
|
return $.when(); |
||||
|
}, |
||||
|
on_detach_callback: function () { |
||||
|
// We want to clear the refresh interval once we exit the view
|
||||
|
if (this.on_fly_interval) { |
||||
|
clearInterval(this.on_fly_interval); |
||||
|
} |
||||
|
this._super.apply(this, arguments); |
||||
|
}, |
||||
|
_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; |
||||
|
}); |
@ -0,0 +1,40 @@ |
|||||
|
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; |
||||
|
}); |
@ -0,0 +1,61 @@ |
|||||
|
odoo.define("kpi_dashboard.KpiFieldWidget", function (require) { |
||||
|
"use strict"; |
||||
|
|
||||
|
var basic_fields = require("web.basic_fields"); |
||||
|
var field_registry = require("web.field_registry"); |
||||
|
var core = require("web.core"); |
||||
|
var qweb = core.qweb; |
||||
|
var registry = require("kpi_dashboard.widget_registry"); |
||||
|
|
||||
|
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"))); |
||||
|
var marginx = 0; |
||||
|
var marginy = 0; |
||||
|
var widgetx = 400; |
||||
|
var widgety = 400; |
||||
|
this.$el.find(".gridster").css("width", widgety); |
||||
|
this.$grid = this.$el.find(".gridster ul"); |
||||
|
var widgetVals = { |
||||
|
value: this.value, |
||||
|
col: 1, |
||||
|
row: 1, |
||||
|
sizex: 1, |
||||
|
sizey: 1, |
||||
|
name: this.recordData[this.nodeOptions.name], |
||||
|
value_last_update: this.recordData[this.nodeOptions.date], |
||||
|
}; |
||||
|
var Widget = registry.getAny([ |
||||
|
this.recordData[this.nodeOptions.widget], |
||||
|
"abstract", |
||||
|
]); |
||||
|
this.state = { |
||||
|
specialData: { |
||||
|
margin_x: marginx, |
||||
|
margin_y: marginy, |
||||
|
widget_dimension_x: widgetx, |
||||
|
widget_dimension_y: widgety, |
||||
|
}, |
||||
|
}; |
||||
|
var widget = new Widget(this, widgetVals); |
||||
|
var element = $(qweb.render("kpi_dashboard.kpi", {widget: widgetVals})); |
||||
|
element.css("background-color", "white"); |
||||
|
element.css("color", "black"); |
||||
|
this.$grid.append(element); |
||||
|
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); |
||||
|
return KpiFieldWidget; |
||||
|
}); |
@ -0,0 +1,50 @@ |
|||||
|
/* |
||||
|
global nv |
||||
|
*/ |
||||
|
odoo.define("web.nvd3.extensions", function () { |
||||
|
"use strict"; |
||||
|
|
||||
|
/** |
||||
|
* The nvd3 library extensions and fixes should be done here to avoid patching |
||||
|
* in place. |
||||
|
*/ |
||||
|
|
||||
|
nv.dev = false; |
||||
|
// Sets nvd3 library in production mode
|
||||
|
|
||||
|
// monkey patch nvd3 to allow removing eventhandler on windowresize events
|
||||
|
// see https://github.com/novus/nvd3/pull/396 for more details
|
||||
|
|
||||
|
// Adds a resize listener to the window.
|
||||
|
nv.utils.onWindowResize = function (fun) { |
||||
|
if (fun === null) return; |
||||
|
window.addEventListener("resize", fun); |
||||
|
}; |
||||
|
|
||||
|
// Backwards compatibility with current API.
|
||||
|
nv.utils.windowResize = nv.utils.onWindowResize; |
||||
|
|
||||
|
// Removes a resize listener from the window.
|
||||
|
nv.utils.offWindowResize = function (fun) { |
||||
|
if (fun === null) return; |
||||
|
window.removeEventListener("resize", fun); |
||||
|
}; |
||||
|
|
||||
|
// Monkey patch nvd3 to prevent crashes when user changes view and nvd3
|
||||
|
// tries to remove tooltips after 500 ms... seriously nvd3, what were you
|
||||
|
// thinking?
|
||||
|
nv.tooltip.cleanup = function () { |
||||
|
$(".nvtooltip").remove(); |
||||
|
}; |
||||
|
|
||||
|
// Monkey patch nvd3 to prevent it to display a tooltip (position: absolute)
|
||||
|
// with a negative `top`; with this patch the highest tooltip's position is
|
||||
|
// still in the graph
|
||||
|
var originalCalcTooltipPosition = nv.tooltip.calcTooltipPosition; |
||||
|
nv.tooltip.calcTooltipPosition = function () { |
||||
|
var container = originalCalcTooltipPosition.apply(this, arguments); |
||||
|
container.style.top = |
||||
|
container.style.top.split("px")[0] < 0 ? 0 + "px" : container.style.top; |
||||
|
return container; |
||||
|
}; |
||||
|
}); |
@ -0,0 +1,101 @@ |
|||||
|
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 used by the widget
|
||||
|
template: "kpi_dashboard.base_widget", |
||||
|
// Specific css of the widget
|
||||
|
cssLibs: [], |
||||
|
// Specific Javascript libraries of the widget
|
||||
|
jsLibs: [], |
||||
|
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.compute_on_fly && |
||||
|
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"); |
||||
|
var action = this.actions[$($data).data("id")]; |
||||
|
return this.do_action(action.id, { |
||||
|
additional_context: action.context, |
||||
|
}); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
registry.add("abstract", AbstractWidget); |
||||
|
return AbstractWidget; |
||||
|
}); |
@ -0,0 +1,13 @@ |
|||||
|
odoo.define("kpi_dashboard.CounterWidget", function (require) { |
||||
|
"use strict"; |
||||
|
|
||||
|
var IntegerWidget = require("kpi_dashboard.IntegerWidget"); |
||||
|
var registry = require("kpi_dashboard.widget_registry"); |
||||
|
|
||||
|
var CounterWidget = IntegerWidget.extend({ |
||||
|
shortList: [], |
||||
|
}); |
||||
|
|
||||
|
registry.add("counter", CounterWidget); |
||||
|
return CounterWidget; |
||||
|
}); |
@ -0,0 +1,108 @@ |
|||||
|
/* |
||||
|
global nv, d3 |
||||
|
*/ |
||||
|
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: [ |
||||
|
"/kpi_dashboard/static/lib/nvd3/d3.v3.js", |
||||
|
"/kpi_dashboard/static/lib/nvd3/nv.d3.js", |
||||
|
"/kpi_dashboard/static/src/js/lib/nvd3.js", |
||||
|
], |
||||
|
cssLibs: ["/kpi_dashboard/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 () { |
||||
|
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; |
||||
|
}); |
@ -0,0 +1,75 @@ |
|||||
|
odoo.define("kpi_dashboard.IntegerWidget", 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 IntegerWidget = AbstractWidget.extend({ |
||||
|
template: "kpi_dashboard.number", |
||||
|
digits: [3, 0], |
||||
|
shortList: [ |
||||
|
[1000000000000, "T", [3, 1]], |
||||
|
[1000000000, "G", [3, 1]], |
||||
|
[1000000, "M", [3, 1]], |
||||
|
[1000, "K", [3, 1]], |
||||
|
], |
||||
|
shortNumber: function (num) { |
||||
|
var suffix = ""; |
||||
|
var shortened = false; |
||||
|
var digits = this.digits; |
||||
|
var result = num; |
||||
|
_.each(this.shortList, function (shortItem) { |
||||
|
if (!shortened && Math.abs(num) >= shortItem[0]) { |
||||
|
shortened = true; |
||||
|
suffix = shortItem[1]; |
||||
|
result /= shortItem[0]; |
||||
|
digits = shortItem[2]; |
||||
|
} |
||||
|
}); |
||||
|
return ( |
||||
|
field_utils.format.float(result, false, { |
||||
|
digits: digits, |
||||
|
}) + suffix |
||||
|
); |
||||
|
}, |
||||
|
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("integer", IntegerWidget); |
||||
|
return IntegerWidget; |
||||
|
}); |
@ -0,0 +1,34 @@ |
|||||
|
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; |
||||
|
}); |
@ -0,0 +1,22 @@ |
|||||
|
odoo.define("kpi_dashboard.NumberWidget", function (require) { |
||||
|
"use strict"; |
||||
|
|
||||
|
var IntegerWidget = require("kpi_dashboard.IntegerWidget"); |
||||
|
var registry = require("kpi_dashboard.widget_registry"); |
||||
|
var field_utils = require("web.field_utils"); |
||||
|
|
||||
|
var NumberWidget = IntegerWidget.extend({ |
||||
|
digits: [3, 1], |
||||
|
shortNumber: function (num) { |
||||
|
if (Math.abs(num) < 10) { |
||||
|
return field_utils.format.float(num, false, { |
||||
|
digits: [3, 2], |
||||
|
}); |
||||
|
} |
||||
|
return this._super.apply(this, arguments); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
registry.add("number", NumberWidget); |
||||
|
return NumberWidget; |
||||
|
}); |
@ -0,0 +1,16 @@ |
|||||
|
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; |
||||
|
}); |
@ -0,0 +1,7 @@ |
|||||
|
odoo.define("kpi_dashboard.widget_registry", function (require) { |
||||
|
"use strict"; |
||||
|
|
||||
|
var Registry = require("web.Registry"); |
||||
|
|
||||
|
return new Registry(); |
||||
|
}); |
@ -0,0 +1,116 @@ |
|||||
|
.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: 0.8; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,97 @@ |
|||||
|
<?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_id"> |
||||
|
<t t-set="action" t-value="widget.actions[action_id]" /> |
||||
|
<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> |
@ -0,0 +1,68 @@ |
|||||
|
<?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/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> |
||||
|
</template> |
||||
|
</odoo> |
@ -0,0 +1,2 @@ |
|||||
|
from . import test_formula |
||||
|
from . import test_kpi_dashboard |
@ -0,0 +1,77 @@ |
|||||
|
# Copyright 2020 Creu Blanca |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from odoo.exceptions import ValidationError |
||||
|
from odoo.tests.common import TransactionCase |
||||
|
|
||||
|
|
||||
|
class TestFormula(TransactionCase): |
||||
|
def setUp(self): |
||||
|
super().setUp() |
||||
|
self.kpi = self.env["kpi.kpi"].create( |
||||
|
{"name": "DEMO KPI", "widget": "number", "computation_method": "code"} |
||||
|
) |
||||
|
|
||||
|
def test_forbidden_words_01(self): |
||||
|
self.kpi.code = """ |
||||
|
result = {"value": 0} |
||||
|
self.env.cr.commit() |
||||
|
""" |
||||
|
with self.assertRaises(ValidationError): |
||||
|
self.kpi.compute() |
||||
|
|
||||
|
def test_forbidden_words_02(self): |
||||
|
self.kpi.code = """ |
||||
|
result = {"value": 0} |
||||
|
self.env.cr.rollback() |
||||
|
""" |
||||
|
with self.assertRaises(ValidationError): |
||||
|
self.kpi.compute() |
||||
|
|
||||
|
def test_forbidden_words_03(self): |
||||
|
self.kpi.code = """ |
||||
|
result = {"value": 0} |
||||
|
self.env.cr.execute("CoMMiT") |
||||
|
""" |
||||
|
with self.assertRaises(ValidationError): |
||||
|
self.kpi.compute() |
||||
|
|
||||
|
def test_computation(self): |
||||
|
self.assertFalse(self.kpi.value) |
||||
|
self.kpi.compute() |
||||
|
self.assertEqual(self.kpi.value, {}) |
||||
|
self.kpi.code = """ |
||||
|
result = {{}} |
||||
|
result['value'] = len(model.search([('id', '=', {})])) |
||||
|
result['previous'] = len(model.search([('id', '!=', {})])) |
||||
|
""".format( |
||||
|
self.kpi.id, |
||||
|
self.kpi.id, |
||||
|
) |
||||
|
self.kpi.compute() |
||||
|
value = self.kpi.value |
||||
|
self.assertTrue(value.get("value")) |
||||
|
self.assertEqual(value.get("value"), 1) |
||||
|
self.assertEqual(value.get("previous"), self.kpi.search_count([]) - 1) |
||||
|
self.assertFalse(self.kpi.history_ids) |
||||
|
|
||||
|
def test_computation_history(self): |
||||
|
self.assertFalse(self.kpi.value) |
||||
|
self.kpi.store_history = True |
||||
|
self.kpi.compute() |
||||
|
self.assertTrue(self.kpi.history_ids) |
||||
|
self.assertEqual(self.kpi.value, {}) |
||||
|
self.kpi.code = """ |
||||
|
result = {{}} |
||||
|
result['value'] = len(model.search([('id', '=', {})])) |
||||
|
result['previous'] = len(model.search([('id', '!=', {})])) |
||||
|
""".format( |
||||
|
self.kpi.id, |
||||
|
self.kpi.id, |
||||
|
) |
||||
|
self.kpi.compute() |
||||
|
value = self.kpi.value |
||||
|
self.assertTrue(value.get("value")) |
||||
|
self.assertEqual(value.get("value"), 1) |
||||
|
self.assertEqual(value.get("previous"), self.kpi.search_count([]) - 1) |
||||
|
self.assertTrue(self.kpi.history_ids) |
@ -0,0 +1,167 @@ |
|||||
|
# Copyright 2020 Creu Blanca |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from mock import patch |
||||
|
|
||||
|
from odoo.exceptions import ValidationError |
||||
|
from odoo.tests.common import Form, TransactionCase |
||||
|
|
||||
|
|
||||
|
class TestKpiDashboard(TransactionCase): |
||||
|
def setUp(self): |
||||
|
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} |
||||
|
) |
||||
|
|
||||
|
def test_constrains_01(self): |
||||
|
with self.assertRaises(ValidationError): |
||||
|
self.kpi_01.dashboard_item_ids.write({"size_x": 2}) |
||||
|
self.kpi_01.flush() |
||||
|
|
||||
|
def test_constrains_02(self): |
||||
|
with self.assertRaises(ValidationError): |
||||
|
self.kpi_02.dashboard_item_ids.write({"size_x": 4}) |
||||
|
self.kpi_01.flush() |
||||
|
|
||||
|
def test_constrains_03(self): |
||||
|
with self.assertRaises(ValidationError): |
||||
|
self.kpi_01.dashboard_item_ids.write({"size_y": 11}) |
||||
|
|
||||
|
def test_menu(self): |
||||
|
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.generate_menu() |
||||
|
self.assertTrue(self.dashboard.menu_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.assertTrue(self.dashboard.menu_id.groups_id) |
||||
|
|
||||
|
def test_onchange(self): |
||||
|
with Form(self.env["kpi.dashboard"]) as dashboard: |
||||
|
dashboard.name = "New Dashboard" |
||||
|
with dashboard.item_ids.new() as item: |
||||
|
item.kpi_id = self.kpi_01 |
||||
|
self.assertTrue(item.name) |
||||
|
|
||||
|
def test_read_dashboard(self): |
||||
|
data = self.dashboard.read_dashboard() |
||||
|
title_found = False |
||||
|
actions = 0 |
||||
|
for item in data["item_ids"]: |
||||
|
if not item.get("kpi_id"): |
||||
|
title_found = True |
||||
|
if item.get("actions", False): |
||||
|
actions += len(item["actions"]) |
||||
|
self.assertTrue(title_found) |
||||
|
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": "{},{}".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() |
||||
|
title_found = False |
||||
|
actions = 0 |
||||
|
for item in data["item_ids"]: |
||||
|
if not item.get("kpi_id"): |
||||
|
title_found = True |
||||
|
if item.get("actions", False): |
||||
|
actions += len(item["actions"]) |
||||
|
self.assertTrue(title_found) |
||||
|
self.assertEqual(2, actions) |
||||
|
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.generate_menu() |
||||
|
data = self.dashboard.read_dashboard() |
||||
|
self.assertTrue(data.get("action_id", False)) |
||||
|
|
||||
|
def test_compute(self): |
||||
|
self.assertFalse(self.kpi_01.value_last_update) |
||||
|
with patch( |
||||
|
"odoo.addons.kpi_dashboard.models.kpi_kpi." "KpiKpi.test_demo_number", |
||||
|
create=True, |
||||
|
) as f: |
||||
|
f.return_value = {"value": 0} |
||||
|
self.kpi_01.compute() |
||||
|
self.assertTrue(self.kpi_01.value_last_update) |
||||
|
|
||||
|
def test_compute_model(self): |
||||
|
self.assertFalse(self.kpi_01.value_last_update) |
||||
|
self.kpi_01.model_id = self.env.ref("base.model_res_partner") |
||||
|
with patch( |
||||
|
"odoo.addons.base.models.res_partner.Partner.test_demo_number", create=True |
||||
|
) as f: |
||||
|
f.return_value = {"value": 0} |
||||
|
self.kpi_01.compute() |
||||
|
self.assertTrue(self.kpi_01.value_last_update) |
||||
|
|
||||
|
def test_generate_cron(self): |
||||
|
self.assertFalse(self.kpi_01.cron_id) |
||||
|
self.kpi_01.generate_cron() |
||||
|
self.assertTrue(self.kpi_01.cron_id) |
||||
|
self.assertFalse(self.kpi_01.value_last_update) |
||||
|
with patch( |
||||
|
"odoo.addons.kpi_dashboard.models.kpi_kpi." "KpiKpi.test_demo_number", |
||||
|
create=True, |
||||
|
) as f: |
||||
|
f.return_value = {"value": 0} |
||||
|
self.kpi_01.cron_id.method_direct_trigger() |
||||
|
self.assertTrue(self.kpi_01.value_last_update) |
@ -0,0 +1,179 @@ |
|||||
|
<?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" /> |
||||
|
<button |
||||
|
name="technical_config" |
||||
|
string="" |
||||
|
type="object" |
||||
|
icon="fa-edit" |
||||
|
groups="base.group_no_one" |
||||
|
/> |
||||
|
</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 name="config"> |
||||
|
<field name="compute_on_fly_refresh" /> |
||||
|
</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> |
||||
|
<record model="ir.ui.view" id="kpi_dashboard_item_form_view"> |
||||
|
<field name="name">kpi.dashboard.item.form (in kpi_dashboard)</field> |
||||
|
<field name="model">kpi.dashboard.item</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form> |
||||
|
<header /> |
||||
|
<sheet> |
||||
|
<div name="button_box" class="oe_button_box" /> |
||||
|
<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" /> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
<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="model">kpi.dashboard.item</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form> |
||||
|
<header /> |
||||
|
<sheet> |
||||
|
<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'}" |
||||
|
/> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
<footer> |
||||
|
<button |
||||
|
name="store_data" |
||||
|
string="Save" |
||||
|
type="object" |
||||
|
class="oe_highlight" |
||||
|
/> |
||||
|
<button special="cancel" string="Cancel" class="oe_link" /> |
||||
|
</footer> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
</odoo> |
@ -0,0 +1,218 @@ |
|||||
|
<?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.actions.act_window" id="kpi_kpi_history_act_window"> |
||||
|
<field name="name">Kpi History</field> |
||||
|
<field name="res_model">kpi.kpi.history</field> |
||||
|
<field name="view_mode">tree</field> |
||||
|
<field name="domain">[('kpi_id', '=', active_id)]</field> |
||||
|
<field name="context">{}</field> |
||||
|
</record> |
||||
|
<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="model">kpi.kpi.history</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<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'}" |
||||
|
/> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
<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="model">kpi.kpi.history</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form> |
||||
|
<field name="raw_value" /> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
<record model="ir.ui.view" id="kpi_kpi_history_tree_view"> |
||||
|
<field name="name">kpi.kpi.history.tree (in kpi_dashboard)</field> |
||||
|
<field name="model">kpi.kpi.history</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<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}" |
||||
|
/> |
||||
|
<button |
||||
|
name="show_form" |
||||
|
string="Raw value" |
||||
|
type="object" |
||||
|
context="{'form_id': %(kpi_dashboard.kpi_kpi_history_raw_form_view)d}" |
||||
|
/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</record> |
||||
|
<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), ('compute_on_fly', '=', True)]}" |
||||
|
/> |
||||
|
<button |
||||
|
name="compute" |
||||
|
string="Compute now" |
||||
|
type="object" |
||||
|
attrs="{'invisible': [('compute_on_fly', '=', True)]}" |
||||
|
/> |
||||
|
</header> |
||||
|
<sheet> |
||||
|
<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" |
||||
|
/> |
||||
|
</div> |
||||
|
<h2> |
||||
|
<field name="name" /> |
||||
|
</h2> |
||||
|
<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')]}" |
||||
|
/> |
||||
|
</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" /> |
||||
|
<field name="context" /> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</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> |
||||
|
</notebook> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
<record model="ir.ui.view" id="kpi_kpi_widget_form_view"> |
||||
|
<field name="name">kpi.kpi.raw.form (in kpi_dashboard)</field> |
||||
|
<field name="model">kpi.kpi</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<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'}" |
||||
|
/> |
||||
|
</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> |
@ -0,0 +1,13 @@ |
|||||
|
<?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> |
@ -0,0 +1 @@ |
|||||
|
from . import kpi_dashboard_menu |
@ -0,0 +1,16 @@ |
|||||
|
# Copyright 2020 Creu Blanca |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from odoo import 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") |
||||
|
|
||||
|
def generate_menu(self): |
||||
|
self.dashboard_id._generate_menu(self.menu_id) |
@ -0,0 +1,33 @@ |
|||||
|
<?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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue