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