Xavier (Open ERP) has proposed merging 
lp:~openerp-dev/openerp-web/trunk-editable-lists-xmo into lp:openerp-web.

Requested reviews:
  Antony Lesuisse (al-openerp)

For more details, see:
https://code.launchpad.net/~openerp-dev/openerp-web/trunk-editable-lists-xmo/+merge/63877

* Editable lists (top|bottom), including within groups
* List-wise aggregations

Bug-fixed a bit since the demo (flicker on edit/cancel edit removed, as well as 
finishing of aggregation handling including for groups)

 controllers/main.py            |   14 -
 static/src/base.html           |    1 
 static/src/css/base.css        |    5 
 static/src/js/base.js          |    1 
 static/src/js/chrome.js        |   10 +
 static/src/js/data.js          |    6 
 static/src/js/form.js          |   59 ++++--
 static/src/js/list-editable.js |  260 ++++++++++++++++++++++++++++
 static/src/js/list.js          |  370 ++++++++++++++++++++++++-----------------
 static/src/js/views.js         |    7 
 static/src/xml/base.xml        |   37 +++-
 11 files changed, 583 insertions(+), 187 deletions(-)

-- 
https://code.launchpad.net/~openerp-dev/openerp-web/trunk-editable-lists-xmo/+merge/63877
Your team OpenERP R&D Team is subscribed to branch 
lp:~openerp-dev/openerp-web/trunk-editable-lists-xmo.
=== modified file 'addons/base/controllers/main.py'
--- addons/base/controllers/main.py	2011-06-06 11:44:35 +0000
+++ addons/base/controllers/main.py	2011-06-08 15:01:21 +0000
@@ -611,20 +611,6 @@
         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')
 

=== modified file 'addons/base/static/src/base.html'
--- addons/base/static/src/base.html	2011-05-24 14:00:55 +0000
+++ addons/base/static/src/base.html	2011-06-08 15:01:21 +0000
@@ -28,6 +28,7 @@
     <script type="text/javascript" src="/base/static/src/js/views.js"></script>
     <script type="text/javascript" src="/base/static/src/js/form.js"></script>
     <script type="text/javascript" src="/base/static/src/js/list.js"></script>
+    <script type="text/javascript" src="/base/static/src/js/list-editable.js"></script>
     <script type="text/javascript" src="/base/static/src/js/tree.js"></script>
     <script type="text/javascript" src="/base/static/src/js/search.js"></script>
     <script type="text/javascript" src="/base/static/src/js/m2o.js"></script>

=== modified file 'addons/base/static/src/css/base.css'
--- addons/base/static/src/css/base.css	2011-06-08 09:44:26 +0000
+++ addons/base/static/src/css/base.css	2011-06-08 15:01:21 +0000
@@ -27,6 +27,10 @@
     overflow-y: scroll;
 }
 
+.openerp .oe-number {
+    text-align: right !important;
+}
+
 /* STATES */
 .openerp .on_logged {
     display: none;
@@ -602,6 +606,7 @@
     padding: 0;
     border: none;
     background: none;
+    width: 100%;
 }
 .openerp .oe-listview .oe-field-cell button:active {
     opacity: 0.5;

=== modified file 'addons/base/static/src/js/base.js'
--- addons/base/static/src/js/base.js	2011-05-23 11:20:26 +0000
+++ addons/base/static/src/js/base.js	2011-06-08 15:01:21 +0000
@@ -132,6 +132,7 @@
     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:

=== modified file 'addons/base/static/src/js/chrome.js'
--- addons/base/static/src/js/chrome.js	2011-06-08 09:44:26 +0000
+++ addons/base/static/src/js/chrome.js	2011-06-08 15:01:21 +0000
@@ -149,6 +149,16 @@
     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 || {}));
     }
 });
 

=== modified file 'addons/base/static/src/js/data.js'
--- addons/base/static/src/js/data.js	2011-05-31 14:11:19 +0000
+++ addons/base/static/src/js/data.js	2011-06-08 15:01:21 +0000
@@ -259,7 +259,7 @@
      */
     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 @@
      */
     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]);
             });
         }

=== modified file 'addons/base/static/src/js/form.js'
--- addons/base/static/src/js/form.js	2011-06-07 10:52:48 +0000
+++ addons/base/static/src/js/form.js	2011-06-08 15:01:21 +0000
@@ -8,12 +8,15 @@
      * 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 @@
         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 @@
         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 @@
             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 @@
             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 @@
             }
         }
     },
-    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.Widget = openerp.base.Controller.extend({
+    template: 'Widget',
     init: function(view, node) {
         this.view = view;
         this.node = node;
@@ -435,7 +466,6 @@
         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.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 @@
     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 @@
  * 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',

=== added file 'addons/base/static/src/js/list-editable.js'
--- addons/base/static/src/js/list-editable.js	1970-01-01 00:00:00 +0000
+++ addons/base/static/src/js/list-editable.js	2011-06-08 15:01:21 +0000
@@ -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 = $('<tr>', {
+                    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('<td>');
+                    }
+                });
+
+                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);
+    });
+};

