Example #1
0
odoo.define('gooderp.fixed_header', function(require) {
    var ListView = require('web.ListView');
       /* 固定表头 */
    ListView.include({
        load_list: function () {
            var self = this;
            return this._super.apply(this, arguments).done(function () {
                var form_field_length = self.$el.parents('.o_form_field').length;
                var scrollArea = $(".o_content")[0];
                function do_freeze () {
                    self.$el.find('table.o_list_view').each(function () {
                        $(this).stickyTableHeaders({scrollableArea: scrollArea});
                    });
                }

                if (form_field_length == 0) {
                    do_freeze();
                    $(window).unbind('resize', do_freeze).bind('resize', do_freeze);
                }
            });
        },
    });

    ListView.Groups.include({
        render_groups: function () {
            var self = this;
            var placeholder = this._super.apply(this, arguments);
            var grouping_freezer = document.createElement("script");

            grouping_freezer.innerText = "$('.o_group_header').click(function () { setTimeout('" +
                "var scrollArea = $(\".o_content\")[0]; " +
                "$(\"table.o_list_view\").each(function () { $(this).stickyTableHeaders({scrollableArea: scrollArea}); }); " +
                "',250); })";

            placeholder.appendChild(grouping_freezer);
            return placeholder;
        },
    });

});
odoo.define('web.ListEditor', function (require) {
"use strict";    
/*---------------------------------------------------------
 * Odoo Editable List view
 *---------------------------------------------------------*/
/**
 * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
 * @namespace
 */

var core = require('web.core');
var data = require('web.data');
var FormView = require('web.FormView');
var common = require('web.list_common');
var ListView = require('web.ListView');
var utils = require('web.utils');
var Widget = require('web.Widget');

var _t = core._t;

var Editor = Widget.extend({
    /**
     * @constructs instance.web.list.Editor
     * @extends instance.web.Widget
     *
     * Adapter between listview and formview for editable-listview purposes
     *
     * @param {instance.web.Widget} parent
     * @param {Object} options
     * @param {instance.web.FormView} [options.formView=instance.web.FormView]
     * @param {Object} [options.delegate]
     */
    init: function (parent, options) {
        this._super(parent);
        this.options = options || {};
        _.defaults(this.options, {
            formView: FormView,
            delegate: this.getParent(),
        });
        this.delegate = this.options.delegate;

        this.record = null;
        this.form = new (this.options.formView)(this, this.delegate.dataset, false, {
            initial_mode: 'edit',
            is_list_editable: true,
            disable_autofocus: true,
            $buttons: $(),
            $pager: $(),
        });
    },
    start: function () {
        var self = this;
        this.form.embedded_view = this._validate_view(this.delegate.edition_view(this));
        return $.when(this._super(), this.form.appendTo($('<div/>')).then(function() {
            self.form.$el.addClass(self.$el.attr('class'));
            self.replaceElement(self.form.$el);
        }).done(this.proxy('do_hide')));
    },
    _validate_view: function (edition_view) {
        if (!edition_view) {
            throw new Error("editor delegate's #edition_view must return a view descriptor");
        }
        var arch = edition_view.arch;
        if (!(arch && arch.children instanceof Array)) {
            throw new Error("Editor delegate's #edition_view must have a non-empty arch");
        }
        if (arch.tag !== "form") {
            throw new Error("Editor delegate's #edition_view must have a 'form' root node");
        }
        if (!(arch.attrs && arch.attrs.version === "7.0")) {
            throw new Error("Editor delegate's #edition_view must be a version 7 view");
        }
        if (!/\boe_form_container\b/.test(arch.attrs['class'])) {
            throw new Error("Editor delegate's #edition_view must have the class " +
                            "'boe_form_container' on its root element");
        }
        return edition_view;
    },
    is_editing: function () {
        return !!this.record;
    },
    is_creating: function () {
        return (this.is_editing() && !this.record.id);
    },
    edit: function (record, configureField, options) {
        // TODO: specify sequence of edit calls
        var loaded;
        if(record) {
            loaded = this.form.trigger('load_record', _.extend({}, record))
        } else {
            loaded = this.form.load_defaults();
        }

        var self = this;
        return $.when(loaded).then(function () {
            return self.do_show({reload: false});
        }).then(function () {
            self.record = self.form.datarecord;
            _(self.form.fields).each(function (field, name) {
                configureField(name, field);
            });
            return self.form;
        });
    },
    save: function () {
        var self = this;
        return this.form
            .save(this.delegate.prepends_on_create())
            .then(function (result) {
                if(result.created && !self.record.id) {
                    self.record.id = result.result;
                }
                return self.record;
            });
    },
    cancel: function (force) {
        var self = this;
        if(force) {
            return do_cancel();
        }
        var message = _t("The line has been modified, your changes will be discarded. Are you sure you want to discard the changes ?");
        return this.form.can_be_discarded(message).then(do_cancel);

        function do_cancel() {
            var record = self.record;
            self.record = null;
            self.do_hide();
            return $.when(record);
        }
    },
    do_hide: function() {
        this.form.do_hide.apply(this.form, arguments);
        this.form.set({display_invalid_fields: false});
    },
    do_show: function() {
        this.form.do_show.apply(this.form, arguments);
    },
});

// editability status of list rows
ListView.prototype.defaults.editable = null;

// TODO: not sure second @lends on existing item is correct, to check
ListView.include(/** @lends instance.web.ListView# */{
    init: function () {
        var self = this;
        this._super.apply(this, arguments);

        this.saving_mutex = new utils.Mutex();

        this._force_editability = null;
        this._context_editable = false;
        this.editor = new Editor(this);
        // Stores records of {field, cell}, allows for re-rendering fields
        // depending on cell state during and after resize events
        this.fields_for_resize = [];
        core.bus.on('resize', this, this.resize_fields);

        $(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.configure_pager(self.dataset);
                self.compute_aggregates();
            }
        });

        this.records.bind('remove', function () {
            if (self.editor.is_editing()) {
                self.cancel_edition();
            }
        });

        this.on('edit:before', this, function (event) {
            if (!self.editable() || self.editor.is_editing()) {
                event.cancel = true;
            }
        });
        this.on('edit:after', this, function () {
            self.$el.add(self.$buttons).addClass('oe_editing');
            self.$('.ui-sortable').sortable('disable');
        });
        this.on('save:after cancel:after', this, function () {
            self.$('.ui-sortable').sortable('enable');
            self.$el.add(self.$buttons).removeClass('oe_editing');
        });
    },
    destroy: function () {
        core.bus.off('resize', this, this.resize_fields);
        this._super();
    },
    do_hide: function () {
        if (this.editor.is_editing()) {
            this.cancel_edition(true);
        }
        this._super();
    },
    sort_by_column: function (e) {
        e.stopPropagation();
        if (!this.editor.is_editing()) {
            this._super.apply(this, arguments);
        }
    },
    /**
     * 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 {instance.web.DataSet} dataset dataset in which the record is available
     */
    do_edit: function (index, id, dataset) {
        _.extend(this.dataset, dataset);
    },
    do_delete: function (ids) {
        var nonfalse = _.compact(ids);
        var _super = this._super.bind(this);
        var next = (this.editor.is_editing())? this.cancel_edition(true) : $.when();
        return next.then(function () {
            return _super(nonfalse);
        });
    },
    editable: function () {
        return !this.grouped
            && !this.options.disable_editable_mode
            && (this.fields_view.arch.attrs.editable || this._context_editable || this.options.editable);
    },
    /**
     * Replace do_search to handle editability process
     */
    do_search: function(domain, context, group_by) {
        var self = this;
        var _super = this._super;
        var args = arguments;
        var ready = (this.editor.is_editing())? this.cancel_edition(true) : $.when();
        return ready.then(function () {
            self._context_editable = !!context.set_editable;
            return _super.apply(self, args);
        });
    },
    /**
     * 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.editable()) {
            this.$('table:first').show();
            this.$('.oe_view_nocontent').remove();
            this.start_edition();
        } else {
            this._super.apply(this, arguments);
        }
    },
    load_list: function (data, grouped) {
        // tree/@editable takes priority on everything else if present.
        var result = this._super.apply(this, arguments);

        // In case current editor was started previously, also has to run
        // when toggling from editable to non-editable in case form widgets
        // have setup global behaviors expecting themselves to exist somehow.
        this.editor.destroy();
        this.editor = new Editor(this); // Editor is not restartable due to formview not being restartable

        if(this.editable()) {
            this.$el.addClass('oe_list_editable');
            return $.when(result, this.editor.prependTo(this.$el).done(this.proxy('setup_events')));
        } else {
            this.$el.removeClass('oe_list_editable');
        }
        return result;
    },
    /**
     * Extend the render_buttons function of ListView by adding event listeners
     * in the case of an editable list.
     * @return {jQuery} the rendered buttons
     */
    render_buttons: function() {
        var add_button = !this.$buttons; // Ensures that this is only done once
        var result = this._super.apply(this, arguments); // Sets this.$buttons

        if (add_button && (this.editable() || this.grouped)) {
            var self = this;
            this.$buttons
                .off('click', '.o_list_button_save')
                .on('click', '.o_list_button_save', this.proxy('save_edition'))
                .off('click', '.o_list_button_discard')
                .on('click', '.o_list_button_discard', function (e) {
                    e.preventDefault();
                    self.cancel_edition();
                });
        }
        return result;
    },
    do_button_action: function (name, id, callback) {
        var self = this;
        this.save_edition().done(function (data) {
            if(!id && data.created) {
                id = data.record.get('id');
            }
            self.handle_button(name, id, callback);
        });
    },
    /**
     * Builds a record with the provided id (``false`` for a creation),
     * setting all columns with ``false`` value so code which relies on
     * having an actual value behaves correctly
     *
     * @param {*} id
     * @return {instance.web.list.Record}
     */
    make_empty_record: function (id) {
        var attrs = {id: id};
        _(this.columns).chain()
            .filter(function (x) { return x.tag === 'field'; })
            .pluck('name')
            .each(function (field) { attrs[field] = false; });
        return new common.Record(attrs);
    },
    /**
     * Set up the edition of a record of the list view "inline"
     *
     * @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record
     * @param {Object} [options]
     * @param {String} [options.focus_field] field to focus at start of edition
     * @return {jQuery.Deferred}
     */
    start_edition: function (record, options) {
        var self = this;
        var item = false;
        if (record) {
            item = record.attributes;
            this.dataset.select_id(record.get('id'));
        } else {
            record = this.make_empty_record(false);
            this.records.add(record, {at: (this.prepends_on_create())? 0 : null});
        }

        return this.save_edition().then(function() {
            return $.when.apply($, self.editor.form.render_value_defs);
        }).then(function () {
            var $recordRow = self.groups.get_row_for(record);
            var cells = self.get_cells_for($recordRow);
            var fields = {};
            self.fields_for_resize.splice(0, self.fields_for_resize.length); // Empty array
            return self.with_event('edit', {
                record: record.attributes,
                cancel: false,
            }, function () {
                return self.editor.edit(item, function (field_name, field) {
                    var cell = cells[field_name];
                    if (!cell) {
                        return;
                    }

                    // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow
                    field.$el.attr('data-fieldname', field_name);
                    fields[field_name] = field;
                    self.fields_for_resize.push({field: field, cell: cell});
                }, options).then(function () {
                    $recordRow.addClass('oe_edition');
                    self.resize_fields();
                    // Local function that returns true if field is visible and editable
                    var is_focusable = function(field) {
                        return field && field.$el.is(':visible:not(.oe_readonly)');
                    };
                    var focus_field = options && options.focus_field ? options.focus_field : undefined;
                    if (!is_focusable(fields[focus_field])) {
                        focus_field = _.find(self.editor.form.fields_order, function(field) {
                            return is_focusable(fields[field]);
                        });
                    }
                    if (fields[focus_field]) {
                        fields[focus_field].$el.find('input, textarea').andSelf().filter('input, textarea').focus();
                    }
                    return record.attributes;
                });
            }).fail(function () {
                // if the start_edition event is cancelled and it was a creation, remove the newly-created empty record
                if(!record.get('id')) {
                    self.records.remove(record);
                }
            });
        }, function() {
            return $.Deferred().resolve(); // Here the save/cancel edition failed so the start_edition is considered as done and succeeded
        });
    },
    get_cells_for: function ($row) {
        var cells = {};
        $row.children('td').each(function (index, el) {
            cells[el.getAttribute('data-field')] = el;
        });
        return cells;
    },
    /**
     * If currently editing a row, resizes all registered form fields based
     * on the corresponding row cell
     */
    resize_fields: function () {
        if (!this.editor.is_editing()) {
            return;
        }
        for(var i = 0, len = this.fields_for_resize.length ; i < len ; i++) {
            var item = this.fields_for_resize[i];
            this.resize_field(item.field, item.cell);
        }
    },
    /**
     * Resizes a field's root element based on the corresponding cell of
     * a listview row
     *
     * @param {instance.web.form.AbstractField} field
     * @param {jQuery} cell
     */
    resize_field: function (field, cell) {
        var $cell = $(cell);
        field.set_dimensions($cell.outerHeight(), $cell.outerWidth());
        field.$el.addClass('o_temp_visible').css({top: 0, left: 0}).position({
            my: 'left top',
            at: 'left top',
            of: $cell,
        }).removeClass('o_temp_visible');
        if(field.get('effective_readonly')) {
            field.$el.addClass('oe_readonly');
        }
        if(field.widget == "handle") {
            field.$el.addClass('oe_list_field_handle');
        }
    },
    /**
     * @return {jQuery.Deferred}
     */
    save_edition: function () {
        var self = this;
        return self.saving_mutex.exec(function() {
            if (!self.editor.is_editing()) {
                return $.when();
            }
            return self.with_event('save', {
                editor: self.editor,
                form: self.editor.form,
                cancel: false,
            }, function () {
                return self.editor.save().then(function (attrs) {
                    var created = false;
                    var record = self.records.get(attrs.id);
                    if (!record) {
                        // new record
                        created = true;
                        record = self.records.find(function (r) {
                            return !r.get('id');
                        }).set('id', attrs.id);
                    }
                    // onwrite callback could be altering & reloading the
                    // record which has *just* been saved, so first perform all
                    // onwrites then do a final reload of the record
                    return self.cancel_edition(true)
                        .then(function() {
                            return self.handle_onwrite(record);
                        })
                        .then(function () {
                            return self.reload_record(record);
                        })
                        .then(function () {
                            return {created: created, record: record};
                        });
                }, function() {
                    return self.cancel_edition();
                });
            });
        });
    },
    /**
     * @param {Boolean} [force] force the line to be discarded (even if there was changes)
     * @return {jQuery.Deferred}
     */
    cancel_edition: function (force) {
        var self = this;
        return this.with_event('cancel', {
            editor: this.editor,
            form: this.editor.form,
            cancel: false
        }, function () {
            return this.editor.cancel(force).then(function (attrs) {
                if (attrs.id) {
                    var record = self.records.get(attrs.id);
                    if (!record) {
                        return; // Record removed by third party during edition
                    }
                    return self.reload_record(record, {do_not_evict: true});
                }
                var to_delete = self.records.find(function (r) {
                    return !r.get('id');
                });
                if (to_delete) {
                    self.records.remove(to_delete);
                }
            });
        });
    },
    /**
     * Executes an action on the view's editor bracketed by a cancellable
     * event of the name provided.
     *
     * The event name provided will be post-fixed with ``:before`` and
     * ``:after``, the ``event`` parameter will be passed alongside the
     * ``:before`` variant and if the parameter's ``cancel`` key is set to
     * ``true`` the action *will not be called* and the method will return
     * a rejection
     *
     * @param {String} event_name name of the event
     * @param {Object} event event object, provided to ``:before`` sub-event
     * @param {Function} action callable, called with the view's editor as its context
     * @param {Array} [args] supplementary arguments provided to the action
     * @param {Array} [trigger_params] supplementary arguments provided to the ``:after`` sub-event, before anything fetched by the ``action`` function
     * @return {jQuery.Deferred}
     */
    with_event: function (event_name, event, action) {
        var self = this;
        event = event || {};
        this.trigger(event_name + ':before', event);
        if (event.cancel) {
            return $.Deferred().reject({
                message: _.str.sprintf("Event %s:before cancelled",
                                       event_name)});
        }
        return $.when(action.call(this)).done(function () {
            self.trigger.apply(self, [event_name + ':after']
                    .concat(_.toArray(arguments)));
        });
    },
    edition_view: function (editor) {
        var view = $.extend(true, {}, this.fields_view);
        view.arch.tag = 'form';
        _.extend(view.arch.attrs, {
            'class': 'oe_form_container',
            version: '7.0'
        });
        _(view.arch.children).chain()
            .zip(_(this.columns).filter(function (c) {
                return !(c instanceof ListView.MetaColumn);}))
            .each(function (ar) {
                var widget = ar[0], column = ar[1];
                var modifiers = _.extend({}, column.modifiers);
                widget.attrs.nolabel = true;
                if (modifiers['tree_invisible'] || widget.tag === 'button') {
                    modifiers.invisible = true;
                }
                widget.attrs.modifiers = JSON.stringify(modifiers);
            });
        return view;
    },
    handle_onwrite: function (source_record) {
        var self = this;
        var on_write_callback = self.fields_view.arch.attrs.on_write;
        if (!on_write_callback) {
            return $.when();
        }
        var context = new data.CompoundContext(self.dataset.get_context(), {'on_write_domain': self.dataset.domain}).eval();
        return this.dataset.call(on_write_callback, [source_record.get('id'), context])
            .then(function (ids) {
                return $.when.apply(null, _(ids).map(_.bind(self.handle_onwrite_record, self, source_record)));
            });
    },
    handle_onwrite_record: function (source_record, id) {
        var record = this.records.get(id);
        if (!record) {
            // insert after the source record
            var index = this.records.indexOf(source_record) + 1;
            record = this.make_empty_record(id);
            this.records.add(record, {at: index});
        }
        return this.reload_record(record);
    },
    prepends_on_create: function () {
        return (this.editable() === 'top');
    },
    setup_events: function () {
        var self = this;
        _.each(this.editor.form.fields, function(field, field_name) {
            field.on("change:effective_readonly", self, function(){
                var item = _(self.fields_for_resize).find(function (item) {
                    return item.field === field;
                });
                if (item) {
                    setTimeout(function() {
                        self.resize_field(item.field, item.cell);
                    }, 0);
                }
                 
            });
        });

        this.editor.$el.on('keyup keypress keydown', function (e) {
            if (!self.editor.is_editing()) { return true; }
            var key = _($.ui.keyCode).chain()
                .map(function (v, k) { return {name: k, code: v}; })
                .find(function (o) { return o.code === e.which; })
                .value();
            if (!key) { return true; }
            var method = e.type + '_' + key.name;
            if (!(method in self)) { return true; }
            return self[method](e);
        });
    },
    /**
     * Saves the current record, and goes to the next one (creation or
     * edition)
     *
     * @private
     * @param {String} [next_record='succ'] method to call on the records collection to get the next record to edit
     * @param {Object} [options]
     * @param {String} [options.focus_field]
     * @return {*}
     */
    _next: function (next_record, options) {
        next_record = next_record || 'succ';
        var self = this;
        return this.save_edition().then(function (saveInfo) {
            if (!saveInfo) { return null; }
            if (saveInfo.created) {
                return self.start_edition();
            }
            var record = self.records[next_record](saveInfo.record, {wraparound: true});
            return self.start_edition(record, options);
        });
    },
    keyup_ENTER: function () {
        return this._next();
    },
    keydown_ESCAPE: function (e) {
        return false;
    },
    keyup_ESCAPE: function (e) {
        return this.cancel_edition();
    },
    /**
     * Gets the selection range (start, end) for the provided element,
     * returns ``null`` if it can't get a range.
     *
     * @private
     */
    _text_selection_range: function (el) {
        var selectionStart;
        try {
            selectionStart = el.selectionStart;
        } catch (e) {
            // radio or checkbox throw on selectionStart access
            return null;
        }
        if (selectionStart !== undefined) {
            return {
                start: selectionStart,
                end: el.selectionEnd
            };
        } else if (document.body.createTextRange) {
            throw new Error("Implement text range handling for MSIE");
        }
        // Element without selection ranges (select, div/@contenteditable)
        return null;
    },
    _text_cursor: function (el) {
        var selection = this._text_selection_range(el);
        if (!selection) {
            return null;
        }
        if (selection.start !== selection.end) {
            return {position: null, collapsed: false};
        }
        return {position: selection.start, collapsed: true};
    },
    /**
     * Checks if the cursor is at the start of the provided el
     *
     * @param {HTMLInputElement | HTMLTextAreaElement}
     * @returns {Boolean}
     * @private
     */
    _at_start: function (cursor, el) {
        return cursor.collapsed && (cursor.position === 0);
    },
    /**
     * Checks if the cursor is at the end of the provided el
     *
     * @param {HTMLInputElement | HTMLTextAreaElement}
     * @returns {Boolean}
     * @private
     */
    _at_end: function (cursor, el) {
        return cursor.collapsed && (cursor.position === el.value.length);
    },
    /**
     * @param DOMEvent event
     * @param {String} record_direction direction to move into to get the next record (pred | succ)
     * @param {Function} is_valid_move whether the edition should be moved to the next record
     * @private
     */
    _key_move_record: function (event, record_direction, is_valid_move) {
        if (!this.editor.is_editing() || this.editor.is_creating()) { return $.when(); }
        var cursor = this._text_cursor(event.target);
        // if text-based input (has a cursor)
        //    and selecting (not collapsed) or not at a field boundary
        //        don't move to the next record
        if (cursor && !is_valid_move(event.target, cursor)) { return $.when(); }

        event.preventDefault();
        var source_field = $(event.target).closest('[data-fieldname]')
                .attr('data-fieldname');
        return this._next(record_direction, {focus_field: source_field});

    },
    keyup_UP: function (e) {
        var self = this;
        return this._key_move_record(e, 'pred', function (el, cursor) {
            return self._at_start(cursor, el);
        });
    },
    keyup_DOWN: function (e) {
        var self = this;
        return this._key_move_record(e, 'succ', function (el, cursor) {
            return self._at_end(cursor, el);
        });
    },

    keydown_LEFT: function (e) {
        // If the cursor is at the beginning of the field
        var source_field = $(e.target).closest('[data-fieldname]')
                .attr('data-fieldname');
        var cursor = this._text_cursor(e.target);
        if (cursor && !this._at_start(cursor, e.target)) { return $.when(); }

        var fields_order = this.editor.form.fields_order;
        var field_index = _(fields_order).indexOf(source_field);

        // Look for the closest visible form field to the left
        var fields = this.editor.form.fields;
        var field;
        do {
            if (--field_index < 0) { return $.when(); }

            field = fields[fields_order[field_index]];
        } while (!field.$el.is(':visible'));

        // and focus it
        field.focus();
        return $.when();
    },
    keydown_RIGHT: function (e) {
        // same as above, but with cursor at the end of the field and
        // looking for new fields at the right
        var source_field = $(e.target).closest('[data-fieldname]')
                .attr('data-fieldname');
        var cursor = this._text_cursor(e.target);
        if (cursor && !this._at_end(cursor, e.target)) { return $.when(); }

        var fields_order = this.editor.form.fields_order;
        var field_index = _(fields_order).indexOf(source_field);

        var fields = this.editor.form.fields;
        var field;
        do {
            if (++field_index >= fields_order.length) { return $.when(); }

            field = fields[fields_order[field_index]];
        } while (!field.$el.is(':visible'));

        field.focus();
        return $.when();
    },
    keydown_TAB: function (e) { // Keydown and not keyup because this handler must be called before the browser has focused the next field
        var form = this.editor.form;
        var last_field = _(form.fields_order).chain()
            .map(function (name) { return form.fields[name]; })
            .filter(function (field) { return field.$el.is(':visible') && !field.get('effective_readonly'); })
            .last()
            .value();
        // tabbed from last field in form
        if (last_field && $(e.target).closest(last_field.el).length) {
            e.preventDefault();
            return this._next();
        }
        this.editor.form.__clicked_inside = true;
        return $.when();
    },
});


ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{
    passthrough_events: ListView.Groups.prototype.passthrough_events + " edit saved",
    get_row_for: function (record) {
        return _(this.children).chain()
            .invoke('get_row_for', record)
            .compact()
            .first()
            .value();
    }
});

ListView.List.include(/** @lends instance.web.ListView.List# */{
    row_clicked: function (event) {
        if (!this.view.editable() || !this.view.is_action_enabled('edit')) {
            return this._super.apply(this, arguments);
        }

        var self = this;
        var args = arguments;
        var _super = self._super;

        var record_id = $(event.currentTarget).data('id');
        return this.view.start_edition(
            ((record_id)? this.records.get(record_id) : null), {
            focus_field: $(event.target).not(".oe_readonly").data('field'),
        }).fail(function() {
            return _super.apply(self, args); // The record can't be edited so open it in a modal (use-case: readonly mode)
        });
    },
    /**
     * If a row mapping to the record (@data-id matching the record's id or
     * no @data-id if the record has no id), returns it. Otherwise returns
     * ``null``.
     *
     * @param {Record} record the record to get a row for
     * @return {jQuery|null}
     */
    get_row_for: function (record) {
        var $row = this.$current.children('[data-id=' + record.get('id') + ']');
        return (($row.length)? $row : null);
    },
});

return Editor;

});