Browse Source

Merge PR #416 into 12.0

Signed-off-by dreispt
12.0
OCA-git-bot 4 years ago
parent
commit
e0180490a8
  1. 27
      kpi_dashboard/models/kpi_dashboard.py
  2. 116
      kpi_dashboard/models/kpi_kpi.py
  3. 2
      kpi_dashboard/security/ir.model.access.csv
  4. 22
      kpi_dashboard/static/src/js/dashboard_controller.js
  5. 13
      kpi_dashboard/static/src/js/dashboard_renderer.js
  6. 68
      kpi_dashboard/static/src/js/field_widget.js
  7. 7
      kpi_dashboard/static/src/js/widget/abstract_widget.js
  8. 5
      kpi_dashboard/static/src/xml/dashboard.xml
  9. 22
      kpi_dashboard/tests/test_formula.py
  10. 3
      kpi_dashboard/views/kpi_dashboard.xml
  11. 84
      kpi_dashboard/views/kpi_kpi.xml
  12. 1
      kpi_dashboard/views/webclient_templates.xml

27
kpi_dashboard/models/kpi_dashboard.py

@ -16,6 +16,10 @@ class KpiDashboard(models.Model):
"kpi.dashboard.item", inverse_name="dashboard_id", copy=True, "kpi.dashboard.item", inverse_name="dashboard_id", copy=True,
) )
number_of_columns = fields.Integer(default=5, required=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") width = fields.Integer(compute="_compute_width")
margin_y = fields.Integer(default=10, required=True) margin_y = fields.Integer(default=10, required=True)
margin_x = fields.Integer(default=10, required=True) margin_x = fields.Integer(default=10, required=True)
@ -43,6 +47,15 @@ class KpiDashboard(models.Model):
+ rec.widget_dimension_x * rec.number_of_columns + 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): def read_dashboard(self):
self.ensure_one() self.ensure_one()
result = { result = {
@ -52,6 +65,7 @@ class KpiDashboard(models.Model):
"max_cols": self.number_of_columns, "max_cols": self.number_of_columns,
"margin_x": self.margin_x, "margin_x": self.margin_x,
"margin_y": self.margin_y, "margin_y": self.margin_y,
"compute_on_fly_refresh": self.compute_on_fly_refresh,
"widget_dimension_x": self.widget_dimension_x, "widget_dimension_x": self.widget_dimension_x,
"widget_dimension_y": self.widget_dimension_y, "widget_dimension_y": self.widget_dimension_y,
"background_color": self.background_color, "background_color": self.background_color,
@ -167,10 +181,19 @@ class KpiDashboardItem(models.Model):
"kpi_id": self.kpi_id.id, "kpi_id": self.kpi_id.id,
"suffix": self.kpi_id.suffix or "", "suffix": self.kpi_id.suffix or "",
"prefix": self.kpi_id.prefix or "", "prefix": self.kpi_id.prefix or "",
"value": self.kpi_id.value,
"value_last_update": self.kpi_id.value_last_update,
"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: if self.kpi_id.action_ids:
vals["actions"] = self.kpi_id.action_ids.read_dashboard() vals["actions"] = self.kpi_id.action_ids.read_dashboard()
else: else:

116
kpi_dashboard/models/kpi_kpi.py

@ -5,7 +5,11 @@ from odoo import api, fields, models, _
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
import ast import ast
from odoo.tools.safe_eval import safe_eval from odoo.tools.safe_eval import safe_eval
from odoo.addons.base.models.ir_cron import _intervalTypes
from odoo.tools.float_utils import float_compare
import re import re
import json
import datetime
class KpiKpi(models.Model): class KpiKpi(models.Model):
@ -38,6 +42,26 @@ class KpiKpi(models.Model):
help="Actions that can be opened from the KPI" help="Actions that can be opened from the KPI"
) )
code = fields.Text("Code") 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): def _cron_vals(self):
return { return {
@ -55,14 +79,32 @@ class KpiKpi(models.Model):
record._compute() record._compute()
return True 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): def _compute(self):
self.write(
{
"value": getattr(
self, "_compute_value_%s" % self.computation_method
)()
}
)
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 = [] notifications = []
for dashboard_item in self.dashboard_item_ids: for dashboard_item in self.dashboard_item_ids:
channel = "kpi_dashboard_%s" % dashboard_item.dashboard_id.id channel = "kpi_dashboard_%s" % dashboard_item.dashboard_id.id
@ -92,6 +134,8 @@ class KpiKpi(models.Model):
return { return {
"self": self, "self": self,
"model": self.browse(), "model": self.browse(),
"datetime": datetime,
"float_compare": float_compare,
} }
def _forbidden_code(self): def _forbidden_code(self):
@ -115,6 +159,20 @@ class KpiKpi(models.Model):
self.env.cr.execute("rollback to %s" % savepoint) self.env.cr.execute("rollback to %s" % savepoint)
return results.get("result", {}) 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): class KpiKpiAction(models.Model):
_name = 'kpi.kpi.action' _name = 'kpi.kpi.action'
@ -129,13 +187,49 @@ class KpiKpiAction(models.Model):
('ir.actions.client', 'ir.actions.client')], ('ir.actions.client', 'ir.actions.client')],
required=True, required=True,
) )
context = fields.Char()
def read_dashboard(self): def read_dashboard(self):
result = []
result = {}
for r in self: for r in self:
result.append({
result[r.id] = {
'id': r.action.id, 'id': r.action.id,
'type': r.action._name, 'type': r.action._name,
'name': 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)
@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 return result

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

@ -3,7 +3,9 @@ access_kpi_dashboard,access_kpi_dashboard,model_kpi_dashboard,base.group_user,1,
access_kpi_dashboard_kpi,access_kpi_dashboard_kpi,model_kpi_dashboard_item,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,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_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,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_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,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_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

22
kpi_dashboard/static/src/js/dashboard_controller.js

@ -10,7 +10,29 @@ odoo.define('kpi_dashboard.DashboardController', function (require) {
var DashboardController = BasicController.extend({ var DashboardController = BasicController.extend({
custom_events: _.extend({}, BasicController.prototype.custom_events, { custom_events: _.extend({}, BasicController.prototype.custom_events, {
addDashboard: '_addDashboard', addDashboard: '_addDashboard',
refresh_on_fly: '_refreshOnFly',
}), }),
_refreshOnFly: function (event) {
var self = this;
this._rpc({
model: this.modelName,
method: 'read_dashboard_on_fly',
args: [[this.renderer.state.res_id]],
context: _.extend(
{},
this.model.get(this.handle, {raw: true}).getContext(),
{bin_size: true}
),
}).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) { renderPager: function ($node, options) {
options = _.extend({}, options, { options = _.extend({}, options, {
validate: this.canBeDiscarded.bind(this), validate: this.canBeDiscarded.bind(this),

13
kpi_dashboard/static/src/js/dashboard_renderer.js

@ -53,8 +53,21 @@ odoo.define('kpi_dashboard.DashboardRenderer', function (require) {
'bus_service', 'onNotification', 'bus_service', 'onNotification',
this, this._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);
};
return $.when(); 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) { _onNotification: function (notifications) {
var self = this; var self = this;
_.each(notifications, function (notification) { _.each(notifications, function (notification) {

68
kpi_dashboard/static/src/js/field_widget.js

@ -0,0 +1,68 @@
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;
});

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

@ -54,7 +54,7 @@ odoo.define('kpi_dashboard.AbstractWidget', function (require) {
return; return;
this.fillWidget(values); this.fillWidget(values);
var item = this.$el.find('[data-bind="value_last_update_display"]'); var item = this.$el.find('[data-bind="value_last_update_display"]');
if (item && values.value_last_update !== undefined) {
if (item && ! values.compute_on_fly && values.value_last_update !== undefined) {
var value = field_utils.parse.datetime(values.value_last_update); var value = field_utils.parse.datetime(values.value_last_update);
item.text(value.clone().add( item.text(value.clone().add(
this.getSession().getTZOffset(value), 'minutes').format( this.getSession().getTZOffset(value), 'minutes').format(
@ -82,7 +82,10 @@ odoo.define('kpi_dashboard.AbstractWidget', function (require) {
_onClickDirectAction: function(event) { _onClickDirectAction: function(event) {
event.preventDefault(); event.preventDefault();
var $data = $(event.currentTarget).closest('a'); var $data = $(event.currentTarget).closest('a');
return this.do_action($($data).data('id'));
var action = this.actions[$($data).data('id')];
return this.do_action(action.id, {
additional_context: action.context
});
} }
}); });

5
kpi_dashboard/static/src/xml/dashboard.xml

@ -19,9 +19,10 @@
</t> </t>
<t t-name="kpi_dashboard.ManagePanel"> <t t-name="kpi_dashboard.ManagePanel">
<t t-if="widget.actions" > <t t-if="widget.actions" >
<t t-foreach="widget.actions" t-as="action">
<t t-foreach="widget.actions" t-as="action_id">
<t t-set="action" t-value="widget.actions[action_id]"/>
<div role="menuitem" class=""> <div role="menuitem" class="">
<a role="menuitem" href="#" class="direct_action" t-att-data-id="action.id" t-att-data-type="action.type">Go to <t t-esc="action.name"/></a>
<a role="menuitem" href="#" class="direct_action" t-att-data-id="action_id" t-att-data-type="action.type">Go to <t t-esc="action.name"/></a>
</div> </div>
</t> </t>
</t> </t>

22
kpi_dashboard/tests/test_formula.py

@ -57,3 +57,25 @@ result['previous'] = len(model.search([('id', '!=', %s)]))
self.assertTrue(value.get("value")) self.assertTrue(value.get("value"))
self.assertEqual(value.get("value"), 1) self.assertEqual(value.get("value"), 1)
self.assertEqual(value.get("previous"), self.kpi.search_count([]) - 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', '=', %s)]))
result['previous'] = len(model.search([('id', '!=', %s)]))
""" % (
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)

3
kpi_dashboard/views/kpi_dashboard.xml

@ -54,6 +54,9 @@
<group name="color"> <group name="color">
<field name="background_color" widget="color"/> <field name="background_color" widget="color"/>
</group> </group>
<group name="config">
<field name="compute_on_fly_refresh"/>
</group>
</group> </group>
</page> </page>
<page name="group" string="Groups"> <page name="group" string="Groups">

84
kpi_dashboard/views/kpi_kpi.xml

@ -4,6 +4,55 @@
<odoo> <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"> <record model="ir.ui.view" id="kpi_kpi_form_view">
<field name="name">kpi.kpi.form (in kpi_dashboard)</field> <field name="name">kpi.kpi.form (in kpi_dashboard)</field>
<field name="model">kpi.kpi</field> <field name="model">kpi.kpi</field>
@ -11,11 +60,20 @@
<form> <form>
<header> <header>
<button name="generate_cron" string="Generate cron" type="object" <button name="generate_cron" string="Generate cron" type="object"
attrs="{'invisible': [('cron_id', '!=',False)]}"/>
<button name="compute" string="Compute now" type="object"/>
attrs="{'invisible': ['|', ('cron_id', '!=',False), ('compute_on_fly', '=', True)]}"/>
<button name="compute" string="Compute now" type="object" attrs="{'invisible': [('compute_on_fly', '=', True)]}"/>
</header> </header>
<sheet> <sheet>
<div class="oe_button_box" name="button_box"/>
<div class="oe_button_box" name="button_box">
<button name="%(kpi_dashboard.kpi_kpi_history_act_window)d"
string="Show history"
type="action"
attrs="{'invisible': ['|', ('store_history', '=', False), ('compute_on_fly', '=', True)]}"
icon="fa-history"/>
<button string="Show value" type="object" name="show_value"
icon="fa-paint-brush"
/>
</div>
<h2> <h2>
<field name="name"/> <field name="name"/>
</h2> </h2>
@ -23,6 +81,10 @@
<group> <group>
<field name="computation_method"/> <field name="computation_method"/>
<field name="widget"/> <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="model_id" attrs="{'invisible': [('computation_method', '!=', 'function')]}"/>
<field name="function" attrs="{'required': [('computation_method', '=', 'function')], '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="args" attrs="{'invisible': [('computation_method', '!=', 'function')]}"/>
@ -42,6 +104,7 @@
<field name="action_ids"> <field name="action_ids">
<tree editable="bottom"> <tree editable="bottom">
<field name="action"/> <field name="action"/>
<field name="context"/>
</tree> </tree>
</field> </field>
</page> </page>
@ -56,6 +119,21 @@
</field> </field>
</record> </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"> <record model="ir.ui.view" id="kpi_kpi_search_view">
<field name="name">kpi.kpi.search (in kpi_dashboard)</field> <field name="name">kpi.kpi.search (in kpi_dashboard)</field>
<field name="model">kpi.kpi</field> <field name="model">kpi.kpi</field>

1
kpi_dashboard/views/webclient_templates.xml

@ -18,6 +18,7 @@
<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/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/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/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"/> <link rel="stylesheet" type="text/scss" href="/kpi_dashboard/static/src/scss/kpi_dashboard.scss"/>
</xpath> </xpath>

Loading…
Cancel
Save