=== modified file 'addons/base/static/src/js/list.js'
--- addons/base/static/src/js/list.js	2011-06-06 07:52:43 +0000
+++ addons/base/static/src/js/list.js	2011-06-08 15:01:21 +0000
@@ -83,8 +83,8 @@
             '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 @@
         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 @@
             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 @@
             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 @@
      * @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);
-
-            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'));
-        });
+        }, $.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;
+        }
+
+        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 @@
      */
     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 @@
      *
      * @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 @@
      * @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']) {
+        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':
+                        sums[field] += record.values[field];
+                        break;
                     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());
+                        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(
-                    "<span>%s: %.2f</span>",
-                    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 @@
      * @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 @@
                 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 @@
         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 @@
         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 @@
      * @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 @@
             _(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 @@
         }
         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 @@
         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 = $('<tr>');
             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 @@
             // 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 @@
     },
     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 @@
             _.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 @@
         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();

=== modified file 'addons/base/static/src/js/views.js'
--- addons/base/static/src/js/views.js	2011-06-06 07:52:43 +0000
+++ addons/base/static/src/js/views.js	2011-06-08 15:01:21 +0000
@@ -9,7 +9,7 @@
     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 @@
             case 'ir.actions.act_window':
                 if (action.target == 'new') {
                     var element_id = _.uniqueId("act_window_dialog");
-                    var dialog = $('<div id="' + element_id + '"></div>');
-                    dialog.dialog({
+                    $('<div>', {id: element_id}).dialog({
                         title: action.name,
                         modal: true,
                         width: '50%',
@@ -347,7 +346,7 @@
             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();

=== modified file 'addons/base/static/src/xml/base.xml'
--- addons/base/static/src/xml/base.xml	2011-06-08 09:44:26 +0000
+++ addons/base/static/src/xml/base.xml	2011-06-08 15:01:21 +0000
@@ -300,11 +300,13 @@
             <th t-if="options.deletable"/>
         </tr>
     </thead>
-    <tfoot class="ui-widget-header" t-if="aggregate_columns.length">
+    <tfoot class="ui-widget-header">
         <tr>
-            <td t-att-colspan="columns_count" class='oe-list-footer'>
-                
+            <td t-if="options.selectable"/>
+            <td t-foreach="aggregate_columns" t-as="column" class="oe-list-footer oe-number"
+                t-att-data-field="column.field" t-att-title="column.label">
             </td>
+            <td t-if="options.deletable"/>
         </tr>
     </tfoot>
 </table>
@@ -316,7 +318,8 @@
         </t-if>
     </t>
 </t>
-<tr t-name="ListView.row" t-att-style="style" t-att-class="row_parity">
+<tr t-name="ListView.row" t-att-style="style" t-att-class="row_parity"
+    t-att-data-index="row_index">
     <t t-foreach="columns" t-as="column">
         <td t-if="column.meta">
             
@@ -326,8 +329,10 @@
         <input type="checkbox"/>
     </th>
     <t t-foreach="columns" t-as="column">
+        <t t-set="align" t-value="column.type === 'integer' or column.type == 'float'"/>
         <td t-if="!column.meta and column.invisible !== '1'" t-att-title="column.help"
-            class="oe-field-cell" t-att-data-field="column.id">
+            t-att-class="'oe-field-cell' + (align ? ' oe-number' : '')"
+            t-att-data-field="column.id">
             <t t-set="attrs" t-value="column.attrs_for(row.data)"/>
             <t t-if="!attrs.invisible">
                 <t t-set="is_button" t-value="column.tag === 'button'"/>
@@ -348,6 +353,9 @@
         <button type="button" name="delete">♻</button>
     </td>
 </tr>
+<t t-name="ListView.row.form">
+    <t t-raw="frame.render()"/>
+</t>
 <t t-name="FormView">
     <h2 class="oe_view_title"><t t-esc="view.fields_view.arch.attrs.string"/></h2>
     <div class="oe_form_header" t-att-id="view.element_id + '_header'">
@@ -882,4 +890,23 @@
     <button type="button" class="oe_many2xselectpopup-form-save">Save</button>
     <button type="button" class="oe_many2xselectpopup-form-close">Close</button>
 </t>
+
+<t t-name="ListView.row.frame" t-extend="WidgetFrame">
+    <t t-jquery="tr">
+        $(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();
+    </t>
+</t>
 </templates>

_______________________________________________
Mailing list: https://launchpad.net/~openerp-dev-gtk
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~openerp-dev-gtk
More help   : https://help.launchpad.net/ListHelp

Reply via email to