[MERGE] fixes in account_followup
[odoo/odoo.git] / addons / web / static / src / js / view_list.js
index e1367fb..b452dc8 100644 (file)
@@ -1,6 +1,6 @@
 openerp.web.list = function (instance) {
 var _t = instance.web._t,
-   _lt = instance.web._lt;
+    _lt = instance.web._lt;
 var QWeb = instance.web.qweb;
 instance.web.views.add('list', 'instance.web.ListView');
 instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListView# */ {
@@ -21,8 +21,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
         // whether the view rows can be reordered (via vertical drag & drop)
         'reorderable': true,
         'action_buttons': true,
-        // if true, the 'Import', 'Export', etc... buttons will be shown
-        'import_enabled': true,
+        //whether the editable property of the view has to be disabled
+        'disable_editable_mode': false,
+    },
+    view_type: 'tree',
+    events: {
+        'click thead th.oe_sortable[data-id]': 'sort_by_column'
     },
     /**
      * Core class for list-type displays.
@@ -66,9 +70,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
         this.set_groups(new (this.options.GroupsType)(this));
 
         if (this.dataset instanceof instance.web.DataSetStatic) {
-            this.groups.datagroup = new instance.web.StaticDataGroup(this.dataset);
+            this.groups.datagroup = new StaticDataGroup(this.dataset);
         } else {
-            this.groups.datagroup = new instance.web.DataGroup(
+            this.groups.datagroup = new DataGroup(
                 this, this.model,
                 dataset.get_domain(),
                 dataset.get_context());
@@ -85,6 +89,10 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
         });
 
         this.no_leaf = false;
+        this.grouped = false;
+    },
+    view_loading: function(r) {
+        return this.load_list(r);
     },
     set_default_options: function (options) {
         this._super(options);
@@ -145,8 +153,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
      * @returns {$.Deferred} loading promise
      */
     start: function() {
-        this.$element.addClass('oe_list');
-        return this.reload_view(null, null, true);
+        this.$el.addClass('oe_list');
+        return this._super();
     },
     /**
      * Returns the style for the provided record in the current view (from the
@@ -220,7 +228,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
      * @param {Object} data.fields_view.arch current list view descriptor
      * @param {Boolean} grouped Is the list view grouped
      */
-    on_loaded: function(data, grouped) {
+    load_list: function(data) {
         var self = this;
         this.fields_view = data;
         this.name = "" + this.fields_view.arch.attrs.string;
@@ -246,50 +254,32 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
                 }).value();
         }
 
-        this.setup_columns(this.fields_view.fields, grouped);
+        this.setup_columns(this.fields_view.fields, this.grouped);
+
+        this.$el.html(QWeb.render(this._template, this));
+        this.$el.addClass(this.fields_view.arch.attrs['class']);
 
-        this.$element.html(QWeb.render(this._template, this));
-        this.$element.addClass(this.fields_view.arch.attrs['class']);
         // Head hook
         // Selecting records
