diff --git a/addons/base/__openerp__.py b/addons/base/__openerp__.py index 9cf8a6c893e..cfe8850ab03 100644 --- a/addons/base/__openerp__.py +++ b/addons/base/__openerp__.py @@ -22,7 +22,7 @@ "static/lib/underscore/underscore.string.js", "static/src/js/boot.js", "static/src/js/core.js", - "static/src/js/dates.js", + "static/src/js/formats.js", "static/src/js/chrome.js", "static/src/js/views.js", "static/src/js/data.js", diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index aaf89853cad..4f6b3ea4f70 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -390,10 +390,10 @@ def load_actions_from_ir_values(req, key, key2, models, meta, context): Values = req.session.model('ir.values') actions = Values.get(key, key2, models, meta, context) - return [(id, name, clean_action(action, req.session)) + return [(id, name, clean_action(action, req.session, context=context)) for id, name, action in actions] -def clean_action(action, session): +def clean_action(action, session, context=None): action.setdefault('flags', {}) if action['type'] != 'ir.actions.act_window': return action @@ -401,7 +401,7 @@ def clean_action(action, session): if isinstance(action.get('context'), basestring): action['context'] = eval( action['context'], - session.evaluation_context()) or {} + session.evaluation_context(context=context)) or {} if isinstance(action.get('domain'), basestring): action['domain'] = eval( @@ -559,6 +559,7 @@ class DataSet(openerpweb.Controller): :rtype: list """ Model = request.session.model(model) + context, domain = eval_context_and_domain( request.session, request.context, domain) @@ -581,9 +582,14 @@ class DataSet(openerpweb.Controller): } + @openerpweb.jsonrequest + def read(self, request, model, ids, fields=False): + return self.do_search_read(request, model, ids, fields) + @openerpweb.jsonrequest def get(self, request, model, ids, fields=False): return self.do_get(request, model, ids, fields) + def do_get(self, request, model, ids, fields=False): """ Fetches and returns the records of the model ``model`` whose ids are in ``ids``. @@ -668,6 +674,12 @@ class DataSet(openerpweb.Controller): Model = req.session.model(model) return Model.default_get(fields, req.session.eval_context(req.context)) + @openerpweb.jsonrequest + def name_search(self, req, model, search_str, domain=[], context={}): + m = req.session.model(model) + r = m.name_search(search_str+'%', domain, '=ilike', context) + return {'result': r} + class DataGroup(openerpweb.Controller): _cp_path = "/base/group" @openerpweb.jsonrequest @@ -1001,6 +1013,19 @@ class Action(openerpweb.Controller): return clean_action(req.session.model('ir.actions.server').run( [action_id], req.session.eval_context(req.context)), req.session) +class TreeView(View): + _cp_path = "/base/treeview" + + @openerpweb.jsonrequest + def load(self, req, model, view_id, toolbar=False): + return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar) + + @openerpweb.jsonrequest + def action(self, req, model, id): + return load_actions_from_ir_values( + req,'action', 'tree_but_open',[(model, id)], + False, req.session.eval_context(req.context)) + def export_csv(fields, result): fp = StringIO() writer = csv.writer(fp, quoting=csv.QUOTE_ALL) @@ -1213,4 +1238,3 @@ class Export(View): return export_xls(field, result) else: return export_csv(field, result) - diff --git a/addons/base/static/lib/qweb/qweb2.js b/addons/base/static/lib/qweb/qweb2.js index 7430464e2d8..89a77bf1b85 100644 --- a/addons/base/static/lib/qweb/qweb2.js +++ b/addons/base/static/lib/qweb/qweb2.js @@ -1,5 +1,7 @@ // TODO: trim support // TODO: line number -> https://bugzilla.mozilla.org/show_bug.cgi?id=618650 +// TODO: templates orverwritten could be called by t-call="__super__" ? +// TODO: t-set + t-value + children node == scoped variable ? var QWeb2 = { expressions_cache: {}, reserved_words: 'true,false,NaN,null,undefined,debugger,in,instanceof,new,function,return,this,typeof,eval,Math,RegExp,Array,Object,Date'.split(','), @@ -21,6 +23,11 @@ var QWeb2 = { } throw new Error(prefix + ": " + message); }, + warning : function(message) { + if (typeof(window) !== 'undefined' && window.console) { + window.console.warn(message); + } + }, trim: function(s, mode) { switch (mode) { case "left": @@ -638,6 +645,9 @@ QWeb2.Element = (function() { compile_action_set : function(value) { var variable = this.format_expression(value); if (this.actions['value']) { + if (this.children.length) { + this.engine.tools.warning("@set with @value plus node chidren found. Children are ignored."); + } this.top(variable + " = (" + (this.format_expression(this.actions['value'])) + ");"); this.process_children = false; } else { diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 7f670be071f..a629e559d4c 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -269,7 +269,7 @@ label.error { border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px; - + color: #eee; text-shadow: #222 0 1px 0; text-decoration: none; @@ -926,8 +926,8 @@ label.error { background-color: #D2D2FF; } .openerp td.invalid input, .openerp td.invalid select, .openerp td.invalid textarea { - background-color: #F66; - border: 1px solid #D00; + background-color: #F66 !important; + border: 1px solid #D00 !important; } .openerp div.oe-progressbar span { position: absolute; @@ -1053,7 +1053,7 @@ label.error { padding-left: 10px; padding-right: 21px; height: 21px; - + background: #ffffff; /* Old browsers */ background: -moz-linear-gradient(top, #ffffff 0%, #ebe9e9 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(100%,#ebe9e9)); /* Chrome,Safari4+ */ @@ -1062,7 +1062,7 @@ background: -o-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* Opera11.10+ */ background: -ms-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* IE10+ */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#EBE9E9',GradientType=0 ); /* IE6-9 */ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ - + border: 1px solid #D2CFCF; border-right-width: 0; border-left-width: 0; @@ -1167,7 +1167,9 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ .openerp .oe-m2o-disabled-cm { color: grey; } - +.parent_top { + vertical-align: text-top; +} .openerp .oe-dialog-warning p { padding-left: 1em; font-size: 1.2em; @@ -1180,3 +1182,30 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */ -webkit-box-shadow: none; box-shadow: none; } + +.openerp .oe-treeview-table { + width: 100%; +} +.treeview-tr, .treeview-td { + cursor: pointer; + vertical-align: top; + text-align: left; +} +.treeview-tr span, .treeview-td span { + display: block; +} +.treeview-tr:first-of-type { + background: transparent url(/base/static/src/img/expand.gif) 0 50% no-repeat; +} +.oe-open .treeview-tr:first-of-type { + background-image: url(/base/static/src/img/collapse.gif); +} +.treeview-tr:first-of-type span, +.treeview-td:first-of-type span { + margin-left: 16px; +} + +.treeview-header { + text-align: left; + vertical-align: top; +} diff --git a/addons/base/static/src/js/boot.js b/addons/base/static/src/js/boot.js index 5754f5ccd14..7ac8a6bb1e0 100644 --- a/addons/base/static/src/js/boot.js +++ b/addons/base/static/src/js/boot.js @@ -60,7 +60,7 @@ openerp.base = function(instance) { openerp.base.core(instance); - openerp.base.dates(instance); + openerp.base.formats(instance); openerp.base.chrome(instance); openerp.base.data(instance); if (openerp.base.views) { diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index 994ce846319..822dc52b5c2 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -949,9 +949,6 @@ openerp.base.Homepage = openerp.base.Widget.extend({ openerp.base.Preferences = openerp.base.Widget.extend({ }); -openerp.base.ImportExport = openerp.base.Widget.extend({ -}); - openerp.base.WebClient = openerp.base.Widget.extend({ init: function(element_id) { this._super(null, element_id); @@ -981,7 +978,7 @@ openerp.base.WebClient = openerp.base.Widget.extend({ this.menu = new openerp.base.Menu(this, "oe_menu", "oe_secondary_menu"); this.menu.on_action.add(this.on_menu_action); - + }, start: function() { this.session.start(); @@ -991,8 +988,8 @@ openerp.base.WebClient = openerp.base.Widget.extend({ this.notification.notify("OpenERP Client", "The openerp client has been initialized."); }, on_logged: function() { - this.action_manager = new openerp.base.ActionManager(this, "oe_app"); - this.action_manager.start(); + this.action_manager = new openerp.base.ActionManager(this); + this.action_manager.appendTo($("#oe_app")); // if using saved actions, load the action and give it to action manager var parameters = jQuery.deparam(jQuery.param.querystring()); @@ -1032,7 +1029,8 @@ openerp.base.WebClient = openerp.base.Widget.extend({ self.execute_home_action(home_action[0], ds); }) }, - default_home: function () { }, + default_home: function () { + }, /** * Bundles the execution of the home action * diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index 90e5e907c8b..e488bb3dfa1 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -1648,7 +1648,7 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ }); this.views = views; - this.viewmanager = new openerp.base.ViewManager(this, this.element_id, this.dataset, views); + this.viewmanager = new openerp.base.ViewManager(this, this.dataset, views); this.viewmanager.registry = openerp.base.views.clone({ list: 'openerp.base.form.One2ManyListView', form: 'openerp.base.form.One2ManyFormView' @@ -1674,7 +1674,7 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({ self.save_form_view(); }); setTimeout(function () { - self.viewmanager.start(); + self.viewmanager.appendTo(this.$element); }, 0); }, reload_current_view: function() { diff --git a/addons/base/static/src/js/dates.js b/addons/base/static/src/js/formats.js similarity index 56% rename from addons/base/static/src/js/dates.js rename to addons/base/static/src/js/formats.js index 8daea39773f..3157997e82d 100644 --- a/addons/base/static/src/js/dates.js +++ b/addons/base/static/src/js/formats.js @@ -1,5 +1,5 @@ -openerp.base.dates = function(openerp) { +openerp.base.formats = function(openerp) { /** * Converts a string to a Date javascript object using OpenERP's @@ -133,5 +133,75 @@ openerp.base.format_time = function(obj) { return fts(obj.getHours(),2) + ":" + fts(obj.getMinutes(),2) + ":" + fts(obj.getSeconds(),2); }; + +/** + * Formats a single atomic value based on a field descriptor + * + * @param {Object} value read from OpenERP + * @param {Object} descriptor union of orm field and view field + * @param {Object} [descriptor.widget] widget to use to display the value + * @param {Object} descriptor.type fallback if no widget is provided, or if the provided widget is unknown + * @param {Object} [descriptor.digits] used for the formatting of floats + * @param {String} [value_if_empty=''] returned if the ``value`` argument is considered empty + */ +openerp.base.format_value = function (value, descriptor, value_if_empty) { + // If NaN value, display as with a `false` (empty cell) + if (typeof value === 'number' && isNaN(value)) { + value = false; + } + switch (value) { + case false: + case Infinity: + case -Infinity: + return value_if_empty === undefined ? '' : value_if_empty; + } + switch (descriptor.widget || descriptor.type) { + case 'integer': + return _.sprintf('%d', value); + case 'float': + var precision = descriptor.digits ? descriptor.digits[1] : 2; + return _.sprintf('%.' + precision + 'f', value); + case 'float_time': + return _.sprintf("%02d:%02d", + Math.floor(value), + Math.round((value % 1) * 60)); + case 'progressbar': + return _.sprintf( + '%.2f%%', + value, value); + case 'many2one': + // name_get value format + return value[1]; + default: + return value; + } +}; + +/** + * Formats a provided cell based on its field type + * + * @param {Object} row_data record whose values should be displayed in the cell + * @param {Object} column column descriptor + * @param {"button"|"field"} column.tag base control type + * @param {String} column.type widget type for a field control + * @param {String} [column.string] button label + * @param {String} [column.icon] button icon + * @param {String} [value_if_empty=''] what to display if the field's value is ``false`` + */ +openerp.base.format_cell = function (row_data, column, value_if_empty) { + var attrs = column.modifiers_for(row_data); + if (attrs.invisible) { return ''; } + if (column.tag === 'button') { + return [ + '' + ].join('') + } + + return openerp.base.format_value( + row_data[column.id].value, column, value_if_empty); +} }; diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index ea9cdaea1ee..11440a003b5 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -1,63 +1,5 @@ openerp.base.list = function (openerp) { openerp.base.views.add('list', 'openerp.base.ListView'); -openerp.base.list = { - /** - * Formats the rendring of a given value based on its field type - * - * @param {Object} row_data record whose values should be displayed in the cell - * @param {Object} column column descriptor - * @param {"button"|"field"} column.tag base control type - * @param {String} column.type widget type for a field control - * @param {String} [column.string] button label - * @param {String} [column.icon] button icon - * @param {String} [value_if_empty=''] what to display if the field's value is ``false`` - */ - render_cell: function (row_data, column, value_if_empty) { - var attrs = column.modifiers_for(row_data); - if (attrs.invisible) { return ''; } - if (column.tag === 'button') { - return [ - '' - ].join('') - } - - var value = row_data[column.id].value; - - // If NaN value, display as with a `false` (empty cell) - if (typeof value === 'number' && isNaN(value)) { - value = false; - } - switch (value) { - case false: - case Infinity: - case -Infinity: - return value_if_empty === undefined ? '' : value_if_empty; - } - switch (column.widget || column.type) { - case 'integer': - return _.sprintf('%d', value); - case 'float': - var precision = column.digits ? column.digits[1] : 2; - return _.sprintf('%.' + precision + 'f', value); - case 'float_time': - return _.sprintf("%02d:%02d", - Math.floor(value), - Math.round((value % 1) * 60)); - case 'progressbar': - return _.sprintf( - '%.2f%%', - value, value); - case 'many2one': - // name_get value format - return value[1]; - default: - return value; - } - } -}; openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListView# */ { defaults: { // records can be selected one by one @@ -637,7 +579,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi } $footer_cells.filter(_.sprintf('[data-field=%s]', column.id)) - .html(openerp.base.list.render_cell(aggregation, column)); + .html(openerp.base.format_cell(aggregation, column)); }); } // TODO: implement reorder (drag and drop rows) @@ -725,7 +667,7 @@ openerp.base.ListView.List = openerp.base.Class.extend( /** @lends openerp.base. this.$current = this.$_element.clone(true); this.$current.empty().append( QWeb.render('ListView.rows', _.extend({ - render_cell: openerp.base.list.render_cell}, this))); + render_cell: openerp.base.format_cell}, this))); }, /** * Gets the ids of all currently selected records, if any @@ -856,7 +798,7 @@ openerp.base.ListView.List = openerp.base.Class.extend( /** @lends openerp.base. row: this.rows[record_index], row_parity: (record_index % 2 === 0) ? 'even' : 'odd', row_index: record_index, - render_cell: openerp.base.list.render_cell + render_cell: openerp.base.format_cell }); }, /** @@ -1016,7 +958,7 @@ openerp.base.ListView.Groups = openerp.base.Class.extend( /** @lends openerp.bas row_data[group.grouped_on] = group; var group_column = _(self.columns).detect(function (column) { return column.id === group.grouped_on; }); - $group_column.html(openerp.base.list.render_cell( + $group_column.html(openerp.base.format_cell( row_data, group_column, "Undefined" )); if (group.openable) { diff --git a/addons/base/static/src/js/view_tree.js b/addons/base/static/src/js/view_tree.js index 90972f84973..05f58c077ff 100644 --- a/addons/base/static/src/js/view_tree.js +++ b/addons/base/static/src/js/view_tree.js @@ -5,22 +5,203 @@ openerp.base.view_tree = function(openerp) { openerp.base.views.add('tree', 'openerp.base.TreeView'); -openerp.base.TreeView = openerp.base.Widget.extend({ /** * Genuine tree view (the one displayed as a tree, not the list) */ +openerp.base.TreeView = openerp.base.View.extend({ + /** + * Indicates that this view is not searchable, and thus that no search + * view should be displayed (if there is one active). + */ + searchable : false, + + init: function(parent, element_id, dataset, view_id, options) { + this._super(parent, element_id); + this.dataset = dataset; + this.model = dataset.model; + this.view_id = view_id; + + this.records = {}; + + this.options = _.extend({}, this.defaults, options || {}); + }, + start: function () { this._super(); - this.$element.append('Tree view'); + return this.rpc("/base/treeview/load", { + model: this.model, + view_id: this.view_id, + toolbar: this.view_manager ? !!this.view_manager.sidebar : false + }, this.on_loaded); }, + /** + * Returns the list of fields needed to correctly read objects. + * + * Gathers the names of all fields in fields_view_get, and adds the + * field_parent (children_field in the tree view) if it's not already one + * of the fields to fetch + * + * @returns {Array} an array of fields which can be provided to DataSet.read_slice and others + */ + fields_list: function () { + var fields = _.keys(this.fields); + if (!_(fields).contains(this.children_field)) { + fields.push(this.children_field); + } + return fields; + }, + on_loaded: function (fields_view) { + var self = this; + var has_toolbar = !!fields_view.arch.attrs.toolbar; + // field name in OpenERP is kinda stupid: this is the name of the field + // holding the ids to the children of the current node, why call it + // field_parent? + this.children_field = fields_view['field_parent']; + this.fields_view = fields_view; + _(this.fields_view.arch.children).each(function (field) { + if (field.attrs.modifiers) { + field.attrs.modifiers = JSON.parse(field.attrs.modifiers); + } + }); + this.fields = fields_view.fields; + this.hook_row_click(); + this.$element.html(QWeb.render('TreeView', { + 'title': this.fields_view.arch.attrs.string, + 'fields_view': this.fields_view.arch.children, + 'fields': this.fields, + 'toolbar': has_toolbar + })); + + this.dataset.read_slice(this.fields_list(), 0, false, function (records) { + if (!has_toolbar) { + // WARNING: will do a second read on the same ids, but only on + // first load so not very important + self.getdata(null, _(records).pluck('id')); + return; + } + + var $select = self.$element.find('select') + .change(function () { + var $option = $(this).find(':selected'); + self.getdata($option.val(), $option.data('children')); + }); + _(records).each(function (record) { + self.records[record.id] = record; + $('