Add widgets and dashboards.
This commit is contained in:
parent
e524ff3eb7
commit
f0762083f1
10
__init__.py
10
__init__.py
|
@ -7,6 +7,8 @@ from . import babi
|
|||
from . import test_model
|
||||
from . import report
|
||||
from . import table
|
||||
from . import dashboard
|
||||
from . import action
|
||||
|
||||
def register():
|
||||
Pool.register(
|
||||
|
@ -32,6 +34,14 @@ def register():
|
|||
babi.UpdateDataWizardStart,
|
||||
babi.UpdateDataWizardUpdated,
|
||||
babi.CleanExecutionsStart,
|
||||
dashboard.Dashboard,
|
||||
dashboard.DashboardItem,
|
||||
dashboard.Widget,
|
||||
dashboard.WidgetParameter,
|
||||
action.View,
|
||||
action.Action,
|
||||
action.ActionDashboard,
|
||||
action.Menu,
|
||||
test_model.TestBabiModel,
|
||||
table.Table,
|
||||
table.Field,
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
from lxml import etree
|
||||
from trytond.pool import PoolMeta, Pool
|
||||
from trytond.model import fields, ModelSQL, ModelView
|
||||
from trytond.ir.action import ActionMixin
|
||||
|
||||
|
||||
class View(metaclass=PoolMeta):
|
||||
__name__ = 'ir.ui.view'
|
||||
|
||||
@classmethod
|
||||
def get_rng(cls, type_):
|
||||
rng = super().get_rng(type_)
|
||||
if type_ in ('form', 'list-form'):
|
||||
widgets = rng.xpath(
|
||||
'//ns:define/ns:optional/ns:attribute'
|
||||
'/ns:name[.="widget"]/following-sibling::ns:choice',
|
||||
namespaces={'ns': 'http://relaxng.org/ns/structure/1.0'})[0]
|
||||
subelem = etree.SubElement(widgets,
|
||||
'{http://relaxng.org/ns/structure/1.0}value')
|
||||
subelem.text = 'chart'
|
||||
return rng
|
||||
|
||||
|
||||
class Action(metaclass=PoolMeta):
|
||||
__name__ = 'ir.action'
|
||||
|
||||
@classmethod
|
||||
def get_action_values(self, type_, action_ids, columns=None):
|
||||
pool = Pool()
|
||||
ActionDashboard = pool.get('babi.action.dashboard')
|
||||
|
||||
actions = super().get_action_values(type_, action_ids, columns)
|
||||
if type_ == 'babi.action.dashboard':
|
||||
boards = {x.id: x for x in ActionDashboard.browse(action_ids)}
|
||||
for values in actions:
|
||||
values['dashboard'] = boards[values['id']].dashboard.id
|
||||
return actions
|
||||
|
||||
|
||||
class ActionDashboard(ActionMixin, ModelSQL, ModelView):
|
||||
"Action Act Dashboard"
|
||||
__name__ = 'babi.action.dashboard'
|
||||
dashboard = fields.Many2One('babi.dashboard', 'Dashboard', required=True,
|
||||
ondelete='CASCADE')
|
||||
action = fields.Many2One('ir.action', 'Action', ondelete='CASCADE')
|
||||
|
||||
@staticmethod
|
||||
def default_type():
|
||||
return 'babi.action.dashboard'
|
||||
|
||||
|
||||
class Menu(metaclass=PoolMeta):
|
||||
__name__ = 'ir.ui.menu'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.action.selection.append(('babi.action.dashboard', 'Dashboard'))
|
||||
|
||||
@classmethod
|
||||
def _get_action(cls, action_id):
|
||||
pool = Pool()
|
||||
Action = pool.get('ir.action')
|
||||
action = Action(action_id)
|
||||
if action.type == 'babi.action.dashboard':
|
||||
action = ActionDashboard(action_id)
|
||||
return super()._get_action(action)
|
|
@ -0,0 +1,36 @@
|
|||
<tryton>
|
||||
<data>
|
||||
<!-- babi.action.dashboard -->
|
||||
<!---->
|
||||
<record model="ir.ui.view" id="action_dashboard_view_form">
|
||||
<field name="model">babi.action.dashboard</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">action_dashboard_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="action_dashboard_view_tree">
|
||||
<field name="model">babi.action.dashboard</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">action_dashboard_list</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_action_dashboard_form">
|
||||
<field name="name">Dashboards</field>
|
||||
<field name="type">ir.action.act_window</field>
|
||||
<field name="res_model">babi.action.dashboard</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_action_url_form_view1">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view" ref="action_dashboard_view_tree"/>
|
||||
<field name="act_window" ref="act_action_dashboard_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_action_url_form_view2">
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="view" ref="action_dashboard_view_form"/>
|
||||
<field name="act_window" ref="act_action_dashboard_form"/>
|
||||
</record>
|
||||
<menuitem
|
||||
parent="ir.menu_action"
|
||||
action="act_action_dashboard_form"
|
||||
sequence="60"
|
||||
id="menu_action_dashboard"/>
|
||||
</data>
|
||||
</tryton>
|
44
babi.xml
44
babi.xml
|
@ -672,51 +672,37 @@ contains the full copyright notices and license terms. -->
|
|||
</record>
|
||||
|
||||
<!-- Menus -->
|
||||
<menuitem id="menu_babi" name="Business Intelligence" sequence="100"
|
||||
icon="tryton-graph"/>
|
||||
<menuitem id="menu_configuration" name="Configuration"
|
||||
parent="menu_babi" sequence="10" icon="tryton-settings"/>
|
||||
<menuitem id="menu_update_data" name="Update data"
|
||||
parent="menu_babi" sequence="10" active="0"/>
|
||||
<menuitem id="menu_historical_data" name="View historical data"
|
||||
parent="menu_babi" sequence="10" active="0"/>
|
||||
<menuitem id="menu_babi" name="Business Intelligence" sequence="100" icon="tryton-graph"/>
|
||||
<menuitem id="menu_configuration" name="Configuration" parent="menu_babi" sequence="10" icon="tryton-settings"/>
|
||||
<menuitem id="menu_update_data" name="Update data" parent="menu_babi" sequence="10" active="0"/>
|
||||
<menuitem id="menu_historical_data" name="View historical data" parent="menu_babi" sequence="10" active="0"/>
|
||||
|
||||
<record model="ir.ui.menu-res.group"
|
||||
id="menu_babi_group_babi">
|
||||
<record model="ir.ui.menu-res.group" id="menu_babi_group_babi">
|
||||
<field name="menu" ref="menu_babi"/>
|
||||
<field name="group" ref="group_babi"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.menu-res.group"
|
||||
id="menu_configuration_group_babi_admin">
|
||||
<record model="ir.ui.menu-res.group" id="menu_configuration_group_babi_admin">
|
||||
<field name="menu" ref="menu_configuration"/>
|
||||
<field name="group" ref="group_babi_admin"/>
|
||||
</record>
|
||||
|
||||
<menuitem action="act_babi_expression" id="menu_babi_expression"
|
||||
parent="menu_configuration" sequence="10" name="Expressions"/>
|
||||
<menuitem action="act_babi_filter" id="menu_babi_filter"
|
||||
parent="menu_configuration" sequence="20" name="Filters"/>
|
||||
<menuitem action="act_babi_report" id="menu_babi_report"
|
||||
parent="menu_babi" sequence="20" name="Reports"/>
|
||||
<record model="ir.ui.menu-res.group"
|
||||
id="menu_report_group_babi">
|
||||
<menuitem action="act_babi_expression" id="menu_babi_expression" parent="menu_configuration" sequence="10" name="Expressions"/>
|
||||
<menuitem action="act_babi_filter" id="menu_babi_filter" parent="menu_configuration" sequence="20" name="Filters"/>
|
||||
|
||||
<menuitem action="act_babi_report" id="menu_babi_report" parent="menu_babi" sequence="20" name="Reports"/>
|
||||
<record model="ir.ui.menu-res.group" id="menu_report_group_babi">
|
||||
<field name="menu" ref="menu_babi_report"/>
|
||||
<field name="group" ref="group_babi"/>
|
||||
</record>
|
||||
<record model="ir.ui.menu-res.group"
|
||||
id="menu_report_group_babi_admin">
|
||||
<record model="ir.ui.menu-res.group" id="menu_report_group_babi_admin">
|
||||
<field name="menu" ref="menu_babi_report"/>
|
||||
<field name="group" ref="group_babi_admin"/>
|
||||
</record>
|
||||
<menuitem action="act_babi_execution" id="menu_babi_execution"
|
||||
parent="menu_babi_report" sequence="10" name="Executions"/>
|
||||
<menuitem action="clear_executions_wizard"
|
||||
id="menu_babi_clean_execution" parent="menu_babi_report"
|
||||
sequence="20" name="Clean Executions"/>
|
||||
<menuitem action="act_babi_execution" id="menu_babi_execution" parent="menu_babi_report" sequence="10" name="Executions"/>
|
||||
<menuitem action="clear_executions_wizard" id="menu_babi_clean_execution" parent="menu_babi_report" sequence="20" name="Clean Executions"/>
|
||||
|
||||
<record model="ir.ui.menu-res.group"
|
||||
id="menu_execution_group_babi_admin">
|
||||
<record model="ir.ui.menu-res.group" id="menu_execution_group_babi_admin">
|
||||
<field name="menu" ref="menu_babi_execution"/>
|
||||
<field name="group" ref="group_babi_admin"/>
|
||||
</record>
|
||||
|
|
|
@ -0,0 +1,536 @@
|
|||
import json
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.backend import DatabaseOperationalError
|
||||
from trytond.model import ModelSQL, ModelView, sequence_ordered, fields
|
||||
from trytond.pyson import Eval, Bool
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.i18n import gettext
|
||||
|
||||
|
||||
class Dashboard(ModelSQL, ModelView):
|
||||
'Dashboard'
|
||||
__name__ = 'babi.dashboard'
|
||||
name = fields.Char('Name', required=True)
|
||||
widgets = fields.One2Many('babi.dashboard.item', 'dashboard', 'Widgets')
|
||||
view = fields.Function(fields.Text('View'), 'get_view')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._buttons.update({
|
||||
'show': {
|
||||
'icon': 'tryton-board',
|
||||
},
|
||||
})
|
||||
|
||||
def get_view(self, name):
|
||||
pool = Pool()
|
||||
Widget = pool.get('babi.dashboard.item')
|
||||
return json.dumps(Widget._get_view(self.widgets))
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
def show(cls, actions):
|
||||
if not actions:
|
||||
return
|
||||
action = actions[0]
|
||||
return {
|
||||
'name': action.name,
|
||||
'type': 'babi.action.dashboard',
|
||||
'dashboard': action.id,
|
||||
}
|
||||
|
||||
class DashboardItem(sequence_ordered(), ModelSQL, ModelView):
|
||||
'Dashboard Item'
|
||||
__name__ = 'babi.dashboard.item'
|
||||
dashboard = fields.Many2One('babi.dashboard', 'Dashboard', required=True)
|
||||
widget = fields.Many2One('babi.widget', 'Widget', required=True)
|
||||
colspan = fields.Integer('Columns')
|
||||
parent = fields.Many2One('babi.dashboard.item', 'Parent', domain=[
|
||||
('dashboard', '=', Eval('dashboard')),
|
||||
], depends=['dashboard'])
|
||||
children = fields.One2Many('babi.dashboard.item', 'parent', 'Children')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.__access__.add('dashboard')
|
||||
|
||||
@classmethod
|
||||
def _get_view(cls, widgets):
|
||||
res = []
|
||||
for widget in widgets:
|
||||
res.append({
|
||||
'widget': widget.widget.id,
|
||||
'colspan': widget.colspan or 1,
|
||||
'children': cls._get_view(widget.children),
|
||||
})
|
||||
return res
|
||||
|
||||
|
||||
class Widget(ModelSQL, ModelView):
|
||||
'Widget'
|
||||
__name__ = 'babi.widget'
|
||||
name = fields.Char('Name', required=True)
|
||||
type = fields.Selection([
|
||||
(None, ''),
|
||||
('area', 'Area'),
|
||||
('bar', 'Bar'),
|
||||
('bubble', 'Bubble'),
|
||||
('doughnut', 'Doughnut'),
|
||||
('funnel', 'Funnel'),
|
||||
('gauge', 'Gauge'),
|
||||
('line', 'Line'),
|
||||
('pie', 'Pie'),
|
||||
('scatter', 'Scatter'),
|
||||
('table', 'Table'),
|
||||
('value', 'Value'),
|
||||
], 'Type')
|
||||
table = fields.Many2One('babi.table', 'Table', states={
|
||||
'invisible': ~Bool(Eval('type')),
|
||||
'required': Bool(Eval('type')),
|
||||
}, depends=['type'])
|
||||
where = fields.Char('Where', states={
|
||||
'invisible': ~Bool(Eval('type')),
|
||||
}, depends=['type'])
|
||||
parameters = fields.One2Many('babi.widget.parameter', 'widget',
|
||||
'Parameters')
|
||||
chart = fields.Function(fields.Text('Chart'), 'on_change_with_chart')
|
||||
timeout = fields.Integer('Timeout (s)', required=True)
|
||||
show_legend = fields.Boolean('Show Legend')
|
||||
|
||||
@staticmethod
|
||||
def default_timeout():
|
||||
Config = Pool().get('babi.configuration')
|
||||
config = Config(1)
|
||||
return config.default_timeout or 30
|
||||
|
||||
@fields.depends('type', 'where', 'parameters', 'timeout', 'show_legend',
|
||||
methods=['get_values'])
|
||||
def on_change_with_chart(self, name=None):
|
||||
data = []
|
||||
layout = {
|
||||
# None should become false, not null in JS
|
||||
'showlegend': bool(self.show_legend),
|
||||
}
|
||||
config = {}
|
||||
try:
|
||||
chart = {
|
||||
'type': self.type,
|
||||
}
|
||||
data.append(chart)
|
||||
if self.type == 'area':
|
||||
chart.update(self.get_values())
|
||||
chart['type'] = 'scatter'
|
||||
chart['fill'] = 'tonexty'
|
||||
elif self.type == 'bar':
|
||||
chart.update(self.get_values())
|
||||
elif self.type == 'pie':
|
||||
chart.update(self.get_values())
|
||||
elif self.type == 'bubble':
|
||||
values = self.get_values()
|
||||
chart['type'] = 'scatter'
|
||||
chart.update({
|
||||
'x': values.get('x', []),
|
||||
'y': values.get('y', []),
|
||||
})
|
||||
chart['mode'] = 'markers'
|
||||
chart['marker'] = {
|
||||
'size': values.get('sizes', []),
|
||||
}
|
||||
elif self.type == 'doughnut':
|
||||
chart.update(self.get_values())
|
||||
chart['type'] = 'pie'
|
||||
chart['hole'] = 0.4
|
||||
elif self.type == 'funnel':
|
||||
values = self.get_values()
|
||||
chart['type'] = 'funnelarea'
|
||||
chart['values'] = values.get('values', [])
|
||||
chart['text'] = values.get('labels', [])
|
||||
layout.update({
|
||||
'funnelmode': 'stack',
|
||||
})
|
||||
elif self.type == 'gauge':
|
||||
values = self.get_values()
|
||||
value = values.get('value', [])
|
||||
chart['value'] = value and value[0] or '-'
|
||||
chart['mode'] = 'gauge'
|
||||
if 'delta' in values:
|
||||
chart['mode'] += '+delta'
|
||||
delta = values['delta']
|
||||
chart['delta'] = {
|
||||
'reference': delta and delta[0] or '-',
|
||||
}
|
||||
min = values.get('min', [])
|
||||
min = min and min[0] or 0
|
||||
max = values.get('max', [])
|
||||
max = max and max[0] or 100
|
||||
chart['type'] = 'indicator'
|
||||
chart['gauge'] = {
|
||||
'axis': {
|
||||
'visible': False,
|
||||
'range': [min, max],
|
||||
},
|
||||
}
|
||||
print(chart)
|
||||
elif self.type == 'line':
|
||||
chart['type'] = 'scatter'
|
||||
chart.update(self.get_values())
|
||||
elif self.type == 'scatter':
|
||||
chart['type'] = 'scatter'
|
||||
chart.update(self.get_values())
|
||||
chart['mode'] = 'markers'
|
||||
elif self.type == 'table':
|
||||
values = self.get_values()
|
||||
chart['type'] = 'table'
|
||||
header = []
|
||||
columns = []
|
||||
for key, vals in self.get_values().items():
|
||||
header.append(key)
|
||||
columns.append(vals)
|
||||
chart['header'] = {
|
||||
'values': header,
|
||||
}
|
||||
chart['cells'] = {
|
||||
'values': columns,
|
||||
}
|
||||
elif self.type == 'value':
|
||||
values = self.get_values()
|
||||
value = values.get('value', [])
|
||||
chart['value'] = value and value[0] or '-'
|
||||
chart['mode'] = 'number'
|
||||
if 'delta' in values:
|
||||
chart['mode'] += '+delta'
|
||||
delta = values['delta']
|
||||
chart['delta'] = {
|
||||
'reference': delta and delta[0] or '-',
|
||||
}
|
||||
chart['type'] = 'indicator'
|
||||
chart['gauge'] = {
|
||||
'axis': {
|
||||
'visible': False,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
data = [{
|
||||
'type': 'error',
|
||||
'message': str(e),
|
||||
}]
|
||||
return json.dumps({
|
||||
'data': data,
|
||||
'layout': layout,
|
||||
'config': config,
|
||||
})
|
||||
|
||||
@fields.depends('type', 'table', 'parameters')
|
||||
def on_change_type(self):
|
||||
pool = Pool()
|
||||
Parameter = pool.get('babi.widget.parameter')
|
||||
|
||||
if not self.type:
|
||||
return
|
||||
settings = self.parameter_settings()
|
||||
parameters = []
|
||||
for type in settings.keys():
|
||||
for parameter in self.parameters:
|
||||
if parameter.type == type:
|
||||
parameters.append(parameter)
|
||||
break
|
||||
else:
|
||||
parameter = Parameter()
|
||||
parameter.type = type
|
||||
parameters.append(parameter)
|
||||
|
||||
self.parameters = tuple(parameters)
|
||||
|
||||
def get_parameter(self, type):
|
||||
for parameter in self.parameters:
|
||||
if parameter.type == type:
|
||||
return parameter
|
||||
|
||||
@fields.depends('table', 'parameters', 'where', 'timeout')
|
||||
def get_values(self):
|
||||
if not self.table:
|
||||
return {}
|
||||
fields = [x.select_expression for x in self.parameters]
|
||||
if None in fields:
|
||||
return {}
|
||||
groupby = [x.groupby_expression for x in self.parameters
|
||||
if x.groupby_expression]
|
||||
records = self.table.execute_query(fields, self.where, groupby,
|
||||
self.timeout)
|
||||
|
||||
res = {}
|
||||
types = [x.type for x in self.parameters]
|
||||
# Transpose records and fields
|
||||
transposed = list(map(list, zip(*records)))
|
||||
for type, values in zip(types, transposed):
|
||||
res[type] = values
|
||||
return res
|
||||
|
||||
@fields.depends('type')
|
||||
def parameter_settings(self):
|
||||
if self.type == 'area':
|
||||
return {
|
||||
'x': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'y': {
|
||||
'min': 1,
|
||||
'max': 10,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'bar':
|
||||
return {
|
||||
'x': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'y': {
|
||||
'min': 1,
|
||||
'max': 10,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'pie':
|
||||
return {
|
||||
'labels': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'values': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'bubble':
|
||||
return {
|
||||
'x': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'y': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'sizes': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'doughnut':
|
||||
return {
|
||||
'labels': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'values': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'funnel':
|
||||
return {
|
||||
'labels': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'values': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'gauge':
|
||||
return {
|
||||
'delta': {
|
||||
'min': 0,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
'minimum': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
'maximum': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
'value': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'line':
|
||||
return {
|
||||
'x': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'y': {
|
||||
'min': 1,
|
||||
'max': 10,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'scatter':
|
||||
return {
|
||||
'x': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'y': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
}
|
||||
elif self.type == 'table':
|
||||
return {
|
||||
'labels': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'forbidden',
|
||||
},
|
||||
'values': {
|
||||
'min': 1,
|
||||
'max': 10,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
elif self.type == 'value':
|
||||
return {
|
||||
'delta': {
|
||||
'min': 0,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
'value': {
|
||||
'min': 1,
|
||||
'max': 1,
|
||||
'aggregate': 'required',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class WidgetParameter(ModelSQL, ModelView):
|
||||
'Widget Parameter'
|
||||
__name__ = 'babi.widget.parameter'
|
||||
widget = fields.Many2One('babi.widget', 'Widget', required=True,
|
||||
ondelete='CASCADE')
|
||||
type = fields.Selection([
|
||||
('delta', 'Delta'),
|
||||
('labels', 'Labels'),
|
||||
('minimum', 'Minimum'),
|
||||
('maximum', 'Maximum'),
|
||||
('sizes', 'Sizes'),
|
||||
('value', 'Value'),
|
||||
('values', 'Values'),
|
||||
('x', 'X'),
|
||||
('y', 'Y'),
|
||||
], 'Type', required=True)
|
||||
field = fields.Many2One('babi.field', 'Field', domain=[
|
||||
('table', '=', Eval('_parent_widget', {}).get('table', -1)),
|
||||
])
|
||||
aggregate = fields.Selection([
|
||||
(None, ''),
|
||||
('sum', 'Sum'),
|
||||
('count', 'Count'),
|
||||
('avg', 'Average'),
|
||||
('min', 'Minimum'),
|
||||
('max', 'Maximum'),
|
||||
], 'Aggregate', states={
|
||||
'required': Bool(Eval('aggregate_required')),
|
||||
'invisible': Bool(Eval('aggregate_invisible')),
|
||||
}, depends=['type', 'aggregate_required', 'aggregate_invisible'])
|
||||
aggregate_required = fields.Function(fields.Boolean('Aggregate Required'),
|
||||
'on_change_with_aggregate_required')
|
||||
aggregate_invisible = fields.Function(fields.Boolean('Aggregate Invisible'),
|
||||
'on_change_with_aggregate_invisible')
|
||||
|
||||
@fields.depends('type', 'widget', '_parent_widget.type')
|
||||
def on_change_with_aggregate_required(self, name=None):
|
||||
if not self.widget:
|
||||
return
|
||||
settings = self.widget.parameter_settings()
|
||||
return settings.get(self.type, {}).get('aggregate') == 'required'
|
||||
|
||||
@fields.depends('type', 'widget', '_parent_widget.type')
|
||||
def on_change_with_aggregate_invisible(self, name=None):
|
||||
if not self.widget:
|
||||
return
|
||||
settings = self.widget.parameter_settings()
|
||||
return settings.get(self.type, {}).get('aggregate') == 'forbidden'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.__access__.add('widget')
|
||||
|
||||
@classmethod
|
||||
def validate(cls, parameters):
|
||||
super().validate(parameters)
|
||||
for parameter in parameters:
|
||||
parameter.check_aggregate()
|
||||
parameter.check_type()
|
||||
|
||||
def get_rec_name(self, name):
|
||||
res = self.type
|
||||
if self.field:
|
||||
res += ' - ' + self.field.name
|
||||
return res
|
||||
|
||||
def check_aggregate(self):
|
||||
if not self.aggregate or not self.field:
|
||||
return
|
||||
|
||||
if self.aggregate in ('sum', 'avg'):
|
||||
if self.field.type not in ('integer', 'float', 'numeric'):
|
||||
raise UserError(gettext('babi.msg_invalid_aggregate',
|
||||
parameter=self.rec_name, widget=self.widget.rec_name))
|
||||
settings = self.widget.parameter_settings()
|
||||
|
||||
def check_type(self):
|
||||
settings = self.widget.parameter_settings()
|
||||
if self.type not in settings:
|
||||
raise UserError(gettext('babi.msg_invalid_parameter_type',
|
||||
parameter=self.rec_name, widget=self.widget.rec_name,
|
||||
types=', '.join(settings.keys())))
|
||||
|
||||
@property
|
||||
def groupby_expression(self):
|
||||
if not self.field:
|
||||
return
|
||||
if self.aggregate:
|
||||
return
|
||||
return self.field.internal_name
|
||||
|
||||
@property
|
||||
def select_expression(self):
|
||||
if not self.field:
|
||||
return
|
||||
if self.aggregate:
|
||||
return '%s(%s)' % (self.aggregate.upper(), self.field.internal_name)
|
||||
else:
|
||||
settings = self.widget.parameter_settings()
|
||||
if settings.get(self.type, {}).get('aggregate') == 'required':
|
||||
return
|
||||
return self.field.internal_name
|
|
@ -0,0 +1,171 @@
|
|||
<tryton>
|
||||
<data>
|
||||
<record model="ir.action.url" id="action_url">
|
||||
<field name="name">Dashboard</field>
|
||||
<field name="url">babi-board</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_dashboard_open" name="Dashboard" parent="menu_babi" sequence="40" action="action_url"/>
|
||||
|
||||
<!-- babi.dashboard -->
|
||||
<record model="ir.ui.view" id="babi_dashboard_form_view">
|
||||
<field name="model">babi.dashboard</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">dashboard_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="babi_dashboard_tree_view">
|
||||
<field name="model">babi.dashboard</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">dashboard_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_babi_dashboard">
|
||||
<field name="name">Dashboards</field>
|
||||
<field name="res_model">babi.dashboard</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_babi_dashboard_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="babi_dashboard_tree_view"/>
|
||||
<field name="act_window" ref="act_babi_dashboard"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_babi_dashboard_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="babi_dashboard_form_view"/>
|
||||
<field name="act_window" ref="act_babi_dashboard"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_babi_dashboard">
|
||||
<field name="model" search="[('model', '=', 'babi.dashboard')]"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_babi_dashboard_babi">
|
||||
<field name="model" search="[('model', '=', 'babi.dashboard')]"/>
|
||||
<field name="group" ref="group_babi"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.button" id="show_dashboard_button">
|
||||
<field name="name">show</field>
|
||||
<field name="string">Show</field>
|
||||
<field name="model" search="[('model', '=', 'babi.dashboard')]"/>
|
||||
</record>
|
||||
<record model="ir.model.button-res.group" id="show_dashboard_button_group_babi">
|
||||
<field name="button" ref="show_dashboard_button"/>
|
||||
<field name="group" ref="group_babi"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_dashboard" parent="menu_babi" sequence="1" action="act_babi_dashboard"/>
|
||||
|
||||
<!-- babi.dashboard.item -->
|
||||
<record model="ir.ui.view" id="babi_dashboard_item_form_view">
|
||||
<field name="model">babi.dashboard.item</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">dashboard_item_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="babi_dashboard_item_tree_view">
|
||||
<field name="model">babi.dashboard.item</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">dashboard_item_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_babi_dashboard_item">
|
||||
<field name="name">Dashboard Items</field>
|
||||
<field name="res_model">babi.dashboard.item</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_babi_dashboard_item_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="babi_dashboard_item_tree_view"/>
|
||||
<field name="act_window" ref="act_babi_dashboard_item"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_babi_dashboard_item_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="babi_dashboard_item_form_view"/>
|
||||
<field name="act_window" ref="act_babi_dashboard_item"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_babi_dashboard_item">
|
||||
<field name="model" search="[('model', '=', 'babi.dashboard.item')]"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_babi_dashboard_item_babi">
|
||||
<field name="model" search="[('model', '=', 'babi.dashboard.item')]"/>
|
||||
<field name="group" ref="group_babi"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- babi.widget -->
|
||||
<record model="ir.ui.view" id="babi_widget_form_view">
|
||||
<field name="model">babi.widget</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">widget_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="babi_widget_tree_view">
|
||||
<field name="model">babi.widget</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">widget_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_babi_widget">
|
||||
<field name="name">Widgets</field>
|
||||
<field name="res_model">babi.widget</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_babi_widget_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="babi_widget_tree_view"/>
|
||||
<field name="act_window" ref="act_babi_widget"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_babi_widget_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="babi_widget_form_view"/>
|
||||
<field name="act_window" ref="act_babi_widget"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_babi_widget">
|
||||
<field name="model" search="[('model', '=', 'babi.widget')]"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_babi_widget_babi">
|
||||
<field name="model" search="[('model', '=', 'babi.widget')]"/>
|
||||
<field name="group" ref="group_babi"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_widget" parent="menu_dashboard" sequence="1" action="act_babi_widget"/>
|
||||
|
||||
<!-- babi.widget.parameter -->
|
||||
<record model="ir.ui.view" id="babi_widget_parameter_form_view">
|
||||
<field name="model">babi.widget.parameter</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">widget_parameter_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="babi_widget_parameter_tree_view">
|
||||
<field name="model">babi.widget.parameter</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">widget_parameter_list</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
|
@ -90,5 +90,11 @@ Exception: %(error)s</field>
|
|||
|
||||
%(error)s</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_invalid_aggregate">
|
||||
<field name="text">Aggregates "Sum" and "Average" are not supported in non numeric fields in parameter "%(parameter)s" in widget "%(widget)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_invalid_parameter_type">
|
||||
<field name="text">Parameter "%(parameter)s" is not valid in widget "%(widget)s". Only the following types can be used: "%(types)s".</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
|
|
325
table.py
325
table.py
|
@ -7,17 +7,20 @@ from simpleeval import EvalWithCompoundTypes
|
|||
from trytond.bus import notify
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool
|
||||
from trytond.model import ModelView, ModelSQL, fields, Unique, DeactivableMixin
|
||||
from trytond.model import (ModelView, ModelSQL, fields, Unique,
|
||||
DeactivableMixin, sequence_ordered)
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.i18n import gettext
|
||||
from trytond.pyson import Eval, PYSONDecoder
|
||||
from .babi import TimeoutChecker, TimeoutException
|
||||
from trytond import backend
|
||||
from .babi import TimeoutChecker, TimeoutException, FIELD_TYPES
|
||||
from .babi_eval import babi_eval
|
||||
|
||||
VALID_FIRST_SYMBOLS = 'abcdefghijklmnopqrstuvwxyz'
|
||||
VALID_NEXT_SYMBOLS = '_0123456789'
|
||||
VALID_SYMBOLS = VALID_FIRST_SYMBOLS + VALID_NEXT_SYMBOLS
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def convert_to_symbol(text):
|
||||
|
@ -46,14 +49,30 @@ class Table(DeactivableMixin, ModelSQL, ModelView):
|
|||
'BABI Table'
|
||||
__name__ = 'babi.table'
|
||||
name = fields.Char('Name', required=True)
|
||||
type = fields.Selection([
|
||||
(None, ''),
|
||||
('model', 'Model'),
|
||||
('table', 'Table'),
|
||||
('query', 'Query'),
|
||||
], 'Type', required=True)
|
||||
internal_name = fields.Char('Internal Name', required=True)
|
||||
model = fields.Many2One('ir.model', 'Model', required=True,
|
||||
domain=[('babi_enabled', '=', True)])
|
||||
model = fields.Many2One('ir.model', 'Model', states={
|
||||
'invisible': Eval('type') != 'model',
|
||||
'required': Eval('type') == 'model',
|
||||
}, domain=[('babi_enabled', '=', True)])
|
||||
filter = fields.Many2One('babi.filter', 'Filter', domain=[
|
||||
('model', '=', Eval('model')),
|
||||
], depends=['model'])
|
||||
], states={
|
||||
'invisible': Eval('type') != 'model',
|
||||
}, depends=['model'])
|
||||
fields_ = fields.One2Many('babi.field', 'table', 'Fields')
|
||||
timeout = fields.Integer('Timeout', required=True, help='If table '
|
||||
query = fields.Text('Query', states={
|
||||
'invisible': ~Eval('type').in_(['query', 'table']),
|
||||
'required': Eval('type').in_(['query', 'table']),
|
||||
}, depends=['type'])
|
||||
timeout = fields.Integer('Timeout', required=True, states={
|
||||
'invisible': ~Eval('type').in_(['model', 'table']),
|
||||
}, help='If table '
|
||||
'calculation should take more than the specified timeout (in seconds) '
|
||||
'the process will be stopped automatically.')
|
||||
babi_raise_user_error = fields.Boolean('Raise User Error',
|
||||
|
@ -134,118 +153,242 @@ class Table(DeactivableMixin, ModelSQL, ModelView):
|
|||
for table in tables:
|
||||
cls.__queue__._compute(table)
|
||||
|
||||
@property
|
||||
def table_name(self):
|
||||
# Add a suffix to the table name to prevent removing production tables
|
||||
return '__' + self.internal_name
|
||||
|
||||
def get_query(self, fields=None, where=None, groupby=None, limit=None):
|
||||
query = 'SELECT '
|
||||
if fields:
|
||||
query += ', '.join(fields) + ' '
|
||||
else:
|
||||
query += '* '
|
||||
|
||||
if self.type == 'query':
|
||||
query += 'FROM (%s) AS a ' % self._stripped_query
|
||||
else:
|
||||
query += 'FROM %s ' % self.table_name
|
||||
if where:
|
||||
where = where.format(**Transaction().context)
|
||||
query += 'WHERE %s ' % where
|
||||
|
||||
if groupby:
|
||||
query += 'GROUP BY %s ' % ', '.join(groupby) + ' '
|
||||
|
||||
query += 'ORDER BY %s' % ', '.join(fields)
|
||||
return query
|
||||
|
||||
def execute_query(self, fields=None, where=None, groupby=None, timeout=None,
|
||||
limit=None):
|
||||
if timeout is None:
|
||||
timeout = 10
|
||||
if not backend.TableHandler.table_exist(self.table_name):
|
||||
return []
|
||||
with Transaction().new_transaction() as transaction:
|
||||
cursor = transaction.connection.cursor()
|
||||
cursor.execute('SET statement_timeout TO %s;' % timeout * 1000)
|
||||
query = self.get_query(fields, where=where, groupby=groupby,
|
||||
limit=limit)
|
||||
cursor.execute(query)
|
||||
records = cursor.fetchall()
|
||||
return records
|
||||
|
||||
def timeout_exception(self):
|
||||
raise TimeoutException
|
||||
|
||||
def _compute(self):
|
||||
if not self.fields_:
|
||||
raise UserError(gettext('babi.msg_table_no_fields',
|
||||
table=self.name))
|
||||
if self.type == 'model':
|
||||
if not self.fields_:
|
||||
raise UserError(gettext('babi.msg_table_no_fields',
|
||||
table=self.name))
|
||||
|
||||
if self.filter and self.filter.parameters:
|
||||
raise UserError(gettext('babi.msg_filter_with_parameters',
|
||||
table=self.rec_name))
|
||||
if self.filter and self.filter.parameters:
|
||||
raise UserError(gettext('babi.msg_filter_with_parameters',
|
||||
table=self.rec_name))
|
||||
|
||||
if self.type == 'model':
|
||||
self._compute_model()
|
||||
elif self.type == 'table':
|
||||
self._compute_table()
|
||||
elif self.type == 'query':
|
||||
self._compute_query()
|
||||
|
||||
def update_fields(self, field_names):
|
||||
pool = Pool()
|
||||
Field = pool.get('babi.field')
|
||||
|
||||
# Update self.fields_
|
||||
to_save = []
|
||||
to_delete = []
|
||||
existing_fields = set([])
|
||||
for field in self.fields_:
|
||||
if field.internal_name not in field_names:
|
||||
to_delete.append(field)
|
||||
continue
|
||||
field.sequence = field_names.index(field.internal_name)
|
||||
existing_fields.add(field.internal_name)
|
||||
to_save.append(field)
|
||||
|
||||
for field_name in (set(field_names) - existing_fields):
|
||||
field = Field()
|
||||
field.table = self
|
||||
field.name = field_name
|
||||
field.internal_name = field_name
|
||||
field.sequence = field_names.index(field.internal_name)
|
||||
to_save.append(field)
|
||||
|
||||
Field.save(to_save)
|
||||
Field.delete(to_delete)
|
||||
|
||||
@property
|
||||
def _stripped_query(self):
|
||||
if self.query:
|
||||
return self.query.strip().rstrip(';')
|
||||
else:
|
||||
return ''
|
||||
|
||||
def _compute_query(self):
|
||||
with Transaction().new_transaction() as transaction:
|
||||
cursor = Transaction().connection.cursor()
|
||||
cursor.execute('%s LIMIT 1' % self._stripped_query)
|
||||
|
||||
field_names = [x[0] for x in cursor.description]
|
||||
self.update_fields(field_names)
|
||||
|
||||
def _compute_table(self):
|
||||
with Transaction().new_transaction() as transaction:
|
||||
cursor = Transaction().connection.cursor()
|
||||
cursor.execute('DROP TABLE IF EXISTS "%s"' % self.table_name)
|
||||
cursor.execute('CREATE TABLE "%s" AS %s' % (self.table_name,
|
||||
self._stripped_query))
|
||||
cursor.execute('SELECT * FROM "%s" LIMIT 1' % self.table_name)
|
||||
|
||||
field_names = [x[0] for x in cursor.description]
|
||||
self.update_fields(field_names)
|
||||
|
||||
|
||||
|
||||
def _compute_model(self):
|
||||
Model = Pool().get(self.model.model)
|
||||
|
||||
cursor = Transaction().connection.cursor()
|
||||
with Transaction().new_transaction() as transaction:
|
||||
cursor = Transaction().connection.cursor()
|
||||
|
||||
# Create table
|
||||
cursor.execute('DROP TABLE IF EXISTS "%s"' % self.internal_name)
|
||||
fields = []
|
||||
for field in self.fields_:
|
||||
fields.append('"%s" %s' % (field.internal_name, field.sql_type()))
|
||||
cursor.execute('CREATE TABLE IF NOT EXISTS "%s" (%s);' % (
|
||||
self.internal_name, ', '.join(fields)))
|
||||
cursor.execute('DROP TABLE IF EXISTS "%s"' % self.table_name)
|
||||
fields = []
|
||||
for field in self.fields_:
|
||||
fields.append('"%s" %s' % (field.internal_name, field.sql_type()))
|
||||
cursor.execute('CREATE TABLE IF NOT EXISTS "%s" (%s);' % (
|
||||
self.table_name, ', '.join(fields)))
|
||||
|
||||
checker = TimeoutChecker(self.timeout, self.timeout_exception)
|
||||
domain = self.get_domain_filter()
|
||||
checker = TimeoutChecker(self.timeout, self.timeout_exception)
|
||||
domain = self.get_domain_filter()
|
||||
|
||||
context = self.get_context()
|
||||
if not context:
|
||||
context = {}
|
||||
else:
|
||||
assert isinstance(context, dict)
|
||||
context['_datetime'] = None
|
||||
# This is needed when execute the wizard to calculate the report, to
|
||||
# ensure the company rule is used.
|
||||
context['_check_access'] = True
|
||||
context = self.get_context()
|
||||
if not context:
|
||||
context = {}
|
||||
else:
|
||||
assert isinstance(context, dict)
|
||||
context['_datetime'] = None
|
||||
# This is needed when execute the wizard to calculate the report, to
|
||||
# ensure the company rule is used.
|
||||
context['_check_access'] = True
|
||||
|
||||
python_filter = self.get_python_filter()
|
||||
python_filter = self.get_python_filter()
|
||||
|
||||
table = sql.Table(self.internal_name)
|
||||
columns = [sql.Column(table, x.internal_name) for x in self.fields_]
|
||||
expressions = [x.expression.expression for x in self.fields_]
|
||||
index = 0
|
||||
count = 0
|
||||
offset = 2000
|
||||
table = sql.Table(self.table_name)
|
||||
columns = [sql.Column(table, x.internal_name) for x in self.fields_]
|
||||
expressions = [x.expression.expression for x in self.fields_]
|
||||
index = 0
|
||||
count = 0
|
||||
offset = 2000
|
||||
|
||||
with Transaction().set_context(**context):
|
||||
try:
|
||||
records = Model.search(domain, offset=index * offset,
|
||||
limit=offset)
|
||||
except Exception as message:
|
||||
if self.babi_raise_user_error:
|
||||
raise UserError(gettext(
|
||||
'babi.create_data_exception',
|
||||
error=repr(message)))
|
||||
raise
|
||||
|
||||
while records:
|
||||
checker.check()
|
||||
logger.info('Calculated %s, %s records in %s seconds'
|
||||
% (self.model.model, count, checker.elapsed))
|
||||
|
||||
to_insert = []
|
||||
for record in records:
|
||||
if python_filter:
|
||||
if not babi_eval(python_filter, record, convert_none=None):
|
||||
continue
|
||||
values = []
|
||||
for expression in expressions:
|
||||
try:
|
||||
values.append(babi_eval(expression, record,
|
||||
convert_none=None))
|
||||
except Exception as message:
|
||||
notify(gettext('babi.msg_compute_table_exception',
|
||||
table=self.name, field=field.name,
|
||||
record=record.id, error=repr(message)),
|
||||
priority=1)
|
||||
if self.babi_raise_user_error:
|
||||
raise UserError(gettext(
|
||||
'babi.msg_compute_table_exception',
|
||||
table=self.name,
|
||||
field=field.name,
|
||||
record=record.id,
|
||||
error=repr(message)))
|
||||
raise
|
||||
|
||||
to_insert.append(values)
|
||||
|
||||
cursor.execute(*table.insert(columns=columns, values=to_insert))
|
||||
|
||||
index += 1
|
||||
count += len(records)
|
||||
with Transaction().set_context(**context):
|
||||
records = Model.search(domain, offset=index * offset,
|
||||
limit=offset)
|
||||
try:
|
||||
records = Model.search(domain, offset=index * offset,
|
||||
limit=offset)
|
||||
except Exception as message:
|
||||
if self.babi_raise_user_error:
|
||||
raise UserError(gettext(
|
||||
'babi.create_data_exception',
|
||||
error=repr(message)))
|
||||
raise
|
||||
|
||||
while records:
|
||||
checker.check()
|
||||
logger.info('Calculated %s, %s records in %s seconds'
|
||||
% (self.model.model, count, checker.elapsed))
|
||||
|
||||
to_insert = []
|
||||
for record in records:
|
||||
if python_filter:
|
||||
if not babi_eval(python_filter, record, convert_none=None):
|
||||
continue
|
||||
values = []
|
||||
for expression in expressions:
|
||||
try:
|
||||
values.append(babi_eval(expression, record,
|
||||
convert_none=None))
|
||||
except Exception as message:
|
||||
notify(gettext('babi.msg_compute_table_exception',
|
||||
table=self.name, field=field.name,
|
||||
record=record.id, error=repr(message)),
|
||||
priority=1)
|
||||
if self.babi_raise_user_error:
|
||||
raise UserError(gettext(
|
||||
'babi.msg_compute_table_exception',
|
||||
table=self.name,
|
||||
field=field.name,
|
||||
record=record.id,
|
||||
error=repr(message)))
|
||||
raise
|
||||
|
||||
to_insert.append(values)
|
||||
|
||||
cursor.execute(*table.insert(columns=columns, values=to_insert))
|
||||
|
||||
index += 1
|
||||
count += len(records)
|
||||
with Transaction().set_context(**context):
|
||||
records = Model.search(domain, offset=index * offset,
|
||||
limit=offset)
|
||||
|
||||
logger.info('Calculated %s, %s records in %s seconds'
|
||||
% (self.model.model, count, checker.elapsed))
|
||||
|
||||
|
||||
class Field(ModelSQL, ModelView):
|
||||
class Field(sequence_ordered(), ModelSQL, ModelView):
|
||||
'BABI Field'
|
||||
__name__ = 'babi.field'
|
||||
table = fields.Many2One('babi.table', 'Table', required=True)
|
||||
name = fields.Char('Name', required=True)
|
||||
internal_name = fields.Char('Internal Name', required=True)
|
||||
expression = fields.Many2One('babi.expression', 'Expression', required=True,
|
||||
domain=[
|
||||
expression = fields.Many2One('babi.expression', 'Expression', states={
|
||||
'invisible': Eval('table_type') != 'model',
|
||||
'required': Eval('table_type') == 'model'
|
||||
}, domain=[
|
||||
('model', '=', Eval('model')),
|
||||
], depends=['model'])
|
||||
model = fields.Function(fields.Many2One('ir.model', 'Model'),
|
||||
'on_change_with_model')
|
||||
type = fields.Function(fields.Selection(FIELD_TYPES, 'Type'),
|
||||
'on_change_with_type')
|
||||
table_type = fields.Function(fields.Selection([
|
||||
('model', 'Model'),
|
||||
('table', 'Table'),
|
||||
('query', 'Query'),
|
||||
], 'Table Type'), 'on_change_with_table_type')
|
||||
|
||||
@fields.depends('expression')
|
||||
def on_change_with_type(self, name=None):
|
||||
if self.expression:
|
||||
return self.expression.ttype
|
||||
|
||||
@fields.depends('table', '_parent_table.type')
|
||||
def on_change_with_table_type(self, name=None):
|
||||
if self.table:
|
||||
return self.table.type
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
|
@ -295,7 +438,7 @@ class Field(ModelSQL, ModelView):
|
|||
self.name = self.expression.name
|
||||
self.on_change_name()
|
||||
|
||||
@fields.depends('table', '_parent_table.id')
|
||||
@fields.depends('table', '_parent_table.model')
|
||||
def on_change_with_model(self, name=None):
|
||||
if self.table:
|
||||
if self.table and self.table.model:
|
||||
return self.table.model.id
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<field name="model" search="[('model', '=', 'babi.table')]"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_babi_table" parent="menu_babi" action="act_babi_table" sequence="10"/>
|
||||
<menuitem id="menu_babi_table" parent="menu_configuration" action="act_babi_table" sequence="30"/>
|
||||
|
||||
<!-- babi.field -->
|
||||
<record model="ir.ui.view" id="babi_field_form_view">
|
||||
|
|
|
@ -717,9 +717,10 @@ class BabiTestCase(CompanyTestMixin, ModuleTestCase):
|
|||
self.create_data()
|
||||
|
||||
table = Table()
|
||||
table.name = 'Table'
|
||||
table.type = 'model'
|
||||
table.name = 'Table 1'
|
||||
table.on_change_name()
|
||||
self.assertEqual(table.internal_name, 'table')
|
||||
self.assertEqual(table.internal_name, 'table_1')
|
||||
table.model, = Model.search([('model', '=', 'babi.test')])
|
||||
|
||||
fields = []
|
||||
|
@ -739,9 +740,25 @@ class BabiTestCase(CompanyTestMixin, ModuleTestCase):
|
|||
table._compute()
|
||||
|
||||
cursor = Transaction().connection.cursor()
|
||||
cursor.execute('SELECT count(*) FROM "%s"' % table.internal_name)
|
||||
cursor.execute('SELECT count(*) FROM "%s"' % table.table_name)
|
||||
count = cursor.fetchall()[0][0]
|
||||
self.assertNotEqual(count, 0)
|
||||
|
||||
table = Table()
|
||||
table.type = 'table'
|
||||
table.name = 'Table 2'
|
||||
table.on_change_name()
|
||||
table.query = 'SELECT amount, category FROM babi_test'
|
||||
table.save()
|
||||
table._compute()
|
||||
fields = [x.internal_name for x in table.fields_]
|
||||
self.assertEqual(fields, ['amount', 'category'])
|
||||
|
||||
table.query = 'SELECT date, amount FROM babi_test'
|
||||
table.save()
|
||||
table._compute()
|
||||
fields = [x.internal_name for x in table.fields_]
|
||||
self.assertEqual(fields, ['date', 'amount'])
|
||||
|
||||
|
||||
del ModuleTestCase
|
||||
|
|
|
@ -12,3 +12,5 @@ xml:
|
|||
configuration.xml
|
||||
cron.xml
|
||||
messages.xml
|
||||
dashboard.xml
|
||||
action.xml
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<form>
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<label name="active"/>
|
||||
<field name="active" xexpand="0" width="100"/>
|
||||
<notebook colspan="4">
|
||||
<page string="General" id="general">
|
||||
<label name="dashboard"/>
|
||||
<field name="dashboard"/>
|
||||
<label name="icon"/>
|
||||
<field name="icon"/>
|
||||
</page>
|
||||
<page name="keywords">
|
||||
<field name="keywords" colspan="4"/>
|
||||
</page>
|
||||
<page name="groups">
|
||||
<field name="groups" colspan="4"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
|
@ -0,0 +1,5 @@
|
|||
<tree>
|
||||
<field name="name" expand="2"/>
|
||||
<field name="type" expand="1"/>
|
||||
<field name="dashboard" expand="1"/>
|
||||
</tree>
|
|
@ -0,0 +1,6 @@
|
|||
<form>
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<button name="show" colspan="2"/>
|
||||
<field name="widgets" colspan="4"/>
|
||||
</form>
|
|
@ -0,0 +1,10 @@
|
|||
<form col="6">
|
||||
<label name="dashboard"/>
|
||||
<field name="dashboard"/>
|
||||
<label name="widget"/>
|
||||
<field name="widget"/>
|
||||
<label name="sequence"/>
|
||||
<field name="sequence"/>
|
||||
<label name="colspan"/>
|
||||
<field name="colspan"/>
|
||||
</form>
|
|
@ -0,0 +1,5 @@
|
|||
<tree sequence="sequence" editable="1">
|
||||
<field name="dashboard"/>
|
||||
<field name="widget"/>
|
||||
<field name="colspan"/>
|
||||
</tree>
|
|
@ -0,0 +1,4 @@
|
|||
<tree>
|
||||
<field name="name"/>
|
||||
<button name="show"/>
|
||||
</tree>
|
|
@ -1,4 +1,4 @@
|
|||
<tree editable="1">
|
||||
<tree editable="1" sequence="sequence">
|
||||
<field name="table"/>
|
||||
<field name="expression"/>
|
||||
<field name="name"/>
|
||||
|
|
|
@ -3,11 +3,16 @@
|
|||
<field name="name"/>
|
||||
<label name="internal_name"/>
|
||||
<field name="internal_name"/>
|
||||
<label name="type"/>
|
||||
<field name="type"/>
|
||||
<label name="model"/>
|
||||
<field name="model"/>
|
||||
<label name="filter"/>
|
||||
<field name="filter" colspan="3"/>
|
||||
<field name="filter"/>
|
||||
<button name="compute" colspan="2"/>
|
||||
<newline/>
|
||||
<separator name="query" colspan="6"/>
|
||||
<field name="query" colspan="6"/>
|
||||
<notebook colspan="6">
|
||||
<page name="fields_">
|
||||
<field name="fields_" colspan="4"/>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<form col="6">
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<label name="type"/>
|
||||
<field name="type"/>
|
||||
<label name="table"/>
|
||||
<field name="table"/>
|
||||
<label name="where"/>
|
||||
<field name="where" colspan="5"/>
|
||||
<label name="timeout"/>
|
||||
<field name="timeout"/>
|
||||
<newline/>
|
||||
<notebook colspan="4">
|
||||
<page name="parameters">
|
||||
<field name="parameters" colspan="4"/>
|
||||
</page>
|
||||
<page id="settings" string="Settings">
|
||||
<label name="show_legend"/>
|
||||
<field name="show_legend"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<field name="chart" widget="chart" colspan="2"/>
|
||||
</form>
|
|
@ -0,0 +1,6 @@
|
|||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="type"/>
|
||||
<field name="table"/>
|
||||
<field name="where"/>
|
||||
</tree>
|
|
@ -0,0 +1,11 @@
|
|||
<form col="6">
|
||||
<label name="widget"/>
|
||||
<field name="widget"/>
|
||||
<newline/>
|
||||
<label name="type"/>
|
||||
<field name="type"/>
|
||||
<label name="field"/>
|
||||
<field name="field"/>
|
||||
<label name="aggregate"/>
|
||||
<field name="aggregate"/>
|
||||
</form>
|
|
@ -0,0 +1,6 @@
|
|||
<tree editable="1">
|
||||
<field name="widget"/>
|
||||
<field name="type"/>
|
||||
<field name="field" expand="1"/>
|
||||
<field name="aggregate"/>
|
||||
</tree>
|
Loading…
Reference in New Issue