-        this.$element.find('.oe_list_record_selector').click(function(){
-            self.$element.find('.oe_list_record_selector input').prop('checked',
-                self.$element.find('.oe_list_record_selector').prop('checked')  || false);
+        this.$el.find('.oe_list_record_selector').click(function(){
+            self.$el.find('.oe_list_record_selector input').prop('checked',
+                self.$el.find('.oe_list_record_selector').prop('checked')  || false);
             var selection = self.groups.get_selection();
             $(self.groups).trigger(
                 'selected', [selection.ids, selection.records]);
         });
 
-        // Sorting columns
-        this.$element.find('thead').delegate('th.oe_sortable[data-id]', 'click', function (e) {
-            e.stopPropagation();
-            var $this = $(this);
-            self.dataset.sort($this.data('id'));
-            if($this.hasClass("sortdown") || $this.hasClass("sortup"))  {
-                $this.toggleClass("sortdown").toggleClass("sortup");
-            } else {
-                $this.toggleClass("sortdown");
-            }
-            $this.siblings('.oe_sortable').removeClass("sortup sortdown");
-
-            self.reload_content();
-        });
-
-        // Add button and Import link
+        // Add button
         if (!this.$buttons) {
             this.$buttons = $(QWeb.render("ListView.buttons", {'widget':self}));
             if (this.options.$buttons) {
                 this.$buttons.appendTo(this.options.$buttons);
             } else {
-                this.$element.find('.oe_list_buttons').replaceWith(this.$buttons);
+                this.$el.find('.oe_list_buttons').replaceWith(this.$buttons);
             }
             this.$buttons.find('.oe_list_add')
                     .click(this.proxy('do_add_record'))
-                    .prop('disabled', grouped);
-            this.$buttons.on('click', '.oe_list_button_import', function() {
-                self.on_sidebar_import();
-                return false;
-            });
+                    .prop('disabled', this.grouped);
         }
 
         // Pager
@@ -298,7 +288,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
             if (this.options.$buttons) {
                 this.$pager.appendTo(this.options.$pager);
             } else {
-                this.$element.find('.oe_list_pager').replaceWith(this.$pager);
+                this.$el.find('.oe_list_pager').replaceWith(this.$pager);
             }
 
             this.$pager
@@ -351,14 +341,35 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
         if (!this.sidebar && this.options.$sidebar) {
             this.sidebar = new instance.web.Sidebar(this);
             this.sidebar.appendTo(this.options.$sidebar);
-            this.sidebar.add_items('other', [
-                { label: _t("Import"), callback: this.on_sidebar_import },
+            this.sidebar.add_items('other', _.compact([
                 { label: _t("Export"), callback: this.on_sidebar_export },
-                { label: _t('Delete'), callback: this.do_delete_selected }
-            ]);
+                self.is_action_enabled('delete') && { label: _t('Delete'), callback: this.do_delete_selected }
+            ]));
             this.sidebar.add_toolbar(this.fields_view.toolbar);
-            this.sidebar.$element.hide();
+            this.sidebar.$el.hide();
+        }
+        //Sort
+        if(this.dataset._sort.length){
+            if(this.dataset._sort[0].indexOf('-') == -1){
+                this.$el.find('th[data-id=' + this.dataset._sort[0] + ']').addClass("sortdown");
+            }else {
+                this.$el.find('th[data-id=' + this.dataset._sort[0].split('-')[1] + ']').addClass("sortup");
+            }
         }
+        this.trigger('list_view_loaded', data, this.grouped);
+    },
+    sort_by_column: function (e) {
+        e.stopPropagation();
+        var $column = $(e.currentTarget);
+        this.dataset.sort($column.data('id'));
+        if($column.hasClass("sortdown") || $column.hasClass("sortup"))  {
+            $column.toggleClass("sortup sortdown");
+        } else {
+            $column.addClass("sortdown");
+        }
+        $column.siblings('.oe_sortable').removeClass("sortup sortdown");
+
+        this.reload_content();
     },
     /**
      * Configures the ListView pager based on the provided dataset's information
@@ -376,7 +387,10 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
 
         var total = dataset.size();
         var limit = this.limit() || total;
-        this.$pager.toggle(total !== 0);
+        if (total == 0)
+            this.$pager.hide();
+        else
+            this.$pager.css("display", "");
         this.$pager.toggleClass('oe_list_pager_single_page', (total <= limit));
         var spager = '-';
         if (total) {
@@ -385,7 +399,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
             if (range_stop > total) {
                 range_stop = total;
             }
-            spager = _.str.sprintf('%d-%d of %d', range_start, range_stop, total);
+            spager = _.str.sprintf(_t("%d-%d of %d"), range_start, range_stop, total);
         }
 
         this.$pager.find('.oe_list_pager_state').text(spager);
@@ -399,77 +413,23 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
      * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
      */
     setup_columns: function (fields, grouped) {
-        var domain_computer = instance.web.form.compute_domain;
-
-        var noop = function () { return {}; };
-        var field_to_column = function (field) {
-            var name = field.attrs.name;
-            var column = _.extend({id: name, tag: field.tag},
-                    fields[name], field.attrs);
-            // modifiers computer
-            if (column.modifiers) {
-                var modifiers = JSON.parse(column.modifiers);
-                column.modifiers_for = function (fields) {
-                    var out = {};
-
-                    for (var attr in modifiers) {
-                        if (!modifiers.hasOwnProperty(attr)) { continue; }
-                        var modifier = modifiers[attr];
-                        out[attr] = _.isBoolean(modifier)
-                            ? modifier
-                            : domain_computer(modifier, fields);
-                    }
-
-                    return out;
-                };
-                if (modifiers['tree_invisible']) {
-                    column.invisible = '1';
-                } else {
-                    delete column.invisible;
-                }
-                column.modifiers = modifiers;
-            } else {
-                column.modifiers_for = noop;
-                column.modifiers = {};
-            }
-            return column;
-        };
-
+        var registry = instance.web.list.columns;
         this.columns.splice(0, this.columns.length);
-        this.columns.push.apply(
-                this.columns,
-                _(this.fields_view.arch.children).map(field_to_column));
+        this.columns.push.apply(this.columns,
+            _(this.fields_view.arch.children).map(function (field) {
+                var id = field.attrs.name;
+                return registry.for_(id, fields[id], field);
+        }));
         if (grouped) {
-            this.columns.unshift({
-                id: '_group', tag: '', string: _t("Group"), meta: true,
-                modifiers_for: function () { return {}; },
-                modifiers: {}
-            }, {
-                id: '_count', tag: '', string: '#', meta: true,
-                modifiers_for: function () { return {}; },
-                modifiers: {}
-            });
+            this.columns.unshift(
+                new instance.web.list.MetaColumn('_group', _t("Group")));
         }
 
         this.visible_columns = _.filter(this.columns, function (column) {
             return column.invisible !== '1';
         });
 
-        this.aggregate_columns = _(this.visible_columns)
-            .map(function (column) {
-                if (column.type !== 'integer' && column.type !== 'float') {
-                    return {};
-                }
-                var aggregation_func = column['group_operator'] || 'sum';
-                if (!(aggregation_func in column)) {
-                    return {};
-                }
-
-                return _.extend({}, column, {
-                    'function': aggregation_func,
-                    label: column[aggregation_func]
-                });
-            });
+        this.aggregate_columns = _(this.visible_columns).invoke('to_aggregate');
     },
     /**
      * Used to handle a click on a table row, if no other handler caught the
@@ -504,7 +464,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
     },
     do_hide: function () {
         if (this.sidebar) {
-            this.sidebar.$element.hide();
+            this.sidebar.$el.hide();
         }
         if (this.$buttons) {
             this.$buttons.hide();
@@ -517,25 +477,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
     /**
      * Reloads the list view based on the current settings (dataset & al)
      *
+     * @deprecated
      * @param {Boolean} [grouped] Should the list be displayed grouped
      * @param {Object} [context] context to send the server while loading the view
      */
     reload_view: function (grouped, context, initial) {
-        var self = this;
-        var callback = function (field_view_get) {
-            self.on_loaded(field_view_get, grouped);
-        };
-        if (this.embedded_view) {
-            return $.Deferred().then(callback).resolve(this.embedded_view);
-        } else {
-            return this.rpc('/web/listview/load', {
-                model: this.model,
-                view_id: this.view_id,
-                view_type: "tree",
-                context: this.dataset.get_context(context),
-                toolbar: !!this.options.$sidebar
-            }, callback);
-        }
+        return this.load_view(context);
     },
     /**
      * re-renders the content of the list view
@@ -544,10 +491,10 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
      */
     reload_content: function () {
         var self = this;
-        self.$element.find('.oe_list_record_selector').prop('checked', false);
+        self.$el.find('.oe_list_record_selector').prop('checked', false);
         this.records.reset();
         var reloaded = $.Deferred();
-        this.$element.find('.oe_list_content').append(
+        this.$el.find('.oe_list_content').append(
             this.groups.render(function () {
                 if (self.dataset.index == null) {
                     var has_one = false;
@@ -569,12 +516,17 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
         return this.reload_content();
     },
     reload_record: function (record) {
+        var self = this;
         return this.dataset.read_ids(
             [record.get('id')],
             _.pluck(_(this.columns).filter(function (r) {
                     return r.tag === 'field';
                 }), 'name')
-        ).then(function (records) {
+        ).done(function (records) {
+            if (!records[0]) {
+                self.records.remove(record);
+                return;
+            }
             _(records[0]).each(function (value, key) {
                 record.set(key, value, {silent: true});
             });
@@ -609,7 +561,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
      */
     do_search: function (domain, context, group_by) {
         this.page = 0;
-        this.groups.datagroup = new instance.web.DataGroup(
+        this.groups.datagroup = new DataGroup(
             this, this.model, domain, context, group_by);
         this.groups.datagroup.sort = this.dataset._sort;
 
@@ -617,8 +569,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
             group_by = null;
         }
         this.no_leaf = !!context['group_by_no_leaf'];
+        this.grouped = !!group_by;
 
-        this.reload_view(!!group_by, context).then(
+        return this.load_view(context).then(
             this.proxy('reload_content'));
     },
     /**
@@ -631,7 +584,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
             return;
         }
         var self = this;
-        return $.when(this.dataset.unlink(ids)).then(function () {
+        return $.when(this.dataset.unlink(ids)).done(function () {
             _(ids).each(function (id) {
                 self.records.remove(self.records.get(id));
             });
@@ -649,7 +602,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
         if (!ids.length) {
             this.dataset.index = 0;
             if (this.sidebar) {
-                this.sidebar.$element.hide();
+                this.sidebar.$el.hide();
             }
             this.compute_aggregates();
             return;
@@ -657,7 +610,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
 
         this.dataset.index = _(this.dataset.ids).indexOf(ids[0]);
         if (this.sidebar) {
-            this.sidebar.$element.show();
+            this.options.$sidebar.show();
+            this.sidebar.$el.show();
         }
 
         this.compute_aggregates(_(records).map(function (record) {
@@ -678,6 +632,10 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
      * Base handling of buttons, can be called when overriding do_button_action
      * in order to bypass parent overrides.
      *
+     * The callback will be provided with the ``id`` as its parameter, in case
+     * handle_button's caller had to alter the ``id`` (or even create one)
+     * while not being ``callback``'s creator.
+     *
      * This method should not be overridden.
      *
      * @param {String} name action name
@@ -703,7 +661,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
             c.add(action.context);
         }
         action.context = c;
-        this.do_execute_action(action, this.dataset, id, callback);
+        this.do_execute_action(
+            action, this.dataset, id, _.bind(callback, null, id));
     },
     /**
      * Handles the activation of a record (clicking on it)
@@ -812,16 +771,14 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
     },
     display_aggregates: function (aggregation) {
         var self = this;
-        var $footer_cells = this.$element.find('.oe_list_footer');
+        var $footer_cells = this.$el.find('.oe_list_footer');
         _(this.aggregate_columns).each(function (column) {
             if (!column['function']) {
                 return;
             }
 
             $footer_cells.filter(_.str.sprintf('[data-field=%s]', column.id))
-                .html(instance.web.format_cell(aggregation, column, {
-                    process_modifiers: false
-            }));
+                .html(column.format(aggregation, { process_modifiers: false }));
         });
     },
     get_selected_ids: function() {
@@ -840,7 +797,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
     pad_columns: function (count, options) {
         options = options || {};
         // padding for action/pager header
-        var $first_header = this.$element.find('thead tr:first th');
+        var $first_header = this.$el.find('thead tr:first th');
         var colspan = $first_header.attr('colspan');
         if (colspan) {
             if (!this.previous_colspan) {
@@ -849,7 +806,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
             $first_header.attr('colspan', parseInt(colspan, 10) + count);
         }
         // Padding for column titles, footer and data rows
-        var $rows = this.$element
+        var $rows = this.$el
                 .find('.oe_list_header_columns, tr:not(thead tr)')
                 .not(options['except']);
         var newcols = new Array(count+1).join('<td class="oe_list_padding"></td>');
@@ -863,25 +820,29 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
      * Removes all padding columns of the table
      */
     unpad_columns: function () {
-        this.$element.find('.oe_list_padding').remove();
+        this.$el.find('.oe_list_padding').remove();
         if (this.previous_colspan) {
-            this.$element
+            this.$el
                     .find('thead tr:first th')
                     .attr('colspan', this.previous_colspan);
             this.previous_colspan = null;
         }
     },
     no_result: function () {
-        this.$element.find('.oe_view_nocontent').remove();
+        this.$el.find('.oe_view_nocontent').remove();
         if (this.groups.group_by
             || !this.options.action
             || !this.options.action.help) {
             return;
         }
-        this.$element.find('table:first').hide();
-        this.$element.prepend(
+        this.$el.find('table:first').hide();
+        this.$el.prepend(
             $('<div class="oe_view_nocontent">').html(this.options.action.help)
         );
+        var create_nocontent = this.$buttons;
+        this.$el.find('.oe_view_nocontent').click(function() {
+            create_nocontent.openerpBounce();
+        });
     }
 });
 instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.ListView.List# */{
@@ -937,8 +898,8 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
                 var $row;
                 if (attribute === 'id') {
                     if (old_value) {
-                        throw new Error("Setting 'id' attribute on existing record "
-                            + JSON.stringify(record.attributes));
+                        throw new Error(_.str.sprintf( _t("Setting 'id' attribute on existing record %s"),
+                            JSON.stringify(record.attributes) ));
                     }
                     if (!_.contains(self.dataset.ids, value)) {
                         // add record to dataset if not already in (added by
@@ -971,8 +932,19 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
             this.records.bind(event, callback);
         }, this);
 
-        this.$_element = $('<tbody>')
-            .appendTo(document.body)
+        this.$current = $('<tbody>')
+            .delegate('input[readonly=readonly]', 'click', function (e) {
+                /*
+                    Against all logic and sense, as of right now @readonly
+                    apparently does nothing on checkbox and radio inputs, so
+                    the trick of using @readonly to have, well, readonly
+                    checkboxes (which still let clicks go through) does not
+                    work out of the box. We *still* need to preventDefault()
+                    on the event, otherwise the checkbox's state *will* toggle
+                    on click
+                 */
+                e.preventDefault();
+            })
             .delegate('th.oe_list_record_selector', 'click', function (e) {
                 e.stopPropagation();
                 var selection = self.get_selection();
@@ -990,13 +962,19 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
                       field = $target.closest('td').data('field'),
                        $row = $target.closest('tr'),
                   record_id = self.row_id($row);
+                
+                if ($target.attr('disabled')) {
+                    return;
+                }
+                $target.attr('disabled', 'disabled');
 
                 // note: $.data converts data to number if it's composed only
                 // of digits, nice when storing actual numbers, not nice when
                 // storing strings composed only of digits. Force the action
                 // name to be a string
-                $(self).trigger('action', [field.toString(), record_id, function () {
-                    return self.reload_record(self.records.get(record_id));
+                $(self).trigger('action', [field.toString(), record_id, function (id) {
+                    $target.removeAttr('disabled');
+                    return self.reload_record(self.records.get(id));
                 }]);
             })
             .delegate('a', 'click', function (e) {
@@ -1007,7 +985,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
                 if (row_id) {
                     e.stopPropagation();
                     if (!self.dataset.select_id(row_id)) {
-                        throw new Error("Could not find id in dataset");
+                        throw new Error(_t("Could not find id in dataset"));
                     }
                     self.row_clicked(e);
                 }
@@ -1029,13 +1007,13 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
             // set) a human-readable version. m2o does not have this issue
             // because the non-human-readable is just a number, where the
             // human-readable version is a pair
-            if (value && (ref_match = /([\w\.]+),(\d+)/.exec(value))) {
+            if (value && (ref_match = /^([\w\.]+),(\d+)$/.exec(value))) {
                 // reference values are in the shape "$model,$id" (as a
                 // string), we need to split and name_get this pair in order
                 // to get a correctly displayable value in the field
                 var model = ref_match[1],
                     id = parseInt(ref_match[2], 10);
-                new instance.web.DataSet(this.view, model).name_get([id], function(names) {
+                new instance.web.DataSet(this.view, model).name_get([id]).done(function(names) {
                     if (!names.length) { return; }
                     record.set(column.id, names[0][1]);
                 });
@@ -1051,23 +1029,46 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
                 // and let the various registered events handle refreshing the
                 // row
                 new instance.web.DataSet(this.view, column.relation)
-                        .name_get([value], function (names) {
+                        .name_get([value]).done(function (names) {
                     if (!names.length) { return; }
                     record.set(column.id, names[0]);
                 });
             }
+        } else if (column.type === 'many2many') {
+            value = record.get(column.id);
+            // non-resolved (string) m2m values are arrays
+            if (value instanceof Array && !_.isEmpty(value)
+                    && !record.get(column.id + '__display')) {
+                var ids;
+                // they come in two shapes:
+                if (value[0] instanceof Array) {
+                    var command = value[0];
+                    // 1. an array of m2m commands (usually (6, false, ids))
+                    if (command[0] !== 6) {
+                        throw new Error(_.str.sprintf( _t("Unknown m2m command %s"), command[0]));
+                    }
+                    ids = command[2];
+                } else {
+                    // 2. an array of ids
+                    ids = value;
+                }
+                new instance.web.Model(column.relation)
+                    .call('name_get', [ids]).done(function (names) {
+                        // FIXME: nth horrible hack in this poor listview
+                        record.set(column.id + '__display',
+                                   _(names).pluck(1).join(', '));
+                        record.set(column.id, ids);
+                    });
+                // temp empty value
+                record.set(column.id, false);
+            }
         }
-        return instance.web.format_cell(record.toForm().data, column, {
+        return column.format(record.toForm().data, {
             model: this.dataset.model,
             id: record.get('id')
         });
     },
     render: function () {
-        var self = this;
-        if (this.$current) {
-            this.$current.remove();
-        }
-        this.$current = this.$_element.clone(true);
         this.$current.empty().append(
             QWeb.render('ListView.rows', _.extend({
                     render_cell: function () {
@@ -1142,8 +1143,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
         }, this);
         if (!this.$current) { return; }
         this.$current.remove();
-        this.$current = null;
-        this.$_element.remove();
     },
     get_records: function () {
         return this.records.map(function (record) {
@@ -1183,7 +1182,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
     passtrough_events: 'action deleted row_link',
     /**
      * Grouped display for the ListView. Handles basic DOM events and interacts
-     * with the :js:class:`~instance.web.DataGroup` bound to it.
+     * with the :js:class:`~DataGroup` bound to it.
      *
      * Provides events similar to those of
      * :js:class:`~instance.web.ListView.List`
@@ -1263,6 +1262,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
                     .replaceWith(self.render());
             });
         this.$row.children().last()
+            .addClass('oe_list_group_pagination')
             .append($prev)
             .append('<span class="oe_list_pager_state"></span>')
             .append($next);
@@ -1310,7 +1310,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
             self.bind_child_events(child);
             child.datagroup = group;
 
-            var $row = child.$row = $('<tr>');
+            var $row = child.$row = $('<tr class="oe_group_header">');
             if (group.openable && group.length) {
                 $row.click(function (e) {
                     if (!$row.data('open')) {
@@ -1342,15 +1342,20 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
                         _t("Grouping on field '%s' is not possible because that field does not appear in the list view."),
                         group.grouped_on));
                 }
+                var group_label;
                 try {
-                    $group_column.html(instance.web.format_cell(
-                        row_data, group_column, {
-                            value_if_empty: _t("Undefined"),
-                            process_modifiers: false
-                    }));
+                    group_label = group_column.format(row_data, {
+                        value_if_empty: _t("Undefined"),
+                        process_modifiers: false
+                    });
                 } catch (e) {
-                    $group_column.html(row_data[group_column.id].value);
+                    group_label = _.str.escapeHTML(row_data[group_column.id].value);
                 }
+                // group_label is html-clean (through format or explicit
+                // escaping if format failed), can inject straight into HTML
+                $group_column.html(_.str.sprintf(_t("%s (%d)"),
+                    group_label, group.length));
+
                 if (group.length && group.openable) {
                     // Make openable if not terminal group & group_by_no_leaf
                     $group_column.prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">');
@@ -1362,14 +1367,12 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
                 }
             }
             self.indent($group_column, group.level);
-            // count column
-            $('<td>').text(group.length).appendTo($row);
 
             if (self.options.selectable) {
                 $row.append('<td>');
             }
             _(self.columns).chain()
-                .filter(function (column) {return !column.invisible;})
+                .filter(function (column) { return column.invisible !== '1'; })
                 .each(function (column) {
                     if (column.meta) {
                         // do not do anything
@@ -1377,8 +1380,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
                         var r = {};
                         r[column.id] = {value: group.aggregates[column.id]};
                         $('<td class="oe_number">')
-                            .html(instance.web.format_cell(
-                                r, column, {process_modifiers: false}))
+                            .html(column.format(r, {process_modifiers: false}))
                             .appendTo($row);
                     } else {
                         $row.append('<td>');
@@ -1426,57 +1428,74 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
         var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field"}), 'name');
         var options = { offset: page * limit, limit: limit, context: {bin_size: true} };
         //TODO xmo: investigate why we need to put the setTimeout
-        $.async_when().then(function() {dataset.read_slice(fields, options).then(function (records) {
-            // FIXME: ignominious hacks, parents (aka form view) should not send two ListView#reload_content concurrently
-            if (self.records.length) {
-                self.records.reset(null, {silent: true});
-            }
-            if (!self.datagroup.openable) {
-                view.configure_pager(dataset);
-            } else {
-                if (dataset.size() == records.length) {
-                    // only one page
-                    self.$row.find('td.oe_list_group_pagination').empty();
+        $.async_when().done(function() {
+            dataset.read_slice(fields, options).done(function (records) {
+                // FIXME: ignominious hacks, parents (aka form view) should not send two ListView#reload_content concurrently
+                if (self.records.length) {
+                    self.records.reset(null, {silent: true});
+                }
+                if (!self.datagroup.openable) {
+                    view.configure_pager(dataset);
                 } else {
-                    var pages = Math.ceil(dataset.size() / limit);
-                    self.$row
-                        .find('.oe_list_pager_state')
-                            .text(_.str.sprintf(_t("%(page)d/%(page_count)d"), {
-                                page: page + 1,
-                                page_count: pages
-                            }))
-                        .end()
-                        .find('button[data-pager-action=previous]')
-                            .attr('disabled', page === 0)
-                        .end()
-                        .find('button[data-pager-action=next]')
-                            .attr('disabled', page === pages - 1);
+                    if (dataset.size() == records.length) {
+                        // only one page
+                        self.$row.find('td.oe_list_group_pagination').empty();
+                    } else {
+                        var pages = Math.ceil(dataset.size() / limit);
+                        self.$row
+                            .find('.oe_list_pager_state')
+                                .text(_.str.sprintf(_t("%(page)d/%(page_count)d"), {
+                                    page: page + 1,
+                                    page_count: pages
+                                }))
+                            .end()
+                            .find('button[data-pager-action=previous]')
+                                .css('visibility',
+                                     page === 0 ? 'hidden' : '')
+                            .end()
+                            .find('button[data-pager-action=next]')
+                                .css('visibility',
+                                     page === pages - 1 ? 'hidden' : '');
+                    }
                 }
-            }
 
-            self.records.add(records, {silent: true});
-            list.render();
-            d.resolve(list);
-            if (_.isEmpty(records)) {
-                view.no_result();
-            }
-        });});
+                self.records.add(records, {silent: true});
+                list.render();
+                d.resolve(list);
+                if (_.isEmpty(records)) {
+                    view.no_result();
+                }
+            });
+        });
         return d.promise();
     },
     setup_resequence_rows: function (list, dataset) {
         // drag and drop enabled if list is not sorted and there is a
-        // "sequence" column in the view.
+        // visible column with @widget=handle or "sequence" column in the view.
         if ((dataset.sort && dataset.sort())
             || !_(this.columns).any(function (column) {
-                    return column.name === 'sequence'; })) {
+                    return column.widget === 'handle'
+                        || column.name === 'sequence'; })) {
             return;
         }
+        var sequence_field = _(this.columns).find(function (c) {
+            return c.widget === 'handle';
+        });
+        var seqname = sequence_field ? sequence_field.name : 'sequence';
+
         // ondrop, move relevant record & fix sequences
         list.$current.sortable({
             axis: 'y',
             items: '> tr[data-id]',
-            containment: 'parent',
-            helper: 'clone',
+            helper: 'clone'
+        });
+        if (sequence_field) {
+            list.$current.sortable('option', 'handle', '.oe_list_field_handle');
+        }
+        list.$current.sortable('option', {
+            start: function (e, ui) {
+                ui.placeholder.height(ui.item.height());
+            },
             stop: function (event, ui) {
                 var to_move = list.records.get(ui.item.data('id')),
                     target_id = ui.item.prev().data('id'),
@@ -1494,7 +1513,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
                 var record, index = to,
                     // if drag to 1st row (to = 0), start sequencing from 0
                     // (exclusive lower bound)
-                    seq = to ? list.records.at(to - 1).get('sequence') : 0;
+                    seq = to ? list.records.at(to - 1).get(seqname) : 0;
                 while (++seq, record = list.records.at(index++)) {
                     // write are independent from one another, so we can just
                     // launch them all at the same time and we don't really
@@ -1503,38 +1522,40 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
                     //        Accounting > Taxes > Taxes, child tax accounts)
                     //        when synchronous (without setTimeout)
                     (function (dataset, id, seq) {
-                        $.async_when().then(function () {
-                            dataset.write(id, {sequence: seq});
+                        $.async_when().done(function () {
+                            var attrs = {};
+                            attrs[seqname] = seq;
+                            dataset.write(id, attrs);
                         });
                     }(dataset, record.get('id'), seq));
-                    record.set('sequence', seq);
+                    record.set(seqname, seq);
                 }
             }
         });
     },
     render: function (post_render) {
         var self = this;
-        var $element = $('<tbody>');
-        this.elements = [$element[0]];
+        var $el = $('<tbody>');
+        this.elements = [$el[0]];
 
         this.datagroup.list(
             _(this.view.visible_columns).chain()
                 .filter(function (column) { return column.tag === 'field' })
                 .pluck('name').value(),
             function (groups) {
-                $element[0].appendChild(
+                $el[0].appendChild(
                     self.render_groups(groups));
                 if (post_render) { post_render(); }
             }, function (dataset) {
-                self.render_dataset(dataset).then(function (list) {
+                self.render_dataset(dataset).done(function (list) {
                     self.children[null] = list;
                     self.elements =
-                        [list.$current.replaceAll($element)[0]];
+                        [list.$current.replaceAll($el)[0]];
                     self.setup_resequence_rows(list, dataset);
                     if (post_render) { post_render(); }
                 });
             });
-        return $element;
+        return $el;
     },
     /**
      * Returns the ids of all selected records for this group, and the records
@@ -1573,6 +1594,59 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
     }
 });
 
+var DataGroup =  instance.web.Class.extend({
+   init: function(parent, model, domain, context, group_by, level) {
+       this.model = new instance.web.Model(model, context, domain);
+       this.group_by = group_by;
+       this.context = context;
+       this.domain = domain;
+
+       this.level = level || 0;
+   },
+   list: function (fields, ifGroups, ifRecords) {
+       var self = this;
+       var query = this.model.query(fields).order_by(this.sort).group_by(this.group_by);
+       $.when(query).done(function (querygroups) {
+           // leaf node
+           if (!querygroups) {
+               var ds = new instance.web.DataSetSearch(self, self.model.name, self.model.context(), self.model.domain());
+               ds._sort = self.sort;
+               ifRecords(ds);
+               return;
+           }
+           // internal node
+           var child_datagroups = _(querygroups).map(function (group) {
+               var child_context = _.extend(
+                   {}, self.model.context(), group.model.context());
+               var child_dg = new DataGroup(
+                   self, self.model.name, group.model.domain(),
+                   child_context, group.model._context.group_by,
+                   self.level + 1);
+               child_dg.sort = self.sort;
+               // copy querygroup properties
+               child_dg.__context = child_context;
+               child_dg.__domain = group.model.domain();
+               child_dg.folded = group.get('folded');
+               child_dg.grouped_on = group.get('grouped_on');
+               child_dg.length = group.get('length');
+               child_dg.value = group.get('value');
+               child_dg.openable = group.get('has_children');
+               child_dg.aggregates = group.get('aggregates');
+               return child_dg;
+           });
+           ifGroups(child_datagroups);
+       });
+   }
+});
+var StaticDataGroup = DataGroup.extend({
+   init: function (dataset) {
+       this.dataset = dataset;
+   },
+   list: function (fields, ifGroups, ifRecords) {
+       ifRecords(this.dataset);
+   }
+});
+
 /**
  * @mixin Events
  */
@@ -1698,7 +1772,7 @@ var Record = instance.web.Class.extend(/** @lends Record# */{
             } else if (val instanceof Array) {
                 output[k] = val[0];
             } else {
-                throw new Error("Can't convert value " + val + " to context");
+                throw new Error(_.str.sprintf(_t("Can't convert value %s to context"), val));
             }
         }
         return output;
@@ -1961,6 +2035,239 @@ instance.web.list = {
     Events: Events,
     Record: Record,
     Collection: Collection
-}
+};
+/**
+ * Registry for column objects used to format table cells (and some other tasks
+ * e.g. aggregation computations).
+ *
+ * Maps a field or button to a Column type via its ``$tag.$widget``,
+ * ``$tag.$type`` or its ``$tag`` (alone).
+ *
+ * This specific registry has a dedicated utility method ``for_`` taking a
+ * field (from fields_get/fields_view_get.field) and a node (from a view) and
+ * returning the right object *already instantiated from the data provided*.
+ *
+ * @type {instance.web.Registry}
+ */
+instance.web.list.columns = new instance.web.Registry({
+    'field': 'instance.web.list.Column',
+    'field.boolean': 'instance.web.list.Boolean',
+    'field.binary': 'instance.web.list.Binary',
+    'field.progressbar': 'instance.web.list.ProgressBar',
+    'field.handle': 'instance.web.list.Handle',
+    'button': 'instance.web.list.Button',
+    'field.many2onebutton': 'instance.web.list.Many2OneButton',
+    'field.many2many': 'instance.web.list.Many2Many'
+});
+instance.web.list.columns.for_ = function (id, field, node) {
+    var description = _.extend({tag: node.tag}, field, node.attrs);
+    var tag = description.tag;
+    var Type = this.get_any([
+        tag + '.' + description.widget,
+        tag + '.'+ description.type,
+        tag
+    ]);
+    return new Type(id, node.tag, description)
+};
+
+instance.web.list.Column = instance.web.Class.extend({
+    init: function (id, tag, attrs) {
+        _.extend(attrs, {
+            id: id,
+            tag: tag
+        });
+
+        this.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
+        delete attrs.modifiers;
+        _.extend(this, attrs);
+
+        if (this.modifiers['tree_invisible']) {
+            this.invisible = '1';
+        } else { delete this.invisible; }
+    },
+    modifiers_for: function (fields) {
+        var out = {};
+        var domain_computer = instance.web.form.compute_domain;
+
+        for (var attr in this.modifiers) {
+            if (!this.modifiers.hasOwnProperty(attr)) { continue; }
+            var modifier = this.modifiers[attr];
+            out[attr] = _.isBoolean(modifier)
+                ? modifier
+                : domain_computer(modifier, fields);
+        }
+
+        return out;
+    },
+    to_aggregate: function () {
+        if (this.type !== 'integer' && this.type !== 'float') {
+            return {};
+        }
+        var aggregation_func = this['group_operator'] || 'sum';
+        if (!(aggregation_func in this)) {
+            return {};
+        }
+        var C = function (fn, label) {
+            this['function'] = fn;
+            this.label = label;
+        };
+        C.prototype = this;
+        return new C(aggregation_func, this[aggregation_func]);
+    },
+    /**
+     *
+     * @param row_data record whose values should be displayed in the cell
+     * @param {Object} [options]
+     * @param {String} [options.value_if_empty=''] what to display if the field's value is ``false``
+     * @param {Boolean} [options.process_modifiers=true] should the modifiers be computed ?
+     * @param {String} [options.model] current record's model
+     * @param {Number} [options.id] current record's id
+     * @return {String}
+     */
+    format: function (row_data, options) {
+        options = options || {};
+        var attrs = {};
+        if (options.process_modifiers !== false) {
+            attrs = this.modifiers_for(row_data);
+        }
+        if (attrs.invisible) { return ''; }
+
+        if (!row_data[this.id]) {
+            return options.value_if_empty === undefined
+                    ? ''
+                    : options.value_if_empty;
+        }
+        return this._format(row_data, options);
+    },
+    /**
+     * Method to override in order to provide alternative HTML content for the
+     * cell. Column._format will simply call ``instance.web.format_value`` and
+     * escape the output.
+     *
+     * The output of ``_format`` will *not* be escaped by ``format``, any
+     * escaping *must be done* by ``format``.
+     *
+     * @private
+     */
+    _format: function (row_data, options) {
+        return _.escape(instance.web.format_value(
+            row_data[this.id].value, this, options.value_if_empty));
+    }
+});
+instance.web.list.MetaColumn = instance.web.list.Column.extend({
+    meta: true,
+    init: function (id, string) {
+        this._super(id, '', {string: string});
+    }
+});
+instance.web.list.Button = instance.web.list.Column.extend({
+    /**
+     * Return an actual ``<button>`` tag
+     */
+    format: function (row_data, options) {
+        options = options || {};
+        var attrs = {};
+        if (options.process_modifiers !== false) {
+            attrs = this.modifiers_for(row_data);
+        }
+        if (attrs.invisible) { return ''; }
+
+        return QWeb.render('ListView.row.button', {
+            widget: this,
+            prefix: instance.session.prefix,
+            disabled: attrs.readonly
+                || isNaN(row_data.id.value)
+                || instance.web.BufferedDataSet.virtual_id_regex.test(row_data.id.value)
+        });
+    }
+});
+instance.web.list.Boolean = instance.web.list.Column.extend({
+    /**
+     * Return a potentially disabled checkbox input
+     *
+     * @private
+     */
+    _format: function (row_data, options) {
+        return _.str.sprintf('<input type="checkbox" %s readonly="readonly"/>',
+                 row_data[this.id].value ? 'checked="checked"' : '');
+    }
+});
+instance.web.list.Binary = instance.web.list.Column.extend({
+    /**
+     * Return a link to the binary data as a file
+     *
+     * @private
+     */
+    _format: function (row_data, options) {
+        var text = _t("Download");
+        var value = row_data[this.id].value;
+        var download_url;
+        if (value && value.substr(0, 10).indexOf(' ') == -1) {
+            download_url = "data:application/octet-stream;base64," + value;
+        } else {
+            download_url = this.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
+            if (this.filename) {
+                download_url += '&filename_field=' + this.filename;
+            }
+        }
+        if (this.filename && row_data[this.filename]) {
+            text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value(
+                    row_data[this.filename].value, {type: 'char'}));
+        }
+        return _.template('<a href="<%-href%>"><%-text%></a> (<%-size%>)', {
+            text: text,
+            href: download_url,
+            size: instance.web.binary_to_binsize(value),
+        });
+    }
+});
+instance.web.list.ProgressBar = instance.web.list.Column.extend({
+    /**
+     * Return a formatted progress bar display
+     *
+     * @private
+     */
+    _format: function (row_data, options) {
+        return _.template(
+            '<progress value="<%-value%>" max="100"><%-value%>%</progress>', {
+                value: _.str.sprintf("%.0f", row_data[this.id].value || 0)
+            });
+    }
+});
+instance.web.list.Handle = instance.web.list.Column.extend({
+    init: function () {
+        this._super.apply(this, arguments);
+        // Handle overrides the field to not be form-editable.
+        this.modifiers.readonly = true;
+    },
+    /**
+     * Return styling hooks for a drag handle
+     *
+     * @private
+     */
+    _format: function (row_data, options) {
+        return '<div class="oe_list_handle">';
+    }
+});
+instance.web.list.Many2OneButton = instance.web.list.Column.extend({
+    _format: function (row_data, options) {
+        this.has_value = !!row_data[this.id].value;
+        this.icon = this.has_value ? 'gtk-yes' : 'gtk-no';
+        this.string = this.has_value ? _t('View') : _t('Create');
+        return QWeb.render('Many2OneButton.cell', {
+            'widget': this,
+            'prefix': instance.session.prefix,
+        });
+    },
+});
+instance.web.list.Many2Many = instance.web.list.Column.extend({
+    _format: function (row_data, options) {
+        if (!_.isEmpty(row_data[this.id].value)) {
+            // If value, use __display version for printing
+            row_data[this.id] = row_data[this.id + '__display'];
+        }
+        return this._super(row_data, options);
+    }
+});
 };
 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: