diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 3462efd656e..70feda44bc0 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -585,56 +585,6 @@ class ListView(View): view_attributes['editable'] = 'bottom' return view - @openerpweb.jsonrequest - def fill(self, request, model, id, domain, - offset=0, limit=False, sort=None): - return self.do_fill(request, model, id, domain, offset, limit, sort) - - def do_fill(self, request, model, id, domain, - offset=0, limit=False, sort=None): - """ Returns all information needed to fill a table: - - * view with processed ``editable`` flag - * fields (columns) with processed ``invisible`` flag - * rows with processed ``attrs`` and ``colors`` - - .. note:: context is passed through ``request`` parameter - - :param request: OpenERP request - :type request: openerpweb.openerpweb.JsonRequest - :type str model: OpenERP model for this list view - :type int id: view_id, or False if none provided - :param list domain: the search domain to search for - :param int offset: search offset, for pagination - :param int limit: search limit, for pagination - :returns: hell if I have any idea yet - """ - view = self.fields_view_get(request, model, id, toolbar=True) - - rows = DataSet().do_search_read(request, model, - offset=offset, limit=limit, - domain=domain, sort=sort) - eval_context = request.session.evaluation_context( - request.context) - - if sort: - sort_criteria = sort.split(',')[0].split(' ') - view['sorted'] = { - 'field': sort_criteria[0], - 'reversed': sort_criteria[1] == 'DESC' - } - else: - view['sorted'] = {} - return { - 'view': view, - 'records': [ - {'data': dict((key, {'value': value}) - for key, value in row.iteritems()), - 'color': self.process_colors(view, row, eval_context)} - for row in rows - ] - } - def process_colors(self, view, row, context): colors = view['arch']['attrs'].get('colors') diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 38fbfb115ef..0ab68967db9 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -552,6 +552,17 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, text-align: right; } +.openerp .oe-listview tfoot td { + padding: 3px 3px 0; +} +.openerp .oe-listview .oe-list-footer { + text-align: center; + white-space: nowrap; +} +.openerp .oe-listview .oe-list-footer span { + margin: 0 1em; +} + /** list rounded corners rounded corners are a pain on tables: need to round not only table, but @@ -572,11 +583,13 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, -moz-border-radius-topright: 7px; border-top-right-radius: 7px; } +.openerp .oe-listview table tfoot td:first-child, .openerp .oe-listview table tbody:last-child tr:last-child th:first-child { -webkit-border-bottom-left-radius: 7px; -moz-border-radius-bottomleft: 7px; border-bottom-left-radius: 7px; } +.openerp .oe-listview table tfoot td:last-child, .openerp .oe-listview table tbody:last-child tr:last-child td:last-child { -webkit-border-bottom-right-radius: 7px; -moz-border-radius-bottomright: 7px; diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index 1d2fab9b9bc..60bffc8bff3 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -104,7 +104,7 @@ openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( || key === field_name + '_count') { return; } - aggregates[key] = value; + aggregates[key] = value || 0; }); return { diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index 89f3a91f676..3374050fa17 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -1080,7 +1080,7 @@ openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({ }, check_load: function() { if(this.is_started && this.is_setted) { - this.list_view.do_reload(); + this.list_view.reload_view(); } } }); @@ -1090,11 +1090,11 @@ openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ this.dataset.ids = _.without.apply(null, [this.dataset.ids].concat(ids)); this.dataset.count = this.dataset.ids.length; // there may be a faster way - this.do_reload(); + this.reload_view(); this.m2m_field.on_ui_change(); }, - do_reload: function () { + reload_view: function () { /* Dear xmo, according to your comments, this method's implementation in list view seems * to be a little bit bullshit. * I assume the list view will be changed later, so I hope it will support static datasets. @@ -1115,7 +1115,7 @@ openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) { self.dataset.ids.push(element_id); self.dataset.count = self.dataset.ids.length; - self.do_reload(); + self.reload_view(); } pop.stop(); }); @@ -1188,7 +1188,7 @@ openerp.base.form.Many2XSelectPopup = openerp.base.BaseWidget.extend({ var tmphack = {"loaded": false}; self.view_list.on_loaded.add_last(function() { if ( !tmphack.loaded ) { - self.view_list.do_reload(); + self.view_list.reload_view(); tmphack.loaded = true; }; }); diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 77a66731f34..3a482164bb6 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -44,7 +44,6 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @borrows openerp.base.ActionExecutor#execute_action as #execute_action */ init: function(view_manager, session, element_id, dataset, view_id, options) { - var self = this; this._super(session, element_id); this.view_manager = view_manager || new openerp.base.NullViewManager(); this.dataset = dataset; @@ -56,41 +55,35 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.options = _.extend({}, this.defaults, options || {}); this.flags = this.view_manager.action.flags; - this.groups = new openerp.base.ListView.Groups(this, { - options: this.options, - columns: this.columns - }); + this.set_groups(new openerp.base.ListView.Groups(this)); + }, + /** + * Set a custom Group construct as the root of the List View. + * + * @param {openerp.base.ListView.Groups} groups + */ + set_groups: function (groups) { + var self = this; + if (this.groups) { + $(this.groups).unbind("selected deleted action row_link"); + delete this.groups; + } + + this.groups = groups; $(this.groups).bind({ - 'selected': function (e, selection) { - self.$element.find('#oe-list-delete') - .toggle(!!selection.length); + 'selected': function (e, ids, records) { + self.do_select(ids, records); }, 'deleted': function (e, ids) { self.do_delete(ids); }, 'action': function (e, action_name, id, callback) { - var action = _.detect(self.columns, function (field) { - return field.name === action_name; - }); - if (!action) { return; } - self.execute_action( - action, self.dataset, self.session.action_manager, - id, function () { - if (callback) { - callback(); - } - }); + self.do_action(action_name, id, callback); }, 'row_link': function (e, index, id, dataset) { - _.extend(self.dataset, { - domain: dataset.domain, - context: dataset.context - }).read_slice([], null, null, function () { - self.select_record(index); - }); + self.do_activate_record(index, id, dataset); } }); - }, /** * View startup method, the default behavior is to set the ``oe-listview`` @@ -100,11 +93,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi */ start: function() { this.$element.addClass('oe-listview'); - return this.rpc("/base/listview/load", { - model: this.model, - view_id: this.view_id, - toolbar: !!this.flags.sidebar - }, this.on_loaded); + return this.reload_view(); }, /** * Called after loading the list view's description, sets up such things @@ -127,15 +116,15 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @param {Object} data.fields_view fields_view_get result (processed) * @param {Object} data.fields_view.fields mapping of fields for the current model * @param {Object} data.fields_view.arch current list view descriptor - * @param {Array} columns columns to move to the front (and make visible) + * @param {Boolean} grouped Is the list view grouped */ - on_loaded: function(data, columns) { + on_loaded: function(data, grouped) { var self = this; this.fields_view = data.fields_view; //this.log(this.fields_view); this.name = "" + this.fields_view.arch.attrs.string; - this.setup_columns(this.fields_view.fields, columns); + this.setup_columns(this.fields_view.fields, grouped); if (!this.fields_view.sorted) { this.fields_view.sorted = {}; } @@ -152,7 +141,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi self.dataset.sort($(this).data('id')); // TODO: should only reload content (and set the right column to a sorted display state) - self.do_reload(); + self.reload_view(); }); this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar); @@ -163,9 +152,9 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * visible. * * @param {Object} fields fields_view_get's fields section - * @param {Array} groupby_columns columns the ListView is grouped by + * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed */ - setup_columns: function (fields, groupby_columns) { + setup_columns: function (fields, grouped) { var domain_computer = openerp.base.form.compute_domain; var noop = function () { return {}; }; @@ -193,7 +182,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.columns.push.apply( this.columns, _(this.fields_view.arch.children).map(field_to_column)); - if (groupby_columns) { + if (grouped) { this.columns.unshift({ id: '_group', tag: '', string: "Group", meta: true, attrs_for: function () { return {}; } @@ -206,6 +195,19 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.visible_columns = _.filter(this.columns, function (column) { return column.invisible !== '1'; }); + + this.aggregate_columns = _(this.columns).chain() + .filter(function (column) { + return column['sum'] || column['avg'];}) + .map(function (column) { + var func = column['sum'] ? 'sum' : 'avg'; + return { + field: column.id, + type: column.type, + 'function': func, + label: column[func] + }; + }).value(); }, /** * Used to handle a click on a table row, if no other handler caught the @@ -245,26 +247,20 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.hidden = true; }, /** - * Reloads the search view based on the current settings (dataset & al) + * Reloads the list view based on the current settings (dataset & al) * - * @param {Array} [primary_columns] columns to bring to the front of the - * sequence + * @param {Boolean} [grouped] Should the list be displayed grouped */ - do_reload: function (primary_columns) { - // TODO: should just fields_view_get I think + reload_view: function (grouped) { var self = this; this.dataset.offset = 0; this.dataset.limit = false; - return this.rpc('/base/listview/fill', { - 'model': this.dataset.model, - 'id': this.view_id, - 'context': this.dataset.context, - 'domain': this.dataset.domain, - 'sort': this.dataset.sort && this.dataset.sort() - }, function (result) { - if (result.view) { - self.on_loaded({fields_view: result.view}, primary_columns); - } + return this.rpc('/base/listview/load', { + model: this.model, + view_id: this.view_id, + toolbar: !!this.flags.sidebar + }, function (field_view_get) { + self.on_loaded(field_view_get, grouped); }); }, /** @@ -286,16 +282,17 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi self.dataset.context = results.context; self.dataset.domain = results.domain; self.groups.datagroup = new openerp.base.DataGroup( - self.session, self.dataset.model, + self.session, self.model, results.domain, results.context, results.group_by); if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) { results.group_by = null; } - self.do_reload(results.group_by).then(function () { - self.$element.find('table').append(self.groups.render()); - }); + self.reload_view(!!results.group_by).then(function () { + self.$element.find('table').append( + self.groups.render(function () { + self.compute_aggregates();}));}); }); }, /** @@ -330,6 +327,58 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi // TODO only refresh modified rows }); }, + /** + * Handles the signal indicating that a new record has been selected + * + * @param {Array} ids selected record ids + * @param {Array} records selected record values + */ + do_select: function (ids, records) { + this.$element.find('#oe-list-delete') + .toggle(!!ids.length); + + if (!records.length) { + this.compute_aggregates(); + return; + } + this.compute_aggregates(records); + }, + /** + * Handles action button signals on a record + * + * @param {String} name action name + * @param {Object} id id of the record the action should be called on + * @param {Function} callback should be called after the action is executed, if non-null + */ + do_action: function (name, id, callback) { + var action = _.detect(this.columns, function (field) { + return field.name === name; + }); + if (!action) { return; } + this.execute_action( + action, this.dataset, this.session.action_manager, + id, function () { + if (callback) { + callback(); + } + }); + }, + /** + * Handles the activation of a record (clicking on it) + * + * @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) + */ + do_activate_record: function (index, id, dataset) { + var self = this; + _.extend(this.dataset, { + domain: dataset.domain, + context: dataset.context + }).read_slice([], 0, false, function () { + self.select_record(index); + }); + }, /** * Handles signal for the addition of a new record (can be a creation, * can be the addition from a remote source, ...) @@ -337,14 +386,96 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * The default implementation is to switch to a new record on the form view */ do_add_record: function () { - this.notification.notify('Add', "New record"); this.select_record(null); }, /** * Handles deletion of all selected lines */ do_delete_selected: function () { - this.do_delete(this.groups.get_selection()); + this.do_delete(this.groups.get_selection().ids); + }, + /** + * Computes the aggregates for the current list view, either on the + * records provided or on the records of the internal + * :js:class:`~openerp.base.ListView.Group`, by calling + * :js:func:`~openerp.base.ListView.group.get_records`. + * + * Then displays the aggregates in the table through + * :js:method:`~openerp.base.ListView.display_aggregates`. + * + * @param {Array} [records] + */ + compute_aggregates: function (records) { + if (_.isEmpty(this.aggregate_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; + case 'sum': + value = (_(collection).chain() + .filter(function (item) { + return !_.isUndefined(item); }) + .reduce(function (total, item) { + return total + item; }, 0).value()); + break; + } + result[key] = value; + }); + + return result; + }; + return aggregator; + }, + display_aggregates: function (aggregation) { + var $footer = this.$element.find('.oe-list-footer').empty(); + _(this.aggregate_columns).each(function (column) { + $(_.sprintf( + "%s: %.2f", + column.label, aggregation[column.field])) + .appendTo($footer); + }); } // TODO: implement reorder (drag and drop rows) }); @@ -378,7 +509,6 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List */ init: function (opts) { var self = this; - // columns, rows, options this.options = opts.options; this.columns = opts.columns; @@ -389,7 +519,9 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List .appendTo(document.body) .delegate('th.oe-record-selector', 'click', function (e) { e.stopPropagation(); - $(self).trigger('selected', [self.get_selection()]); + var selection = self.get_selection(); + $(self).trigger( + 'selected', [selection.ids, selection.records]); }) .delegate('td.oe-record-delete button', 'click', function (e) { e.stopPropagation(); @@ -422,17 +554,24 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List }, /** * Gets the ids of all currently selected records, if any - * @returns {Array} empty if no record is selected (or the list view is not selectable) + * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves. */ get_selection: function () { if (!this.options.selectable) { return []; } var rows = this.rows; - return this.$current.find('th.oe-record-selector input:checked') - .closest('tr').map(function () { - return rows[$(this).prevAll().length].data.id.value; - }).get(); + var result = {ids: [], records: []}; + this.$current.find('th.oe-record-selector input:checked') + .closest('tr').each(function () { + var record = {}; + _(rows[$(this).prevAll().length].data).each(function (obj, key) { + record[key] = obj.value; + }); + result.ids.push(record.id); + result.records.push(record); + }); + return result; }, /** * Returns the index of the row in the list of rows. @@ -460,6 +599,16 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List if (!this.$current) { return; } this.$current.remove(); this.$current = null; + this.$_element.remove(); + }, + get_records: function () { + return _(this.rows).map(function (row) { + var record = {}; + _(row.data).each(function (obj, key) { + record[key] = obj.value; + }); + return record; + }); } // drag and drop // editable? @@ -472,11 +621,11 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr * Provides events similar to those of * :js:class:`~openerp.base.ListView.List` */ - init: function (view, opts) { + init: function (view) { this.view = view; - this.options = opts.options; - this.columns = opts.columns; - this.datagroup = {}; + this.options = view.options; + this.columns = view.columns; + this.datagroup = null; this.sections = []; this.children = {}; @@ -502,19 +651,19 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr */ point_insertion: function (row) { var $row = $(row); - var red_letter_tbody = $row.closest('tbody')[0]; + var red_letter_tboday = $row.closest('tbody')[0]; var $next_siblings = $row.nextAll(); if ($next_siblings.length) { - var $root_kanal = $('
').insertAfter(red_letter_tbody); + var $root_kanal = $('').insertAfter(red_letter_tboday); $root_kanal.append($next_siblings); this.elements.splice( - _.indexOf(this.elements, red_letter_tbody), + _.indexOf(this.elements, red_letter_tboday), 0, $root_kanal[0]); } - return red_letter_tbody; + return red_letter_tboday; }, open_group: function (e, group) { var row = e.currentTarget; @@ -570,7 +719,6 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr } placeholder.appendChild($row[0]); - var $group_column = $('