diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 36ea829a2f9..3a81ea01aea 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -611,20 +611,6 @@ class ListView(View): fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar) return {'fields_view': fields_view} - def fields_view_get(self, request, model, view_id, view_type="tree", - transform=True, toolbar=False, submenu=False): - """ Sets @editable on the view's arch if it isn't already set and - ``set_editable`` is present in the request context - """ - view = super(ListView, self).fields_view_get( - request, model, view_id, view_type, transform, toolbar, submenu) - - view_attributes = view['arch']['attrs'] - if request.context.get('set_editable')\ - and 'editable' not in view_attributes: - view_attributes['editable'] = 'bottom' - return view - def process_colors(self, view, row, context): colors = view['arch']['attrs'].get('colors') diff --git a/addons/base/static/src/base.html b/addons/base/static/src/base.html index 467bccb92ff..71a6a2aabf6 100644 --- a/addons/base/static/src/base.html +++ b/addons/base/static/src/base.html @@ -28,6 +28,7 @@ + diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 68cb3737025..d8ce605a78a 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -27,6 +27,10 @@ body.openerp { overflow-y: scroll; } +.openerp .oe-number { + text-align: right !important; +} + /* STATES */ .openerp .on_logged { display: none; @@ -602,6 +606,7 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, padding: 0; border: none; background: none; + width: 100%; } .openerp .oe-listview .oe-field-cell button:active { opacity: 0.5; diff --git a/addons/base/static/src/js/base.js b/addons/base/static/src/js/base.js index 6316715df89..2cab4460e5c 100644 --- a/addons/base/static/src/js/base.js +++ b/addons/base/static/src/js/base.js @@ -132,6 +132,7 @@ openerp.base = function(instance) { openerp.base.tree(instance); openerp.base.m2o(instance); openerp.base.form(instance); + openerp.base.list.editable(instance); }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index 0c446c48e0b..9ce0efa8d9f 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -149,6 +149,16 @@ openerp.base.Registry = Class.extend( /** @lends openerp.base.Registry# */ { add: function (key, object_path) { this.map[key] = object_path; return this; + }, + /** + * Creates and returns a copy of the current mapping, with the provided + * mapping argument added in (replacing existing keys if needed) + * + * @param {Object} [mapping={}] a mapping of keys to object-paths + */ + clone: function (mapping) { + return new openerp.base.Registry( + _.extend({}, this.map, mapping || {})); } }); diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index 6c0a3a354a4..3706b653651 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -259,7 +259,7 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. */ read_ids: function (ids, fields, callback) { var self = this; - this.rpc('/base/dataset/get', { + return this.rpc('/base/dataset/get', { model: this.model, ids: ids, fields: fields @@ -279,10 +279,10 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. */ read_index: function (fields, callback) { if (_.isEmpty(this.ids)) { - callback([]); + return $.Deferred().reject().promise(); } else { fields = fields || false; - this.read_ids([this.ids[this.index]], fields, function(records) { + return this.read_ids([this.ids[this.index]], fields, function(records) { callback(records[0]); }); } diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index 50b391c9b92..a7be25516b6 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -8,12 +8,15 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV * view should be displayed (if there is one active). */ searchable: false, + template: "FormView", /** * @constructs * @param {openerp.base.Session} session the current openerp session * @param {String} element_id this view's root element id * @param {openerp.base.DataSet} dataset the dataset this view will work with * @param {String} view_id the identifier of the OpenERP view object + * + * @property {openerp.base.Registry} registry=openerp.base.form.widgets widgets registry for this form view instance */ init: function(view_manager, session, element_id, dataset, view_id) { this._super(session, element_id); @@ -29,7 +32,8 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.ready = false; this.show_invalid = true; this.touched = false; - this.flags = this.view_manager.flags || {}; + this.flags = this.view_manager.action.flags || {}; + this.registry = openerp.base.form.widgets; }, start: function() { //this.log('Starting FormView '+this.model+this.view_id) @@ -44,9 +48,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV var self = this; this.fields_view = data.fields_view; - var frame = new openerp.base.form.WidgetFrame(this, this.fields_view.arch); + var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch); - this.$element.html(QWeb.render("FormView", { 'frame': frame, 'view': this })); + this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this })); _.each(this.widgets, function(w) { w.start(); }); @@ -220,7 +224,15 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV self.on_record_loaded(result.result); }); }, - do_save: function(success) { + /** + * Triggers saving the form's record. Chooses between creating a new + * record or saving an existing one depending on whether the record + * already has an id property. + * + * @param {Function} success callback on save success + * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end) + */ + do_save: function(success, prepend_on_create) { var self = this; if (!this.ready) { return false; @@ -243,7 +255,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV this.log("About to save", values); if (!this.datarecord.id) { this.dataset.create(values, function(r) { - self.on_created(r, success); + self.on_created(r, success, prepend_on_create); }); } else { this.dataset.write(this.datarecord.id, values, function(r) { @@ -281,19 +293,37 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV } } }, - on_created: function(r, success) { + /** + * Updates the form' dataset to contain the new record: + * + * * Adds the newly created record to the current dataset (at the end by + * default) + * * Selects that record (sets the dataset's index to point to the new + * record's id). + * * Updates the pager and sidebar displays + * + * @param {Object} r + * @param {Function} success callback to execute after having updated the dataset + * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end + */ + on_created: function(r, success, prepend_on_create) { if (!r.result) { this.notification.warn("Record not created", "Problem while creating record."); } else { - this.datarecord.id = arguments[0].result; - this.dataset.ids.push(this.datarecord.id); - this.dataset.index = this.dataset.ids.length - 1; + this.datarecord.id = r.result; + if (!prepend_on_create) { + this.dataset.ids.push(this.datarecord.id); + this.dataset.index = this.dataset.ids.length - 1; + } else { + this.dataset.ids.unshift(this.datarecord.id); + this.dataset.index = 0; + } this.dataset.count++; this.do_update_pager(); this.do_update_sidebar(); this.notification.notify("Record created", "The record has been created with id #" + this.datarecord.id); if (success) { - success(r); + success(_.extend(r, {created: true})); } } }, @@ -422,6 +452,7 @@ openerp.base.form.compute_domain = function(expr, fields) { }; openerp.base.form.Widget = openerp.base.Controller.extend({ + template: 'Widget', init: function(view, node) { this.view = view; this.node = node; @@ -435,7 +466,6 @@ openerp.base.form.Widget = openerp.base.Controller.extend({ this.view.widgets[this.element_id] = this; this.children = node.children; this.colspan = parseInt(node.attrs.colspan || 1); - this.template = "Widget"; this.string = this.string || node.attrs.string; this.help = this.help || node.attrs.help; @@ -460,9 +490,9 @@ openerp.base.form.Widget = openerp.base.Controller.extend({ }); openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ + template: 'WidgetFrame', init: function(view, node) { this._super(view, node); - this.template = "WidgetFrame"; this.columns = node.attrs.col || 4; this.x = 0; this.y = 0; @@ -504,9 +534,9 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ handle_node: function(node) { var type = this.view.fields_view.fields[node.attrs.name] || {}; var widget_type = node.attrs.widget || type.type || node.tag; - var widget = new (openerp.base.form.widgets.get_object(widget_type)) (this.view, node); + var widget = new (this.view.registry.get_object(widget_type)) (this.view, node); if (node.tag == 'field' && node.attrs.nolabel != '1') { - var label = new (openerp.base.form.widgets.get_object('label')) (this.view, node); + var label = new (this.view.registry.get_object('label')) (this.view, node); label["for"] = widget; this.add_widget(label); } @@ -1369,6 +1399,7 @@ openerp.base.form.FieldBinaryImage = openerp.base.form.FieldBinary.extend({ * Registry of form widgets, called by :js:`openerp.base.FormView` */ openerp.base.form.widgets = new openerp.base.Registry({ + 'frame' : 'openerp.base.form.WidgetFrame', 'group' : 'openerp.base.form.WidgetFrame', 'notebook' : 'openerp.base.form.WidgetNotebook', 'separator' : 'openerp.base.form.WidgetSeparator', diff --git a/addons/base/static/src/js/list-editable.js b/addons/base/static/src/js/list-editable.js new file mode 100644 index 00000000000..f7eb3e27bca --- /dev/null +++ b/addons/base/static/src/js/list-editable.js @@ -0,0 +1,260 @@ +/** + * @namespace handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out + */ +openerp.base.list.editable = function (openerp) { + var KEY_RETURN = 13, + KEY_ESCAPE = 27; + + // editability status of list rows + openerp.base.ListView.prototype.defaults.editable = null; + + var old_init = openerp.base.ListView.prototype.init, + old_actual_search = openerp.base.ListView.prototype.do_actual_search, + old_add_record = openerp.base.ListView.prototype.do_add_record, + old_on_loaded = openerp.base.ListView.prototype.on_loaded; + // TODO: not sure second @lends on existing item is correct, to check + _.extend(openerp.base.ListView.prototype, /** @lends openerp.base.ListView# */{ + init: function () { + var self = this; + old_init.apply(this, arguments); + $(this.groups).bind({ + 'edit': function (e, id, dataset) { + self.do_edit(dataset.index, id, dataset); + }, + 'saved': function () { + if (self.groups.get_selection().length) { + return; + } + self.compute_aggregates(); + } + }) + }, + /** + * Handles the activation of a record in editable mode (making a record + * editable), called *after* the record has become editable. + * + * The default behavior is to setup the listview's dataset to match + * whatever dataset was provided by the editing List + * + * @param {Number} index index of the record in the dataset + * @param {Object} id identifier of the record being edited + * @param {openerp.base.DataSet} dataset dataset in which the record is available + */ + do_edit: function (index, id, dataset) { + _.extend(this.dataset, dataset); + }, + /** + * Sets editability status for the list, based on defaults, view + * architecture and the provided flag, if any. + * + * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom". + */ + set_editable: function (force) { + // If ``force``, set editability to bottom + // otherwise rely on view default + // view' @editable is handled separately as we have not yet + // fetched and processed the view at this point. + this.options.editable = ( + (force && "bottom") + || this.defaults.editable); + }, + /** + * Replace do_actual_search to handle editability process + */ + do_actual_search: function (results) { + this.set_editable(results.context['set_editable']); + old_actual_search.call(this, results); + }, + /** + * Replace do_add_record to handle editability (and adding new record + * as an editable row at the top or bottom of the list) + */ + do_add_record: function () { + if (this.options.editable) { + this.groups.new_record(); + } else { + old_add_record.call(this); + } + }, + on_loaded: function (data, grouped) { + // tree/@editable takes priority on everything else if present. + this.options.editable = data.fields_view.arch.attrs.editable || this.options.editable; + return old_on_loaded.call(this, data, grouped); + } + }); + + _.extend(openerp.base.ListView.Groups.prototype, /** @lends openerp.base.ListView.Groups# */{ + passtrough_events: openerp.base.ListView.Groups.prototype.passtrough_events + " edit saved", + new_record: function () { + // TODO: handle multiple children + this.children[null].new_record(); + } + }); + + var old_list_row_clicked = openerp.base.ListView.List.prototype.row_clicked; + _.extend(openerp.base.ListView.List.prototype, /** @lends openerp.base.ListView.List */{ + row_clicked: function (event) { + if (!this.options.editable) { + return old_list_row_clicked.call(this, event); + } + this.edit_record(); + }, + /** + * Checks if a record is being edited, and if so cancels it + */ + cancel_pending_edition: function () { + if (!this.edition) { + return; + } + + if (this.edition_index !== null) { + this.reload_record(this.edition_index); + } + this.edition_form.stop(); + this.edition_form.$element.remove(); + delete this.edition_form; + delete this.edition_index; + delete this.edition; + }, + render_row_as_form: function (row) { + this.cancel_pending_edition(); + + var self = this; + var $new_row = $('', { + id: _.uniqueId('oe-editable-row-'), + 'class': $(row).attr('class'), + click: function (e) {e.stopPropagation();} + }) + .keyup(function (e) { + switch (e.which) { + case KEY_RETURN: + self.save_row(true); + break; + case KEY_ESCAPE: + self.cancel_edition(); + break; + default: + return; + } + }) + .delegate('button.oe-edit-row-save', 'click', function () { + self.save_row(); + }) + .delegate('button.oe-edit-row-cancel', 'click', function () { + self.cancel_edition(); + }); + if (row) { + $new_row.replaceAll(row); + } else if (this.options.editable === 'top') { + this.$current.prepend($new_row); + } else if (this.options.editable) { + this.$current.append($new_row); + } + this.edition = true; + this.edition_index = this.dataset.index; + this.edition_form = _.extend(new openerp.base.FormView( + null, this.group.view.session, $new_row.attr('id'), + this.dataset, false), { + template: 'ListView.row.form', + registry: openerp.base.list.form.widgets + }); + $.when(this.edition_form.on_loaded({fields_view: this.get_fields_view()})).then(function () { + // put in $.when just in case FormView.on_loaded becomes asynchronous + $new_row.find('td') + .addClass('oe-field-cell') + .removeAttr('width') + .end() + .find('td:first').removeClass('oe-field-cell').end() + .find('td:last').removeClass('oe-field-cell').end(); + // pad in case of groupby + _(self.columns).each(function (column) { + if (column.meta) { + $new_row.prepend(''); + } + }); + + self.edition_form.do_show(); + }); + }, + /** + * Saves the current row, and triggers the edition of its following + * sibling if asked. + * + * @param {Boolean} [edit_next=false] should the next row become editable + */ + save_row: function (edit_next) { + var self = this; + this.edition_form.do_save(function (result) { + self.reload_record(self.dataset.index, true).then(function () { + self.edition_form.stop(); + delete self.edition_form; + delete self.edition_index; + delete self.edition; + + $(self).trigger('saved', [self.dataset]); + if (!edit_next) { + return; + } + if (result.created) { + self.new_record(); + return; + } + self.dataset.next(); + self.edit_record(); + }); + }, this.options.editable === 'top'); + }, + /** + * Cancels the edition of the row for the current dataset index + */ + cancel_edition: function () { + this.cancel_pending_edition(); + }, + /** + * Edits record currently selected via dataset + */ + edit_record: function () { + this.render_row_as_form( + this.$current.children( + _.sprintf('[data-index=%d]', + this.dataset.index))); + $(this).trigger( + 'edit', + [this.rows[this.dataset.index].data.id.value, this.dataset]); + }, + new_record: function () { + this.dataset.index = null; + this.render_row_as_form(); + } + }); + openerp.base.list = {form: {}}; + openerp.base.list.form.WidgetFrame = openerp.base.form.WidgetFrame.extend({ + template: 'ListView.row.frame' + }); + var form_widgets = openerp.base.form.widgets; + openerp.base.list.form.widgets = form_widgets.clone({ + 'frame': 'openerp.base.list.form.WidgetFrame' + }); + // All form widgets inherit a problematic behavior from + // openerp.base.form.WidgetFrame: the cell itself is removed when invisible + // whether it's @invisible or @attrs[invisible]. In list view, only the + // former should completely remove the cell. We need to override update_dom + // on all widgets since we can't just hit on widget itself (I think) + var list_form_widgets = openerp.base.list.form.widgets; + _(list_form_widgets.map).each(function (widget_path, key) { + if (key === 'frame') { return; } + var new_path = 'openerp.base.list.form.' + key; + + openerp.base.list.form[key] = (form_widgets.get_object(key)).extend({ + update_dom: function () { + this.$element.children().css('visibility', ''); + if (this.invisible && this.node.attrs.invisible !== '1') { + this.$element.children().css('visibility', 'hidden'); + } else { + this._super(); + } + } + }); + list_form_widgets.add(key, new_path); + }); +}; diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 7643e2b62d4..b4528de5afd 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -83,8 +83,8 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi 'action': function (e, action_name, id, callback) { self.do_action(action_name, id, callback); }, - 'row_link': function (e, index, id, dataset) { - self.do_activate_record(index, id, dataset); + 'row_link': function (e, id, dataset) { + self.do_activate_record(dataset.index, id, dataset); } }); }, @@ -134,9 +134,11 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.$element.html(QWeb.render("ListView", this)); // Head hook - this.$element.find('#oe-list-add').click(this.do_add_record); + this.$element.find('#oe-list-add') + .click(this.do_add_record) + .attr('disabled', grouped && this.options.editable); this.$element.find('#oe-list-delete') - .hide() + .attr('disabled', true) .click(this.do_delete_selected); this.$element.find('thead').delegate('th[data-id]', 'click', function (e) { e.stopPropagation(); @@ -199,18 +201,24 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi return column.invisible !== '1'; }); - this.aggregate_columns = _(this.columns).chain() - .filter(function (column) { - return column['sum'] || column['avg'];}) + this.aggregate_columns = _(this.visible_columns) .map(function (column) { - var func = column['sum'] ? 'sum' : 'avg'; + if (column.type !== 'integer' && column.type !== 'float') { + return {}; + } + var aggregation_func = column['group_operator'] || 'sum'; + + if (!column[aggregation_func]) { + return {}; + } + return { field: column.id, type: column.type, - 'function': func, - label: column[func] + 'function': aggregation_func, + label: column[aggregation_func] }; - }).value(); + }); }, /** * Used to handle a click on a table row, if no other handler caught the @@ -267,6 +275,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi return this.rpc('/base/listview/load', { model: this.model, view_id: this.view_id, + context: this.dataset.context, toolbar: !!this.flags.sidebar }, callback); } @@ -289,25 +298,32 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @returns {$.Deferred} fold request evaluation promise */ do_search: function (domains, contexts, groupbys) { - var self = this; return this.rpc('/base/session/eval_domain_and_context', { domains: domains, contexts: contexts, group_by_seq: groupbys - }, function (results) { - self.dataset.context = results.context; - self.dataset.domain = results.domain; - self.groups.datagroup = new openerp.base.DataGroup( - self.session, self.model, - results.domain, results.context, - results.group_by); + }, $.proxy(this, 'do_actual_search')); + }, + /** + * Handler for the result of eval_domain_and_context, actually perform the + * searching + * + * @param {Object} results results of evaluating domain and process for a search + */ + do_actual_search: function (results) { + this.dataset.context = results.context; + this.dataset.domain = results.domain; + this.groups.datagroup = new openerp.base.DataGroup( + this.session, this.model, + results.domain, results.context, + results.group_by); - if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) { - results.group_by = null; - } - self.reload_view(!!results.group_by).then( - $.proxy(self, 'reload_content')); - }); + if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) { + results.group_by = null; + } + + this.reload_view(!!results.group_by).then( + $.proxy(this, 'reload_content')); }, /** * Handles the signal to delete a line from the DOM @@ -349,13 +365,15 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi */ do_select: function (ids, records) { this.$element.find('#oe-list-delete') - .toggle(!!ids.length); + .attr('disabled', !ids.length); if (!records.length) { this.compute_aggregates(); return; } - this.compute_aggregates(records); + this.compute_aggregates(_(records).map(function (record) { + return {count: 1, values: record}; + })); }, /** * Handles action button signals on a record @@ -382,7 +400,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * * @param {Number} index index of the record in the dataset * @param {Object} id identifier of the activated record - * @param {openobject.base.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups) + * @param {openerp.base.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups) */ do_activate_record: function (index, id, dataset) { var self = this; @@ -420,75 +438,56 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @param {Array} [records] */ compute_aggregates: function (records) { - if (_.isEmpty(this.aggregate_columns)) { - return; - } + var columns = _(this.aggregate_columns).filter(function (column) { + return column['function']; }); + + if (_.isEmpty(columns)) { return; } + if (_.isEmpty(records)) { records = this.groups.get_records(); } - var aggregator = this.build_aggregator(this.aggregate_columns); - this.display_aggregates( - _(records).reduce(aggregator, aggregator).value()); - }, - /** - * Creates a stateful callable aggregator object, which can be reduced over - * a collection of records in order to build the aggregations described - * by the parameter - * - * @param {Array} aggregation_descriptors - */ - build_aggregator: function (aggregation_descriptors) { - var values = {}; - var descriptors = {}; - _(aggregation_descriptors).each(function (descriptor) { - values[descriptor.field] = []; - descriptors[descriptor.field] = descriptor; - }); - - var aggregator = function (_i, record) { - _(values).each(function (collection, key) { - collection.push(record[key]); - }); - - return aggregator; - }; - aggregator.value = function () { - var result = {}; - - _(values).each(function (collection, key) { - var value; - switch(descriptors[key]['function']) { - case 'avg': - value = (_(collection).chain() - .filter(function (item) { - return !_.isUndefined(item); }) - .reduce(function (total, item) { - return total + item; }, 0).value() - / collection.length); - break; + var count = 0, sums = {}; + _(columns).each(function (column) { sums[column.field] = 0; }); + _(records).each(function (record) { + count += record.count || 1; + _(columns).each(function (column) { + var field = column.field; + switch (column['function']) { case 'sum': - value = (_(collection).chain() - .filter(function (item) { - return !_.isUndefined(item); }) - .reduce(function (total, item) { - return total + item; }, 0).value()); + sums[field] += record.values[field]; + break; + case 'avg': + sums[field] += record.count * record.values[field]; break; } - result[key] = value; }); + }); - return result; - }; - return aggregator; + var aggregates = {}; + _(columns).each(function (column) { + var field = column.field; + switch (column['function']) { + case 'sum': + aggregates[field] = sums[field]; + break; + case 'avg': + aggregates[field] = sums[field] / count; + break; + } + }); + + this.display_aggregates(aggregates); }, display_aggregates: function (aggregation) { - var $footer = this.$element.find('.oe-list-footer').empty(); + var $footer_cells = this.$element.find('.oe-list-footer'); _(this.aggregate_columns).each(function (column) { - $(_.sprintf( - "%s: %.2f", - column.label, aggregation[column.field])) - .appendTo($footer); + if (!column['function']) { + return; + } + var pattern = (column.type == 'integer') ? '%d' : '%.2f'; + $footer_cells.filter(_.sprintf('[data-field=%s]', column.field)) + .text(_.sprintf(pattern, aggregation[column.field])); }); } // TODO: implement reorder (drag and drop rows) @@ -521,8 +520,9 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List * @constructs * @param {Object} opts display options, identical to those of :js:class:`openerp.base.ListView` */ - init: function (opts) { + init: function (group, opts) { var self = this; + this.group = group; this.options = opts.options; this.columns = opts.columns; @@ -546,19 +546,26 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List e.stopPropagation(); var $target = $(e.currentTarget), field = $target.closest('td').data('field'), - record_id = self.row_id($target.closest('tr')); + $row = $target.closest('tr'), + record_id = self.row_id($row), + index = self.row_position($row); - $(self).trigger('action', [field, record_id]); + $(self).trigger('action', [field, record_id, function () { + self.reload_record(index, true); + }]); }) .delegate('tr', 'click', function (e) { e.stopPropagation(); - $(self).trigger( - 'row_link', - [self.row_position(e.currentTarget), - self.row_id(e.currentTarget), - self.dataset]); + self.dataset.index = self.row_position(e.currentTarget); + self.row_clicked(e); }); }, + row_clicked: function () { + $(this).trigger( + 'row_link', + [this.rows[this.dataset.index].data.id.value, + this.dataset]); + }, render: function () { if (this.$current) { this.$current.remove(); @@ -566,6 +573,18 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List this.$current = this.$_element.clone(true); this.$current.empty().append($(QWeb.render('ListView.rows', this))); }, + get_fields_view: function () { + // deep copy of view + var view = $.extend(true, {}, this.group.view.fields_view); + _(view.arch.children).each(function (widget) { + widget.attrs.nolabel = true; + if (widget.tag === 'button') { + delete widget.attrs.string; + } + }); + view.arch.attrs.col = 2 * view.arch.children.length; + return view; + }, /** * Gets the ids of all currently selected records, if any * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves. @@ -579,7 +598,7 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List this.$current.find('th.oe-record-selector input:checked') .closest('tr').each(function () { var record = {}; - _(rows[$(this).prevAll().length].data).each(function (obj, key) { + _(rows[$(this).data('index')].data).each(function (obj, key) { record[key] = obj.value; }); result.ids.push(record.id); @@ -594,7 +613,7 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List * @returns {Number} the position of the row in this.rows */ row_position: function (row) { - return $(row).prevAll().length; + return $(row).data('index'); }, /** * Returns the identifier of the object displayed in the provided table @@ -621,13 +640,86 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List _(row.data).each(function (obj, key) { record[key] = obj.value; }); - return record; + return {count: 1, values: record}; + }); + }, + /** + * Transforms a record from what is returned by a dataset read (a simple + * mapping of ``$fieldname: $value``) to the format expected by list rows + * and form views: + * + * data: { + * $fieldname: { + * value: $value + * } + * } + * + * This format allows for the insertion of a bunch of metadata (names, + * colors, etc...) + * + * @param {Object} record original record, in dataset format + * @returns {Object} record displayable in a form or list view + */ + transform_record: function (record) { + // TODO: colors handling + var form_data = {}, + form_record = {data: form_data}; + + _(record).each(function (value, key) { + form_data[key] = {value: value}; + }); + + return form_record; + }, + /** + * Reloads the record at index ``row_index`` in the list's rows. + * + * By default, simply re-renders the record. If the ``fetch`` parameter is + * provided and ``true``, will first fetch the record anew. + * + * @param {Number} record_index index of the record to reload + * @param {Boolean} fetch fetches the record from remote before reloading it + */ + reload_record: function (record_index, fetch) { + var self = this; + var read_p = null; + if (fetch) { + // save index to restore it later, if already set + var old_index = this.dataset.index; + this.dataset.index = record_index; + read_p = this.dataset.read_index( + _.filter(_.pluck(this.columns, 'name'), _.identity), + function (record) { + var form_record = self.transform_record(record); + self.rows.splice(record_index, 1, form_record); + self.dataset.index = old_index; + } + ) + } + + return $.when(read_p).then(function () { + self.$current.children().eq(record_index) + .replaceWith(self.render_record(record_index)); }) + }, + /** + * Renders a list record to HTML + * + * @param {Number} record_index index of the record to render in ``this.rows`` + * @returns {String} QWeb rendering of the selected record + */ + render_record: function (record_index) { + return QWeb.render('ListView.row', { + columns: this.columns, + options: this.options, + row: this.rows[record_index], + row_parity: (record_index % 2 === 0) ? 'even' : 'odd', + row_index: record_index }); } // drag and drop - // editable? }); openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Groups# */{ + passtrough_events: 'action deleted row_link', /** * Grouped display for the ListView. Handles basic DOM events and interacts * with the :js:class:`~openerp.base.DataGroup` bound to it. @@ -679,24 +771,11 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr } return red_letter_tboday; }, - open_group: function (e, group) { - var row = e.currentTarget; - - if (this.children[group.value]) { - this.children[group.value].apoptosis(); - delete this.children[group.value]; - } - var prospekt = this.children[group.value] = new openerp.base.ListView.Groups(this.view, { - options: this.options, - columns: this.columns - }); - this.bind_child_events(prospekt); - prospekt.datagroup = group; - prospekt.render().insertAfter( - this.point_insertion(row)); - $(row).find('span.ui-icon') - .removeClass('ui-icon-triangle-1-e') - .addClass('ui-icon-triangle-1-s'); + open: function (point_insertion) { + this.render().insertAfter(point_insertion); + }, + close: function () { + this.apoptosis(); }, /** * Prefixes ``$node`` with floated spaces in order to indent it relative @@ -716,18 +795,32 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr var self = this; var placeholder = this.make_fragment(); _(datagroups).each(function (group) { + if (self.children[group.value]) { + self.children[group.value].apoptosis(); + delete self.children[group.value]; + } + var child = self.children[group.value] = new openerp.base.ListView.Groups(self.view, { + options: self.options, + columns: self.columns + }); + self.bind_child_events(child); + child.datagroup = group; + var $row = $(''); if (group.openable) { $row.click(function (e) { if (!$row.data('open')) { - $row.data('open', true); - self.open_group(e, group); + $row.data('open', true) + .find('span.ui-icon') + .removeClass('ui-icon-triangle-1-e') + .addClass('ui-icon-triangle-1-s'); + child.open(self.point_insertion(e.currentTarget)); } else { $row.removeData('open') .find('span.ui-icon') .removeClass('ui-icon-triangle-1-s') .addClass('ui-icon-triangle-1-e'); - _(self.children).each(function (child) {child.apoptosis();}); + child.close(); } }); } @@ -779,21 +872,7 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr // can have selections spanning multiple links var selection = self.get_selection(); $this.trigger(e, [selection.ids, selection.records]); - }).bind('action', function (e, name, id, callback) { - if (!callback) { - callback = function () { - var $prev = child.$current.prev(); - if (!$prev.is('tbody')) { - // ungrouped - $(self.elements[0]).replaceWith(self.render()); - } else { - // ghetto reload child (and its siblings) - $prev.children().last().click(); - } - }; - } - $this.trigger(e, [name, id, callback]); - }).bind('deleted row_link', function (e) { + }).bind(this.passtrough_events, function (e) { // additional positional parameters are provided to trigger as an // Array, following the event type or event object, but are // provided to the .bind event handler as *args. @@ -806,7 +885,7 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr }, render_dataset: function (dataset) { var rows = [], - list = new openerp.base.ListView.List({ + list = new openerp.base.ListView.List(this, { options: this.options, columns: this.columns, dataset: dataset, @@ -819,17 +898,8 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr _.filter(_.pluck(this.columns, 'name'), _.identity), 0, false, function (records) { - var form_records = _(records).map(function (record) { - // TODO: colors handling - var form_data = {}, - form_record = {data: form_data}; - - _(record).each(function (value, key) { - form_data[key] = {value: value}; - }); - - return form_record; - }); + var form_records = _(records).map( + $.proxy(list, 'transform_record')); rows.splice(0, rows.length); rows.push.apply(rows, form_records); @@ -881,6 +951,12 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr return this; }, get_records: function () { + if (_(this.children).isEmpty()) { + return { + count: this.datagroup.length, + values: this.datagroup.aggregates + } + } return _(this.children).chain() .map(function (child) { return child.get_records(); diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index 4c259babd8f..ecd16d77d94 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -9,7 +9,7 @@ openerp.base.ActionManager = openerp.base.Controller.extend({ init: function(session, element_id) { this._super(session, element_id); this.viewmanager = null; - this.dialog_stack = [] + this.dialog_stack = []; // Temporary linking view_manager to session. // Will use controller_parent to find it when implementation will be done. session.action_manager = this; @@ -33,8 +33,7 @@ openerp.base.ActionManager = openerp.base.Controller.extend({ case 'ir.actions.act_window': if (action.target == 'new') { var element_id = _.uniqueId("act_window_dialog"); - var dialog = $('
'); - dialog.dialog({ + $('
', {id: element_id}).dialog({ title: action.name, modal: true, width: '50%', @@ -347,7 +346,7 @@ openerp.base.Sidebar = openerp.base.BaseWidget.extend({ var action = self.sections[index[0]].elements[index[1]]; action.flags = { new_window : true - } + }; self.session.action_manager.do_action(action); e.stopPropagation(); e.preventDefault(); diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 44dcc3d9830..8a4d3a4ed38 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -300,11 +300,13 @@ - + - - + + + @@ -316,7 +318,8 @@ - + @@ -326,8 +329,10 @@ + + t-att-class="'oe-field-cell' + (align ? ' oe-number' : '')" + t-att-data-field="column.id"> @@ -348,6 +353,9 @@ + + +

@@ -882,4 +890,23 @@ + + + + $(document.createElement('t')) + .append(this.contents()) + .attr({ + 't-foreach': this.attr('t-foreach'), + 't-as': this.attr('t-as') + }) + .replaceAll(this) + .after($(document.createElement('td')).append( + $(document.createElement('button')).attr({ + 'class': 'oe-edit-row-save', 'type': 'button'}).text('Save'))) + .before($(document.createElement('td')).append( + $(document.createElement('button')).attr({ + 'class': 'oe-edit-row-cancel', 'type': 'button'}).text('Cancel'))) + .unwrap(); + +