"static/src/js/views.js",
"static/src/js/data.js",
"static/src/js/data_export.js",
- "static/src/js/form.js",
- "static/src/js/list.js",
- "static/src/js/list-editable.js",
"static/src/js/search.js",
+ "static/src/js/view_form.js",
+ "static/src/js/view_list.js",
+ "static/src/js/view_list_editable.js",
"static/src/js/view_tree.js",
],
'css' : [
+++ /dev/null
-openerp.web.form = function (openerp) {
-
-var _t = openerp.web._t;
-var QWeb = openerp.web.qweb;
-
-openerp.web.views.add('form', 'openerp.web.FormView');
-openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# */{
- /**
- * Indicates that this view is not searchable, and thus that no search
- * view should be displayed (if there is one active).
- */
- searchable: false,
- template: "FormView",
- /**
- * @constructs openerp.web.FormView
- * @extends openerp.web.View
- *
- * @param {openerp.web.Session} session the current openerp session
- * @param {String} element_id this view's root element id
- * @param {openerp.web.DataSet} dataset the dataset this view will work with
- * @param {String} view_id the identifier of the OpenERP view object
- *
- * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance
- */
- init: function(parent, element_id, dataset, view_id, options) {
- this._super(parent, element_id);
- this.set_default_options(options);
- this.dataset = dataset;
- this.model = dataset.model;
- this.view_id = view_id;
- this.fields_view = {};
- this.widgets = {};
- this.widgets_counter = 0;
- this.fields = {};
- this.datarecord = {};
- this.ready = false;
- this.show_invalid = true;
- this.dirty = false;
- this.default_focus_field = null;
- this.default_focus_button = null;
- this.registry = openerp.web.form.widgets;
- this.has_been_loaded = $.Deferred();
- this.$form_header = null;
- this.translatable_fields = [];
- _.defaults(this.options, {"always_show_new_button": true});
- },
- start: function() {
- if (this.embedded_view) {
- var def = $.Deferred().then(this.on_loaded);
- var self = this;
- setTimeout(function() {def.resolve(self.embedded_view);}, 0);
- return def.promise();
- } else {
- var context = new openerp.web.CompoundContext(this.dataset.get_context());
- return this.rpc("/web/view/load", {
- "model": this.model,
- "view_id": this.view_id,
- "view_type": "form",
- toolbar: this.options.sidebar,
- context: context
- }, this.on_loaded);
- }
- },
- stop: function() {
- if (this.sidebar) {
- this.sidebar.attachments.stop();
- this.sidebar.stop();
- }
- _.each(this.widgets, function(w) {
- w.stop();
- });
- },
- on_loaded: function(data) {
- var self = this;
- this.fields_view = data;
- var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
-
- this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this }));
- _.each(this.widgets, function(w) {
- w.start();
- });
- this.$form_header = this.$element.find('#' + this.element_id + '_header');
- this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() {
- var action = $(this).data('pager-action');
- self.on_pager_action(action);
- });
-
- this.$form_header.find('button.oe_form_button_save').click(this.do_save);
- this.$form_header.find('button.oe_form_button_save_edit').click(this.do_save_edit);
- this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel);
- this.$form_header.find('button.oe_form_button_new').click(this.on_button_new);
-
- if (this.options.sidebar && this.options.sidebar_id) {
- this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
- this.sidebar.start();
- this.sidebar.do_unfold();
- this.sidebar.attachments = new openerp.web.form.SidebarAttachments(this.sidebar, this.sidebar.add_section('attachments', "Attachments"), this);
- this.sidebar.add_toolbar(this.fields_view.toolbar);
- this.set_common_sidebar_sections(this.sidebar);
- }
- this.has_been_loaded.resolve();
- },
- do_show: function () {
- var promise;
- if (this.dataset.index === null) {
- // null index means we should start a new record
- promise = this.on_button_new();
- } else {
- promise = this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
- }
- this.$element.show();
- if (this.sidebar) {
- this.sidebar.$element.show();
- }
- return promise;
- },
- do_hide: function () {
- this.$element.hide();
- if (this.sidebar) {
- this.sidebar.$element.hide();
- }
- },
- on_record_loaded: function(record) {
- if (!record) {
- throw("Form: No record received");
- }
- if (!record.id) {
- this.$form_header.find('.oe_form_on_create').show();
- this.$form_header.find('.oe_form_on_update').hide();
- if (!this.options["always_show_new_button"]) {
- this.$form_header.find('button.oe_form_button_new').hide();
- }
- } else {
- this.$form_header.find('.oe_form_on_create').hide();
- this.$form_header.find('.oe_form_on_update').show();
- this.$form_header.find('button.oe_form_button_new').show();
- }
- this.dirty = false;
- this.datarecord = record;
- for (var f in this.fields) {
- var field = this.fields[f];
- field.dirty = false;
- field.set_value(this.datarecord[f] || false);
- field.validate();
- }
- if (!record.id) {
- // New record: Second pass in order to trigger the onchanges
- this.dirty = true;
- this.show_invalid = false;
- for (var f in record) {
- var field = this.fields[f];
- if (field) {
- field.dirty = true;
- this.do_onchange(field);
- }
- }
- }
- this.on_form_changed();
- this.show_invalid = this.ready = true;
- this.do_update_pager(record.id == null);
- if (this.sidebar) {
- this.sidebar.attachments.do_update();
- this.sidebar.$element.find('.oe_sidebar_translate').toggleClass('oe_hide', !record.id);
- }
- if (this.default_focus_field && !this.embedded_view) {
- this.default_focus_field.focus();
- }
- },
- on_form_changed: function() {
- for (var w in this.widgets) {
- w = this.widgets[w];
- w.process_modifiers();
- w.update_dom();
- }
- },
- on_pager_action: function(action) {
- switch (action) {
- case 'first':
- this.dataset.index = 0;
- break;
- case 'previous':
- this.dataset.previous();
- break;
- case 'next':
- this.dataset.next();
- break;
- case 'last':
- this.dataset.index = this.dataset.ids.length - 1;
- break;
- }
- this.reload();
- },
- do_update_pager: function(hide_index) {
- var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager');
- var index = hide_index ? '-' : this.dataset.index + 1;
- $pager.find('span.oe_pager_index').html(index);
- $pager.find('span.oe_pager_count').html(this.dataset.ids.length);
- },
- do_onchange: function(widget, processed) {
- processed = processed || [];
- if (widget.node.attrs.on_change) {
- var self = this;
- this.ready = false;
- var onchange = _.trim(widget.node.attrs.on_change);
- var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
- if (call) {
- var method = call[1], args = [];
- var context_index = null;
- var argument_replacement = {
- 'False' : function() {return false;},
- 'True' : function() {return true;},
- 'None' : function() {return null;},
- 'context': function(i) {
- context_index = i;
- var ctx = widget.build_context ? widget.build_context() : {};
- return ctx;
- }
- };
- var parent_fields = null;
- _.each(call[2].split(','), function(a, i) {
- var field = _.trim(a);
- if (field in argument_replacement) {
- args.push(argument_replacement[field](i));
- return;
- } else if (self.fields[field]) {
- var value = self.fields[field].get_on_change_value();
- args.push(value == null ? false : value);
- return;
- } else {
- var splitted = field.split('.');
- if (splitted.length > 1 && _.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
- if (parent_fields === null) {
- parent_fields = self.dataset.parent_view.get_fields_values();
- }
- var p_val = parent_fields[_.trim(splitted[1])];
- if (p_val !== undefined) {
- args.push(p_val == null ? false : p_val);
- return;
- }
- }
- }
- throw "Could not get field with name '" + field +
- "' for onchange '" + onchange + "'";
- });
- var ajax = {
- url: '/web/dataset/call',
- async: false
- };
- return this.rpc(ajax, {
- model: this.dataset.model,
- method: method,
- args: [(this.datarecord.id == null ? [] : [this.datarecord.id])].concat(args),
- context_id: context_index === null ? null : context_index + 1
- }, function(response) {
- self.on_processed_onchange(response, processed);
- });
- } else {
- console.log("Wrong on_change format", on_change);
- }
- }
- },
- on_processed_onchange: function(response, processed) {
- var result = response;
- if (result.value) {
- for (var f in result.value) {
- var field = this.fields[f];
- // If field is not defined in the view, just ignore it
- if (field) {
- var value = result.value[f];
- processed.push(field.name);
- if (field.get_value() != value) {
- field.set_value(value);
- field.dirty = true;
- if (_.indexOf(processed, field.name) < 0) {
- this.do_onchange(field, processed);
- }
- }
- }
- }
- this.on_form_changed();
- }
- if (!_.isEmpty(result.warning)) {
- $(QWeb.render("DialogWarning", result.warning)).dialog({
- modal: true,
- buttons: {
- Ok: function() {
- $(this).dialog("close");
- }
- }
- });
- }
- if (result.domain) {
- // TODO:
- }
- this.ready = true;
- },
- on_button_new: function() {
- var self = this;
- var def = $.Deferred();
- $.when(this.has_been_loaded).then(function() {
- self.dataset.default_get(
- _.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() {
- def.resolve();
- });
- });
- return def.promise();
- },
- /**
- * 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;
- }
- var form_dirty = false,
- form_invalid = false,
- values = {},
- first_invalid_field = null;
- for (var f in this.fields) {
- f = this.fields[f];
- if (!f.is_valid()) {
- form_invalid = true;
- f.update_dom();
- if (!first_invalid_field) {
- first_invalid_field = f;
- }
- } else if (f.is_dirty()) {
- form_dirty = true;
- values[f.name] = f.get_value();
- }
- }
- if (form_invalid) {
- first_invalid_field.focus();
- this.on_invalid();
- return false;
- } else if (form_dirty) {
- console.log("About to save", values);
- if (!this.datarecord.id) {
- return this.dataset.create(values, function(r) {
- self.on_created(r, success, prepend_on_create);
- });
- } else {
- return this.dataset.write(this.datarecord.id, values, {}, function(r) {
- self.on_saved(r, success);
- });
- }
- } else {
- setTimeout(function() {
- self.on_saved({ result: true }, success);
- });
- return true;
- }
- },
- do_save_edit: function() {
- this.do_save();
- //this.switch_readonly(); Use promises
- },
- switch_readonly: function() {
- },
- switch_editable: function() {
- },
- on_invalid: function() {
- var msg = "<ul>";
- _.each(this.fields, function(f) {
- if (!f.is_valid()) {
- msg += "<li>" + f.string + "</li>";
- }
- });
- msg += "</ul>";
- this.notification.warn("The following fields are invalid :", msg);
- },
- on_saved: function(r, success) {
- if (!r.result) {
- // should not happen in the server, but may happen for internal purpose
- } else {
- console.debug(_.sprintf("The record #%s has been saved.", this.datarecord.id));
- if (success) {
- success(r);
- }
- this.reload();
- }
- },
- /**
- * 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) {
- // should not happen in the server, but may happen for internal purpose
- } else {
- 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.do_update_pager();
- if (this.sidebar) {
- this.sidebar.attachments.do_update();
- }
- console.debug("The record has been created with id #" + this.datarecord.id);
- if (success) {
- success(_.extend(r, {created: true}));
- }
- this.reload();
- }
- },
- do_search: function (domains, contexts, groupbys) {
- console.debug("Searching form");
- },
- on_action: function (action) {
- console.debug('Executing action', action);
- },
- do_cancel: function () {
- console.debug("Cancelling form");
- },
- reload: function() {
- if (this.dataset.index == null || this.dataset.index < 0) {
- this.on_button_new();
- } else {
- this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
- }
- },
- get_fields_values: function() {
- var values = {};
- _.each(this.fields, function(value, key) {
- var val = value.get_value();
- values[key] = val;
- });
- return values;
- },
- get_selected_ids: function() {
- var id = this.dataset.ids[this.dataset.index];
- return id ? [id] : [];
- }
-});
-openerp.web.FormDialog = openerp.web.Dialog.extend({
- init: function(parent, options, view_id, dataset) {
- this._super(parent, options);
- this.dataset = dataset;
- this.view_id = view_id;
- return this;
- },
- start: function() {
- this._super();
- this.form = new openerp.web.FormView(this, this.element_id, this.dataset, this.view_id, {
- sidebar: false,
- pager: false
- });
- this.form.start();
- this.form.on_created.add_last(this.on_form_dialog_saved);
- this.form.on_saved.add_last(this.on_form_dialog_saved);
- return this;
- },
- load_id: function(id) {
- var self = this;
- return this.dataset.read_ids([id], _.keys(this.form.fields_view.fields), function(records) {
- self.form.on_record_loaded(records[0]);
- });
- },
- on_form_dialog_saved: function(r) {
- this.close();
- }
-});
-
-/** @namespace */
-openerp.web.form = {};
-
-openerp.web.form.SidebarAttachments = openerp.web.Widget.extend({
- init: function(parent, element_id, form_view) {
- this._super(parent, element_id);
- this.view = form_view;
- },
- do_update: function() {
- if (!this.view.datarecord.id) {
- this.on_attachments_loaded([]);
- } else {
- (new openerp.web.DataSetSearch(
- this, 'ir.attachment', this.view.dataset.get_context(),
- [
- ['res_model', '=', this.view.dataset.model],
- ['res_id', '=', this.view.datarecord.id],
- ['type', 'in', ['binary', 'url']]
- ])).read_slice(['name', 'url', 'type'], {}, this.on_attachments_loaded);
- }
- },
- on_attachments_loaded: function(attachments) {
- this.attachments = attachments;
- this.$element.html(QWeb.render('FormView.sidebar.attachments', this));
- this.$element.find('.oe-binary-file').change(this.on_attachment_changed);
- this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete);
- },
- on_attachment_changed: function(e) {
- window[this.element_id + '_iframe'] = this.do_update;
- var $e = $(e.target);
- if ($e.val() != '') {
- this.$element.find('form.oe-binary-form').submit();
- $e.parent().find('input[type=file]').attr('disabled', 'true');
- $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle();
- }
- },
- on_attachment_delete: function(e) {
- var self = this, $e = $(e.currentTarget);
- var name = _.trim($e.parent().find('a.oe-sidebar-attachments-link').text());
- if (confirm("Do you really want to delete the attachment " + name + " ?")) {
- this.rpc('/web/dataset/unlink', {
- model: 'ir.attachment',
- ids: [parseInt($e.attr('data-id'))]
- }, function(r) {
- $e.parent().remove();
- self.notification.notify("Delete an attachment", "The attachment '" + name + "' has been deleted");
- });
- }
- }
-});
-
-openerp.web.form.compute_domain = function(expr, fields) {
- var stack = [];
- for (var i = expr.length - 1; i >= 0; i--) {
- var ex = expr[i];
- if (ex.length == 1) {
- var top = stack.pop();
- switch (ex) {
- case '|':
- stack.push(stack.pop() || top);
- continue;
- case '&':
- stack.push(stack.pop() && top);
- continue;
- case '!':
- stack.push(!top);
- continue;
- default:
- throw new Error('Unknown domain operator ' + ex);
- }
- }
-
- var field = fields[ex[0]];
- if (!field) {
- throw new Error("Domain references unknown field : " + ex[0]);
- }
- var field_value = field.get_value ? fields[ex[0]].get_value() : fields[ex[0]].value;
- var op = ex[1];
- var val = ex[2];
-
- switch (op.toLowerCase()) {
- case '=':
- case '==':
- stack.push(field_value == val);
- break;
- case '!=':
- case '<>':
- stack.push(field_value != val);
- break;
- case '<':
- stack.push(field_value < val);
- break;
- case '>':
- stack.push(field_value > val);
- break;
- case '<=':
- stack.push(field_value <= val);
- break;
- case '>=':
- stack.push(field_value >= val);
- break;
- case 'in':
- stack.push(_(val).contains(field_value));
- break;
- case 'not in':
- stack.push(!_(val).contains(field_value));
- break;
- default:
- console.log("Unsupported operator in modifiers :", op);
- }
- }
- return _.all(stack, _.identity);
-};
-
-openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{
- template: 'Widget',
- /**
- * @constructs openerp.web.form.Widget
- * @extends openerp.web.Widget
- *
- * @param view
- * @param node
- */
- init: function(view, node) {
- this.view = view;
- this.node = node;
- this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
- this.type = this.type || node.tag;
- this.element_name = this.element_name || this.type;
- this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_");
-
- this._super(view, this.element_id);
-
- this.view.widgets[this.element_id] = this;
- this.children = node.children;
- this.colspan = parseInt(node.attrs.colspan || 1, 10);
- this.decrease_max_width = 0;
-
- this.string = this.string || node.attrs.string;
- this.help = this.help || node.attrs.help;
- this.invisible = this.modifiers['invisible'] === true;
- this.classname = 'oe_form_' + this.type;
-
- this.width = this.node.attrs.width;
- },
- start: function() {
- this.$element = $('#' + this.element_id);
- },
- stop: function() {
- if (this.$element) {
- this.$element.remove();
- }
- },
- process_modifiers: function() {
- var compute_domain = openerp.web.form.compute_domain;
- for (var a in this.modifiers) {
- this[a] = compute_domain(this.modifiers[a], this.view.fields);
- }
- },
- update_dom: function() {
- this.$element.toggle(!this.invisible);
- },
- render: function() {
- var template = this.template;
- return QWeb.render(template, { "widget": this });
- }
-});
-
-openerp.web.form.WidgetFrame = openerp.web.form.Widget.extend({
- template: 'WidgetFrame',
- init: function(view, node) {
- this._super(view, node);
- this.columns = parseInt(node.attrs.col || 4, 10);
- this.x = 0;
- this.y = 0;
- this.table = [];
- this.add_row();
- for (var i = 0; i < node.children.length; i++) {
- var n = node.children[i];
- if (n.tag == "newline") {
- this.add_row();
- } else {
- this.handle_node(n);
- }
- }
- this.set_row_cells_with(this.table[this.table.length - 1]);
- },
- add_row: function(){
- if (this.table.length) {
- this.set_row_cells_with(this.table[this.table.length - 1]);
- }
- var row = [];
- this.table.push(row);
- this.x = 0;
- this.y += 1;
- return row;
- },
- set_row_cells_with: function(row) {
- var bypass = 0,
- max_width = 100;
- for (var i = 0; i < row.length; i++) {
- bypass += row[i].width === undefined ? 0 : 1;
- max_width -= row[i].decrease_max_width;
- }
- var size_unit = Math.round(max_width / (this.columns - bypass)),
- colspan_sum = 0;
- for (var i = 0; i < row.length; i++) {
- var w = row[i];
- colspan_sum += w.colspan;
- if (w.width === undefined) {
- var width = (i === row.length - 1 && colspan_sum === this.columns) ? max_width : Math.round(size_unit * w.colspan);
- max_width -= width;
- w.width = width + '%';
- }
- }
- },
- handle_node: function(node) {
- var type = {};
- if (node.tag == 'field') {
- type = this.view.fields_view.fields[node.attrs.name] || {};
- }
- var widget = new (this.view.registry.get_any(
- [node.attrs.widget, type.type, node.tag])) (this.view, node);
- if (node.tag == 'field') {
- if (!this.view.default_focus_field || node.attrs.default_focus == '1') {
- this.view.default_focus_field = widget;
- }
- if (node.attrs.nolabel != '1') {
- var label = new (this.view.registry.get_object('label')) (this.view, node);
- label["for"] = widget;
- this.add_widget(label, widget.colspan + 1);
- }
- }
- this.add_widget(widget);
- },
- add_widget: function(widget, colspan) {
- colspan = colspan || widget.colspan;
- var current_row = this.table[this.table.length - 1];
- if (current_row.length && (this.x + colspan) > this.columns) {
- current_row = this.add_row();
- }
- current_row.push(widget);
- this.x += widget.colspan;
- return widget;
- }
-});
-
-openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({
- template: 'WidgetNotebook',
- init: function(view, node) {
- this._super(view, node);
- this.pages = [];
- for (var i = 0; i < node.children.length; i++) {
- var n = node.children[i];
- if (n.tag == "page") {
- var page = new openerp.web.form.WidgetNotebookPage(this.view, n, this, this.pages.length);
- this.pages.push(page);
- }
- }
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$element.tabs();
- this.view.on_button_new.add_last(this.do_select_first_visible_tab);
- },
- do_select_first_visible_tab: function() {
- for (var i = 0; i < this.pages.length; i++) {
- var page = this.pages[i];
- if (page.invisible === false) {
- this.$element.tabs('select', page.index);
- break;
- }
- }
- }
-});
-
-openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({
- template: 'WidgetNotebookPage',
- init: function(view, node, notebook, index) {
- this.notebook = notebook;
- this.index = index;
- this.element_name = 'page_' + index;
- this._super(view, node);
- this.element_tab_id = this.element_id + '_tab';
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$element_tab = $('#' + this.element_tab_id);
- },
- update_dom: function() {
- if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) {
- this.notebook.do_select_first_visible_tab();
- }
- this.$element_tab.toggle(!this.invisible);
- this.$element.toggle(!this.invisible);
- }
-});
-
-openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "WidgetSeparator";
- this.orientation = node.attrs.orientation || 'horizontal';
- if (this.orientation === 'vertical') {
- this.width = '1';
- }
- this.classname += '_' + this.orientation;
- }
-});
-
-openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "WidgetButton";
- if (this.string) {
- // We don't have button key bindings in the webclient
- this.string = this.string.replace(/_/g, '');
- }
- if (node.attrs.default_focus == '1') {
- // TODO fme: provide enter key binding to widgets
- this.view.default_focus_button = this;
- }
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$element.click(this.on_click);
- },
- on_click: function(saved) {
- var self = this;
- if (!this.node.attrs.special && this.view.dirty && saved !== true) {
- this.view.do_save(function() {
- self.on_click(true);
- });
- } else {
- if (this.node.attrs.confirm) {
- var dialog = $('<div>' + this.node.attrs.confirm + '</div>').dialog({
- title: 'Confirm',
- modal: true,
- buttons: {
- Ok: function() {
- self.on_confirmed();
- $(this).dialog("close");
- },
- Cancel: function() {
- $(this).dialog("close");
- }
- }
- });
- } else {
- this.on_confirmed();
- }
- }
- },
- on_confirmed: function() {
- var self = this;
-
- this.view.do_execute_action(
- this.node.attrs, this.view.dataset, this.view.datarecord.id, function () {
- self.view.reload();
- });
- }
-});
-
-openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
- init: function(view, node) {
- this.element_name = 'label_' + node.attrs.name;
-
- this._super(view, node);
-
- // TODO fme: support for attrs.align
- if (this.node.tag == 'label' && (this.node.attrs.colspan || (this.string && this.string.length > 32))) {
- this.template = "WidgetParagraph";
- this.colspan = parseInt(this.node.attrs.colspan || 1, 10);
- } else {
- this.template = "WidgetLabel";
- this.colspan = 1;
- this.width = '1%';
- this.decrease_max_width = 1;
- this.nowrap = true;
- }
- },
- render: function () {
- if (this['for'] && this.type !== 'label') {
- return QWeb.render(this.template, {widget: this['for']});
- }
- // Actual label widgets should not have a false and have type label
- return QWeb.render(this.template, {widget: this});
- },
- start: function() {
- this._super();
- var self = this;
- this.$element.find("label").dblclick(function() {
- var widget = self['for'] || self;
- console.log(widget.element_id , widget);
- window.w = widget;
- });
- }
-});
-
-openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.form.Field# */{
- /**
- * @constructs openerp.web.form.Field
- * @extends openerp.web.form.Widget
- *
- * @param view
- * @param node
- */
- init: function(view, node) {
- this.name = node.attrs.name;
- this.value = undefined;
- view.fields[this.name] = this;
- this.type = node.attrs.widget || view.fields_view.fields[node.attrs.name].type;
- this.element_name = "field_" + this.name + "_" + this.type;
-
- this._super(view, node);
-
- if (node.attrs.nolabel != '1' && this.colspan > 1) {
- this.colspan--;
- }
- this.field = view.fields_view.fields[node.attrs.name] || {};
- this.string = node.attrs.string || this.field.string;
- this.help = node.attrs.help || this.field.help;
- this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1';
- this.readonly = this.modifiers['readonly'] === true;
- this.required = this.modifiers['required'] === true;
- this.invalid = false;
- this.dirty = false;
-
- this.classname = 'oe_form_field_' + this.type;
- },
- start: function() {
- this._super.apply(this, arguments);
- if (this.field.translate) {
- this.view.translatable_fields.push(this);
- this.$element.find('.oe_field_translate').click(this.on_translate);
- }
- },
- set_value: function(value) {
- this.value = value;
- this.invalid = false;
- this.update_dom();
- this.on_value_changed();
- },
- set_value_from_ui: function() {
- this.on_value_changed();
- },
- on_value_changed: function() {
- },
- on_translate: function() {
- this.view.open_translate_dialog(this);
- },
- get_value: function() {
- return this.value;
- },
- is_valid: function() {
- return !this.invalid;
- },
- is_dirty: function() {
- return this.dirty;
- },
- get_on_change_value: function() {
- return this.get_value();
- },
- update_dom: function() {
- this._super.apply(this, arguments);
- if (this.field.translate) {
- this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id);
- }
- if (!this.disable_utility_classes) {
- this.$element.toggleClass('disabled', this.readonly);
- this.$element.toggleClass('required', this.required);
- if (this.view.show_invalid) {
- this.$element.toggleClass('invalid', !this.is_valid());
- }
- }
- },
- on_ui_change: function() {
- this.dirty = this.view.dirty = true;
- this.validate();
- if (this.is_valid()) {
- this.set_value_from_ui();
- this.view.do_onchange(this);
- this.view.on_form_changed();
- } else {
- this.update_dom();
- }
- },
- validate: function() {
- this.invalid = false;
- },
- focus: function() {
- },
- _build_view_fields_values: function() {
- var a_dataset = this.view.dataset || {};
- var fields_values = this.view.get_fields_values();
- var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {};
- fields_values.parent = parent_values;
- return fields_values;
- },
- /**
- * Builds a new context usable for operations related to fields by merging
- * the fields'context with the action's context.
- */
- build_context: function() {
- // I previously belevied contexts should be herrited, but now I doubt it
- //var a_context = this.view.dataset.get_context() || {};
- var f_context = this.field.context || null;
- // maybe the default_get should only be used when we do a default_get?
- var v_context1 = this.node.attrs.default_get || {};
- var v_context2 = this.node.attrs.context || {};
- var v_context = new openerp.web.CompoundContext(v_context1, v_context2);
- if (v_context1.__ref || v_context2.__ref || true) { //TODO niv: remove || true
- var fields_values = this._build_view_fields_values();
- v_context.set_eval_context(fields_values);
- }
- // if there is a context on the node, overrides the model's context
- var ctx = f_context || v_context;
- return ctx;
- },
- build_domain: function() {
- var f_domain = this.field.domain || null;
- var v_domain = this.node.attrs.domain || [];
- if (!(v_domain instanceof Array) || true) { //TODO niv: remove || true
- var fields_values = this._build_view_fields_values();
- v_domain = new openerp.web.CompoundDomain(v_domain).set_eval_context(fields_values);
- }
- // if there is a domain on the node, overrides the model's domain
- return f_domain || v_domain;
- }
-});
-
-openerp.web.form.FieldChar = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldChar";
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$element.find('input').change(this.on_ui_change);
- },
- set_value: function(value) {
- this._super.apply(this, arguments);
- var show_value = openerp.web.format_value(value, this, '');
- this.$element.find('input').val(show_value);
- },
- update_dom: function() {
- this._super.apply(this, arguments);
- this.$element.find('input').attr('disabled', this.readonly);
- },
- set_value_from_ui: function() {
- this.value = openerp.web.parse_value(this.$element.find('input').val(), this);
- this._super();
- },
- validate: function() {
- this.invalid = false;
- try {
- var value = openerp.web.parse_value(this.$element.find('input').val(), this, '');
- this.invalid = this.required && value === '';
- } catch(e) {
- this.invalid = true;
- }
- },
- focus: function() {
- this.$element.find('input').focus();
- }
-});
-
-openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldEmail";
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$element.find('button').click(this.on_button_clicked);
- },
- on_button_clicked: function() {
- if (!this.value || !this.is_valid()) {
- this.notification.warn("E-mail error", "Can't send email to invalid e-mail address");
- } else {
- location.href = 'mailto:' + this.value;
- }
- },
- set_value: function(value) {
- this._super.apply(this, arguments);
- this.$element.find('a').attr('href', 'mailto:' + this.$element.find('input').val());
- }
-});
-
-openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldUrl";
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$element.find('button').click(this.on_button_clicked);
- },
- on_button_clicked: function() {
- if (!this.value) {
- this.notification.warn("Resource error", "This resource is empty");
- } else {
- window.open(this.value);
- }
- }
-});
-
-openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({
- set_value: function(value) {
- if (value === false || value === undefined) {
- // As in GTK client, floats default to 0
- value = 0;
- this.dirty = true;
- }
- this._super.apply(this, [value]);
- }
-});
-
-openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldDate";
- this.jqueryui_object = 'datetimepicker';
- },
- start: function() {
- var self = this;
- this._super.apply(this, arguments);
- this.$element.find('input').change(this.on_ui_change);
- this.picker({
- onSelect: this.on_picker_select,
- changeMonth: true,
- changeYear: true,
- showWeek: true,
- showButtonPanel: false
- });
- this.$element.find('img.oe_datepicker_trigger').click(function() {
- if (!self.readonly) {
- self.picker('setDate', self.value || new Date());
- self.$element.find('.oe_datepicker').toggle();
- }
- });
- this.$element.find('.ui-datepicker-inline').removeClass('ui-widget-content ui-corner-all');
- this.$element.find('button.oe_datepicker_close').click(function() {
- self.$element.find('.oe_datepicker').hide();
- });
- },
- picker: function() {
- return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments);
- },
- on_picker_select: function(text, instance) {
- var date = this.picker('getDate');
- this.$element.find('input').val(date ? this.format_client(date) : '').change();
- },
- set_value: function(value) {
- value = this.parse(value);
- this._super(value);
- this.$element.find('input').val(value ? this.format_client(value) : '');
- },
- get_value: function() {
- return this.format(this.value);
- },
- set_value_from_ui: function() {
- var value = this.$element.find('input').val() || false;
- this.value = this.parse_client(value);
- this._super();
- },
- update_dom: function() {
- this._super.apply(this, arguments);
- this.$element.find('input').attr('disabled', this.readonly);
- this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', this.readonly);
- },
- validate: function() {
- this.invalid = false;
- var value = this.$element.find('input').val();
- if (value === "") {
- this.invalid = this.required;
- } else {
- try {
- this.parse_client(value);
- this.invalid = false;
- } catch(e) {
- this.invalid = true;
- }
- }
- },
- focus: function() {
- this.$element.find('input').focus();
- },
- parse: openerp.web.auto_str_to_date,
- parse_client: function(v) {
- return openerp.web.parse_value(v, this.field);
- },
- format: function(val) {
- return openerp.web.auto_date_to_str(val, this.field.type);
- },
- format_client: function(v) {
- return openerp.web.format_value(v, this.field);
- }
-});
-
-openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
- init: function(view, node) {
- this._super(view, node);
- this.jqueryui_object = 'datepicker';
- },
- on_picker_select: function(text, instance) {
- this._super(text, instance);
- this.$element.find('.oe_datepicker').hide();
- }
-});
-
-openerp.web.form.FieldText = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldText";
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$element.find('textarea').change(this.on_ui_change);
- },
- set_value: function(value) {
- this._super.apply(this, arguments);
- var show_value = openerp.web.format_value(value, this, '');
- this.$element.find('textarea').val(show_value);
- },
- update_dom: function() {
- this._super.apply(this, arguments);
- this.$element.find('textarea').attr('disabled', this.readonly);
- },
- set_value_from_ui: function() {
- this.value = openerp.web.parse_value(this.$element.find('textarea').val(), this);
- this._super();
- },
- validate: function() {
- this.invalid = false;
- try {
- var value = openerp.web.parse_value(this.$element.find('textarea').val(), this, '');
- this.invalid = this.required && value === '';
- } catch(e) {
- this.invalid = true;
- }
- },
- focus: function() {
- this.$element.find('textarea').focus();
- }
-});
-
-openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldBoolean";
- },
- start: function() {
- var self = this;
- this._super.apply(this, arguments);
- this.$element.find('input').click(function() {
- if ($(this).is(':checked') != self.value) {
- self.on_ui_change();
- }
- });
- },
- set_value: function(value) {
- this._super.apply(this, arguments);
- this.$element.find('input')[0].checked = value;
- },
- set_value_from_ui: function() {
- this.value = this.$element.find('input').is(':checked');
- this._super();
- },
- update_dom: function() {
- this._super.apply(this, arguments);
- this.$element.find('input').attr('disabled', this.readonly);
- },
- validate: function() {
- this.invalid = this.required && !this.$element.find('input').is(':checked');
- },
- focus: function() {
- this.$element.find('input').focus();
- }
-});
-
-openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldProgressBar";
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$element.find('div').progressbar({
- value: this.value,
- disabled: this.readonly
- });
- },
- set_value: function(value) {
- this._super.apply(this, arguments);
- var show_value = Number(value);
- if (isNaN(show_value)) {
- show_value = 0;
- }
- this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
- }
-});
-
-openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({
-// to replace view editor
-});
-
-openerp.web.form.FieldSelection = openerp.web.form.Field.extend({
- init: function(view, node) {
- var self = this;
- this._super(view, node);
- this.template = "FieldSelection";
- this.values = this.field.selection;
- _.each(this.values, function(v, i) {
- if (v[0] === false && v[1] === '') {
- self.values.splice(i, 1);
- }
- });
- this.values.unshift([false, '']);
- },
- start: function() {
- // Flag indicating whether we're in an event chain containing a change
- // event on the select, in order to know what to do on keyup[RETURN]:
- // * If the user presses [RETURN] as part of changing the value of a
- // selection, we should just let the value change and not let the
- // event broadcast further (e.g. to validating the current state of
- // the form in editable list view, which would lead to saving the
- // current row or switching to the next one)
- // * If the user presses [RETURN] with a select closed (side-effect:
- // also if the user opened the select and pressed [RETURN] without
- // changing the selected value), takes the action as validating the
- // row
- var ischanging = false;
- this._super.apply(this, arguments);
- this.$element.find('select')
- .change(this.on_ui_change)
- .change(function () { ischanging = true; })
- .click(function () { ischanging = false; })
- .keyup(function (e) {
- if (e.which !== 13 || !ischanging) { return; }
- e.stopPropagation();
- ischanging = false;
- });
- },
- set_value: function(value) {
- value = value === null ? false : value;
- value = value instanceof Array ? value[0] : value;
- this._super(value);
- var index = 0;
- for (var i = 0, ii = this.values.length; i < ii; i++) {
- if (this.values[i][0] === value) index = i;
- }
- this.$element.find('select')[0].selectedIndex = index;
- },
- set_value_from_ui: function() {
- this.value = this.values[this.$element.find('select')[0].selectedIndex][0];
- this._super();
- },
- update_dom: function() {
- this._super.apply(this, arguments);
- this.$element.find('select').attr('disabled', this.readonly);
- },
- validate: function() {
- var value = this.values[this.$element.find('select')[0].selectedIndex];
- this.invalid = !(value && !(this.required && value[0] === false));
- },
- focus: function() {
- this.$element.find('select').focus();
- }
-});
-
-// jquery autocomplete tweak to allow html
-(function() {
- var proto = $.ui.autocomplete.prototype,
- initSource = proto._initSource;
-
- function filter( array, term ) {
- var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
- return $.grep( array, function(value) {
- return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() );
- });
- }
-
- $.extend( proto, {
- _initSource: function() {
- if ( this.options.html && $.isArray(this.options.source) ) {
- this.source = function( request, response ) {
- response( filter( this.options.source, request.term ) );
- };
- } else {
- initSource.call( this );
- }
- },
-
- _renderItem: function( ul, item) {
- return $( "<li></li>" )
- .data( "item.autocomplete", item )
- .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
- .appendTo( ul );
- }
- });
-})();
-
-openerp.web.form.dialog = function(content, options) {
- options = _.extend({
- autoOpen: true,
- width: '90%',
- height: '90%',
- min_width: '800px',
- min_height: '600px'
- }, options || {});
- options.autoOpen = true;
- var dialog = new openerp.web.Dialog(null, options);
- dialog.$dialog = $(content).dialog(dialog.dialog_options);
- return dialog.$dialog;
-};
-
-openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldMany2One";
- this.limit = 7;
- this.value = null;
- this.cm_id = _.uniqueId('m2o_cm_');
- this.last_search = [];
- this.tmp_value = undefined;
- },
- start: function() {
- this._super();
- var self = this;
- this.$input = this.$element.find("input");
- this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
- this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
-
- // context menu
- var init_context_menu_def = $.Deferred().then(function(e) {
- var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context());
- rdataset.call("get", ['action', 'client_action_relate',
- [[self.field.relation, false]], false, rdataset.get_context()], false, 0)
- .then(function(result) {
- self.related_entries = result;
-
- var $cmenu = $("#" + self.cm_id);
- $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self}));
- var bindings = {};
- bindings[self.cm_id + "_search"] = function() {
- self._search_create_popup("search");
- };
- bindings[self.cm_id + "_create"] = function() {
- self._search_create_popup("form");
- };
- bindings[self.cm_id + "_open"] = function() {
- if (!self.value) {
- return;
- }
- var pop = new openerp.web.form.FormOpenPopup(self.view);
- pop.show_element(self.field.relation, self.value[0],self.build_context(), {});
- pop.on_write_completed.add_last(function() {
- self.set_value(self.value[0]);
- });
- };
- _.each(_.range(self.related_entries.length), function(i) {
- bindings[self.cm_id + "_related_" + i] = function() {
- self.open_related(self.related_entries[i]);
- };
- });
- var cmenu = self.$menu_btn.contextMenu(self.cm_id, {'leftClickToo': true,
- bindings: bindings, itemStyle: {"color": ""},
- onContextMenu: function() {
- if(self.value) {
- $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").removeClass("oe-m2o-disabled-cm");
- } else {
- $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").addClass("oe-m2o-disabled-cm");
- }
- return true;
- }, menuStyle: {width: "200px"}
- });
- setTimeout(function() {self.$menu_btn.trigger(e);}, 0);
- });
- });
- var ctx_callback = function(e) {init_context_menu_def.resolve(e); e.preventDefault()};
- this.$menu_btn.bind('contextmenu', ctx_callback);
- this.$menu_btn.click(ctx_callback);
-
- // some behavior for input
- this.$input.keyup(function() {
- if (self.$input.val() === "") {
- self._change_int_value(null);
- } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) {
- self._change_int_value(undefined);
- }
- });
- this.$drop_down.click(function() {
- if (self.$input.autocomplete("widget").is(":visible")) {
- self.$input.autocomplete("close");
- } else {
- if (self.value) {
- self.$input.autocomplete("search", "");
- } else {
- self.$input.autocomplete("search");
- }
- self.$input.focus();
- }
- });
- var anyoneLoosesFocus = function() {
- if (!self.$input.is(":focus") &&
- !self.$input.autocomplete("widget").is(":visible") &&
- !self.value) {
- if (self.value === undefined && self.last_search.length > 0) {
- self._change_int_ext_value(self.last_search[0]);
- } else {
- self._change_int_ext_value(null);
- }
- }
- };
- this.$input.focusout(anyoneLoosesFocus);
-
- var isSelecting = false;
- // autocomplete
- this.$input.autocomplete({
- source: function(req, resp) { self.get_search_result(req, resp); },
- select: function(event, ui) {
- isSelecting = true;
- var item = ui.item;
- if (item.id) {
- self._change_int_value([item.id, item.name]);
- } else if (item.action) {
- self._change_int_value(undefined);
- item.action();
- return false;
- }
- },
- focus: function(e, ui) {
- e.preventDefault();
- },
- html: true,
- close: anyoneLoosesFocus,
- minLength: 0,
- delay: 0
- });
- // used to correct a bug when selecting an element by pushing 'enter' in an editable list
- this.$input.keyup(function(e) {
- if (e.which === 13) {
- if (isSelecting)
- e.stopPropagation();
- }
- isSelecting = false;
- });
- },
- // autocomplete component content handling
- get_search_result: function(request, response) {
- var search_val = request.term;
- var self = this;
-
- var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
-
- dataset.name_search(search_val, self.build_domain(), 'ilike',
- this.limit + 1, function(data) {
- self.last_search = data;
- // possible selections for the m2o
- var values = _.map(data, function(x) {
- return {label: $('<span />').text(x[1]).html(), name:x[1], id:x[0]};
- });
-
- // search more... if more results that max
- if (values.length > self.limit) {
- values = values.slice(0, self.limit);
- values.push({label: _t("<em>Â Â Â Search More...</em>"), action: function() {
- dataset.name_search(search_val, self.build_domain(), 'ilike'
- , false, function(data) {
- self._change_int_value(null);
- self._search_create_popup("search", data);
- });
- }});
- }
- // quick create
- var raw_result = _(data.result).map(function(x) {return x[1];});
- if (search_val.length > 0 &&
- !_.include(raw_result, search_val) &&
- (!self.value || search_val !== self.value[1])) {
- values.push({label: _.sprintf(_t('<em>Â Â Â Create "<strong>%s</strong>"</em>'),
- $('<span />').text(search_val).html()), action: function() {
- self._quick_create(search_val);
- }});
- }
- // create...
- values.push({label: _t("<em>Â Â Â Create and Edit...</em>"), action: function() {
- self._change_int_value(null);
- self._search_create_popup("form", undefined, {"default_name": search_val});
- }});
-
- response(values);
- });
- },
- _quick_create: function(name) {
- var self = this;
- var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
- dataset.name_create(name, function(data) {
- self._change_int_ext_value(data);
- }).fail(function(error, event) {
- event.preventDefault();
- self._change_int_value(null);
- self._search_create_popup("form", undefined, {"default_name": name});
- });
- },
- // all search/create popup handling
- _search_create_popup: function(view, ids, context) {
- var self = this;
- var pop = new openerp.web.form.SelectCreatePopup(this);
- pop.select_element(self.field.relation,{
- initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
- initial_view: view,
- disable_multiple_selection: true
- }, self.build_domain(),
- new openerp.web.CompoundContext(self.build_context(), context || {}));
- pop.on_select_elements.add(function(element_ids) {
- var dataset = new openerp.web.DataSetStatic(self, self.field.relation, self.build_context());
- dataset.name_get([element_ids[0]], function(data) {
- self._change_int_ext_value(data[0]);
- });
- });
- },
- _change_int_ext_value: function(value) {
- this._change_int_value(value);
- this.$input.val(this.value ? this.value[1] : "");
- },
- _change_int_value: function(value) {
- this.value = value;
- var back_orig_value = this.original_value;
- if (this.value === null || this.value) {
- this.original_value = this.value;
- }
- if (back_orig_value === undefined) { // first use after a set_value()
- return;
- }
- if (this.value !== undefined && ((back_orig_value ? back_orig_value[0] : null)
- !== (this.value ? this.value[0] : null))) {
- this.on_ui_change();
- }
- },
- set_value: function(value) {
- value = value || null;
- this.invalid = false;
- var self = this;
- this.tmp_value = value;
- self.update_dom();
- self.on_value_changed();
- var real_set_value = function(rval) {
- self.tmp_value = undefined;
- self.value = rval;
- self.original_value = undefined;
- self._change_int_ext_value(rval);
- };
- if(typeof(value) === "number") {
- var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
- dataset.name_get([value], function(data) {
- real_set_value(data[0]);
- }).fail(function() {self.tmp_value = undefined;});
- } else {
- setTimeout(function() {real_set_value(value);}, 0);
- }
- },
- get_value: function() {
- if (this.tmp_value !== undefined) {
- if (this.tmp_value instanceof Array) {
- return this.tmp_value[0];
- }
- return this.tmp_value ? this.tmp_value : false;
- }
- if (this.value === undefined)
- return this.original_value ? this.original_value[0] : false;
- return this.value ? this.value[0] : false;
- },
- validate: function() {
- this.invalid = false;
- var val = this.tmp_value !== undefined ? this.tmp_value : this.value;
- if (val === null) {
- this.invalid = this.required;
- }
- },
- open_related: function(related) {
- var self = this;
- if (!self.value)
- return;
- var additional_context = {
- active_id: self.value[0],
- active_ids: [self.value[0]],
- active_model: self.field.relation
- };
- self.rpc("/web/action/load", {
- action_id: related[2].id,
- context: additional_context
- }, function(result) {
- result.result.context = _.extend(result.result.context || {}, additional_context);
- self.do_action(result.result);
- });
- }
-});
-
-/*
-# Values: (0, 0, { fields }) create
-# (1, ID, { fields }) update
-# (2, ID) remove (delete)
-# (3, ID) unlink one (target id or target of relation)
-# (4, ID) link
-# (5) unlink all (only valid for one2many)
-*/
-var commands = {
- // (0, _, {values})
- CREATE: 0,
- 'create': function (values) {
- return [commands.CREATE, false, values];
- },
- // (1, id, {values})
- UPDATE: 1,
- 'update': function (id, values) {
- return [commands.UPDATE, id, values];
- },
- // (2, id[, _])
- DELETE: 2,
- 'delete': function (id) {
- return [commands.DELETE, id, false];
- },
- // (3, id[, _]) removes relation, but not linked record itself
- FORGET: 3,
- 'forget': function (id) {
- return [commands.FORGET, id, false];
- },
- // (4, id[, _])
- LINK_TO: 4,
- 'link_to': function (id) {
- return [commands.LINK_TO, id, false];
- },
- // (5[, _[, _]])
- DELETE_ALL: 5,
- 'delete_all': function () {
- return [5, false, false];
- },
- // (6, _, ids) replaces all linked records with provided ids
- REPLACE_WITH: 6,
- 'replace_with': function (ids) {
- return [6, false, ids];
- }
-};
-openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
- multi_selection: false,
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldOne2Many";
- this.is_started = $.Deferred();
- this.form_last_update = $.Deferred();
- this.disable_utility_classes = true;
- },
- start: function() {
- this._super.apply(this, arguments);
-
- var self = this;
-
- this.dataset = new openerp.web.form.One2ManyDataSet(this, this.field.relation);
- this.dataset.o2m = this;
- this.dataset.parent_view = this.view;
- this.dataset.on_change.add_last(function() {
- self.on_ui_change();
- });
-
- var modes = this.node.attrs.mode;
- modes = !!modes ? modes.split(",") : ["tree", "form"];
- var views = [];
- _.each(modes, function(mode) {
- var view = {
- view_id: false,
- view_type: mode == "tree" ? "list" : mode,
- options: { sidebar : false }
- };
- if (self.field.views && self.field.views[mode]) {
- view.embedded_view = self.field.views[mode];
- }
- if(view.view_type === "list") {
- view.options.selectable = self.multi_selection;
- }
- views.push(view);
- });
- this.views = views;
-
- this.viewmanager = new openerp.web.ViewManager(this, this.dataset, views);
- this.viewmanager.registry = openerp.web.views.clone({
- list: 'openerp.web.form.One2ManyListView',
- form: 'openerp.web.form.One2ManyFormView'
- });
- var once = $.Deferred().then(function() {
- self.form_last_update.resolve();
- });
- this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
- if (view_type == "list") {
- controller.o2m = self;
- } else if (view_type == "form") {
- controller.on_record_loaded.add_last(function() {
- once.resolve();
- });
- controller.on_pager_action.add_first(function() {
- self.save_form_view();
- });
- controller.$element.find(".oe_form_button_save_edit").hide();
- }
- self.is_started.resolve();
- });
- this.viewmanager.on_mode_switch.add_first(function() {
- self.save_form_view();
- });
- setTimeout(function () {
- self.viewmanager.appendTo(self.$element);
- }, 0);
- },
- reload_current_view: function() {
- var self = this;
- var view = self.viewmanager.views[self.viewmanager.active_view].controller;
- if(self.viewmanager.active_view === "list") {
- view.reload_content();
- } else if (self.viewmanager.active_view === "form") {
- if (this.dataset.index === null && this.dataset.ids.length >= 1) {
- this.dataset.index = 0;
- }
- this.form_last_update.then(function() {
- this.form_last_update = view.do_show();
- });
- }
- },
- set_value: function(value) {
- value = value || [];
- var self = this;
- this.dataset.reset_ids([]);
- if(value.length >= 1 && value[0] instanceof Array) {
- var ids = [];
- _.each(value, function(command) {
- var obj = {values: command[2]};
- switch (command[0]) {
- case commands.CREATE:
- obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
- self.dataset.to_create.push(obj);
- self.dataset.cache.push(_.clone(obj));
- ids.push(obj.id);
- return;
- case commands.UPDATE:
- obj['id'] = command[1];
- self.dataset.to_write.push(obj);
- self.dataset.cache.push(_.clone(obj));
- ids.push(obj.id);
- return;
- case commands.DELETE:
- self.dataset.to_delete.push({id: command[1]});
- return;
- case commands.LINK_TO:
- ids.push(command[1]);
- return;
- case commands.DELETE_ALL:
- self.dataset.delete_all = true;
- return;
- }
- });
- this._super(ids);
- this.dataset.set_ids(ids);
- } else if (value.length >= 1 && typeof(value[0]) === "object") {
- var ids = [];
- this.dataset.delete_all = true;
- _.each(value, function(command) {
- var obj = {values: command};
- obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
- self.dataset.to_create.push(obj);
- self.dataset.cache.push(_.clone(obj));
- ids.push(obj.id);
- });
- this._super(ids);
- this.dataset.set_ids(ids);
- } else {
- this._super(value);
- this.dataset.reset_ids(value);
- }
- if (this.dataset.index === null && this.dataset.ids.length > 0) {
- this.dataset.index = 0;
- }
- $.when(this.is_started).then(function() {
- self.reload_current_view();
- });
- },
- get_value: function() {
- var self = this;
- if (!this.dataset)
- return [];
- var val = this.dataset.delete_all ? [commands.delete_all()] : [];
- val = val.concat(_.map(this.dataset.ids, function(id) {
- var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
- if (alter_order) {
- return commands.create(alter_order.values);
- }
- alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
- if (alter_order) {
- return commands.update(alter_order.id, alter_order.values);
- }
- return commands.link_to(id);
- }));
- return val.concat(_.map(
- this.dataset.to_delete, function(x) {
- return commands['delete'](x.id);}));
- },
- save_form_view: function() {
- if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
- this.viewmanager.views[this.viewmanager.active_view] &&
- this.viewmanager.views[this.viewmanager.active_view].controller) {
- var view = this.viewmanager.views[this.viewmanager.active_view].controller;
- if (this.viewmanager.active_view === "form") {
- var res = $.when(view.do_save());
- if (res === false) {
- // ignore
- } else if (res.isRejected()) {
- throw "Save or create on one2many dataset is not supposed to fail.";
- } else if (!res.isResolved()) {
- throw "Asynchronous get_value() is not supported in form view.";
- }
- return res;
- }
- }
- return false;
- },
- is_valid: function() {
- this.validate();
- return this._super();
- },
- validate: function() {
- this.invalid = false;
- var self = this;
- var view = self.viewmanager.views[self.viewmanager.active_view].controller;
- if (self.viewmanager.active_view === "form") {
- for (var f in view.fields) {
- f = view.fields[f];
- if (!f.is_valid()) {
- this.invalid = true;
- return;
- }
- }
- }
- },
- is_dirty: function() {
- this.save_form_view();
- return this._super();
- },
- update_dom: function() {
- this._super.apply(this, arguments);
- this.$element.toggleClass('disabled', this.readonly);
- }
-});
-
-openerp.web.form.One2ManyDataSet = openerp.web.BufferedDataSet.extend({
- get_context: function() {
- this.context = this.o2m.build_context();
- return this.context;
- }
-});
-
-openerp.web.form.One2ManyFormView = openerp.web.FormView.extend({
-});
-
-openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
- do_add_record: function () {
- if (this.options.editable) {
- this._super.apply(this, arguments);
- } else {
- var self = this;
- var pop = new openerp.web.form.SelectCreatePopup(this);
- pop.select_element(self.o2m.field.relation,{
- initial_view: "form",
- alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
- create_function: function(data) {
- return self.o2m.dataset.create(data, function(r) {
- self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result]));
- self.o2m.dataset.on_change();
- });
- },
- parent_view: self.o2m.view
- }, self.o2m.build_domain(), self.o2m.build_context());
- pop.on_select_elements.add_last(function() {
- self.o2m.reload_current_view();
- });
- }
- },
- do_activate_record: function(index, id) {
- var self = this;
- var pop = new openerp.web.form.FormOpenPopup(self.o2m.view);
- pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{
- auto_write: false,
- alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
- parent_view: self.o2m.view,
- read_function: function() {
- return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
- }
- });
- pop.on_write.add(function(id, data) {
- self.o2m.dataset.write(id, data, {}, function(r) {
- self.o2m.reload_current_view();
- });
- });
- }
-});
-
-openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({
- multi_selection: false,
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldMany2Many";
- this.list_id = _.uniqueId("many2many");
- this.is_started = $.Deferred();
- },
- start: function() {
- this._super.apply(this, arguments);
-
- var self = this;
-
- this.dataset = new openerp.web.form.Many2ManyDataSet(this, this.field.relation);
- this.dataset.m2m = this;
- this.dataset.on_unlink.add_last(function(ids) {
- self.on_ui_change();
- });
-
- this.list_view = new openerp.web.form.Many2ManyListView(this, this.list_id, this.dataset, false, {
- 'addable': 'Add',
- 'selectable': self.multi_selection
- });
- this.list_view.m2m_field = this;
- this.list_view.on_loaded.add_last(function() {
- self.is_started.resolve();
- });
- setTimeout(function () {
- self.list_view.start();
- }, 0);
- },
- set_value: function(value) {
- value = value || [];
- if (value.length >= 1 && value[0] instanceof Array) {
- value = value[0][2];
- }
- this._super(value);
- this.dataset.set_ids(value);
- var self = this;
- $.when(this.is_started).then(function() {
- self.list_view.reload_content();
- });
- },
- get_value: function() {
- return [commands.replace_with(this.dataset.ids)];
- },
- validate: function() {
- this.invalid = false;
- // TODO niv
- }
-});
-
-openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({
- get_context: function() {
- this.context = this.m2m.build_context();
- return this.context;
- }
-});
-
-/**
- * @class
- * @extends openerp.web.ListView
- */
-openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends openerp.web.form.Many2ManyListView# */{
- do_add_record: function () {
- var pop = new openerp.web.form.SelectCreatePopup(this);
- pop.select_element(this.model, {},
- new openerp.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
- this.m2m_field.build_context());
- var self = this;
- pop.on_select_elements.add(function(element_ids) {
- _.each(element_ids, function(element_id) {
- if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) {
- self.dataset.set_ids([].concat(self.dataset.ids, [element_id]));
- self.m2m_field.on_ui_change();
- self.reload_content();
- }
- });
- });
- },
- do_activate_record: function(index, id) {
- var self = this;
- var pop = new openerp.web.form.FormOpenPopup(this);
- pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {});
- pop.on_write_completed.add_last(function() {
- self.reload_content();
- });
- }
-});
-
-/**
- * @class
- * @extends openerp.web.OldWidget
- */
-openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.SelectCreatePopup# */{
- identifier_prefix: "selectcreatepopup",
- template: "SelectCreatePopup",
- /**
- * options:
- * - initial_ids
- * - initial_view: form or search (default search)
- * - disable_multiple_selection
- * - alternative_form_view
- * - create_function (defaults to a naive saving behavior)
- * - parent_view
- */
- select_element: function(model, options, domain, context) {
- var self = this;
- this.model = model;
- this.domain = domain || [];
- this.context = context || {};
- this.options = _.defaults(options || {}, {"initial_view": "search", "create_function": function() {
- return self.create_row.apply(self, arguments);
- }});
- this.initial_ids = this.options.initial_ids;
- this.created_elements = [];
- openerp.web.form.dialog(this.render(), {close:function() {
- self.check_exit();
- }});
- this.start();
- },
- start: function() {
- this._super();
- this.dataset = new openerp.web.ReadOnlyDataSetSearch(this, this.model,
- this.context);
- this.dataset.parent_view = this.options.parent_view;
- if (this.options.initial_view == "search") {
- this.setup_search_view();
- } else { // "form"
- this.new_object();
- }
- },
- setup_search_view: function() {
- var self = this;
- if (this.searchview) {
- this.searchview.stop();
- }
- this.searchview = new openerp.web.SearchView(this,
- this.element_id + "_search", this.dataset, false, {
- "selectable": !this.options.disable_multiple_selection,
- "deletable": false
- });
- this.searchview.on_search.add(function(domains, contexts, groupbys) {
- if (self.initial_ids) {
- self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]], self.domain]),
- contexts, groupbys);
- self.initial_ids = undefined;
- } else {
- self.view_list.do_search.call(self, domains.concat([self.domain]), contexts, groupbys);
- }
- });
- this.searchview.on_loaded.add_last(function () {
- var $buttons = self.searchview.$element.find(".oe_search-view-buttons");
- $buttons.append(QWeb.render("SelectCreatePopup.search.buttons"));
- var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close");
- $cbutton.click(function() {
- self.stop();
- });
- var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select");
- if(self.options.disable_multiple_selection) {
- $sbutton.hide();
- }
- $sbutton.click(function() {
- self.on_select_elements(self.selected_ids);
- self.stop();
- });
- self.view_list = new openerp.web.form.SelectCreateListView(self,
- self.element_id + "_view_list", self.dataset, false,
- {'deletable': false});
- self.view_list.popup = self;
- self.view_list.do_show();
- self.view_list.start().then(function() {
- self.searchview.do_search();
- });
- });
- this.searchview.start();
- },
- create_row: function(data) {
- var self = this;
- var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
- wdataset.parent_view = this.options.parent_view;
- return wdataset.create(data);
- },
- on_select_elements: function(element_ids) {
- },
- on_click_element: function(ids) {
- this.selected_ids = ids || [];
- if(this.selected_ids.length > 0) {
- this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
- } else {
- this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
- }
- },
- new_object: function() {
- var self = this;
- if (this.searchview) {
- this.searchview.hide();
- }
- if (this.view_list) {
- this.view_list.$element.hide();
- }
- this.dataset.index = null;
- this.view_form = new openerp.web.FormView(this, this.element_id + "_view_form", this.dataset, false);
- if (this.options.alternative_form_view) {
- this.view_form.set_embedded_view(this.options.alternative_form_view);
- }
- this.view_form.start();
- this.view_form.on_loaded.add_last(function() {
- var $buttons = self.view_form.$element.find(".oe_form_buttons");
- $buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self}));
- var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save-new");
- $nbutton.click(function() {
- self._created = $.Deferred().then(function() {
- self._created = undefined;
- self.view_form.on_button_new();
- });
- self.view_form.do_save();
- });
- var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save");
- $nbutton.click(function() {
- self._created = $.Deferred().then(function() {
- self._created = undefined;
- self.check_exit();
- });
- self.view_form.do_save();
- });
- var $cbutton = $buttons.find(".oe_selectcreatepopup-form-close");
- $cbutton.click(function() {
- self.check_exit();
- });
- });
- this.dataset.on_create.add(function(data) {
- self.options.create_function(data).then(function(r) {
- self.created_elements.push(r.result);
- if (self._created) {
- self._created.resolve();
- }
- });
- });
- this.view_form.do_show();
- },
- check_exit: function() {
- if (this.created_elements.length > 0) {
- this.on_select_elements(this.created_elements);
- }
- this.stop();
- }
-});
-
-openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({
- do_add_record: function () {
- this.popup.new_object();
- },
- select_record: function(index) {
- this.popup.on_select_elements([this.dataset.ids[index]]);
- this.popup.stop();
- },
- do_select: function(ids, records) {
- this._super(ids, records);
- this.popup.on_click_element(ids);
- }
-});
-
-/**
- * @class
- * @extends openerp.web.OldWidget
- */
-openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.FormOpenPopup# */{
- identifier_prefix: "formopenpopup",
- template: "FormOpenPopup",
- /**
- * options:
- * - alternative_form_view
- * - auto_write (default true)
- * - read_function
- * - parent_view
- */
- show_element: function(model, row_id, context, options) {
- this.model = model;
- this.row_id = row_id;
- this.context = context || {};
- this.options = _.defaults(options || {}, {"auto_write": true});
- jQuery(this.render()).dialog({title: '',
- modal: true,
- width: 960,
- height: 600});
- this.start();
- },
- start: function() {
- this._super();
- this.dataset = new openerp.web.form.FormOpenDataset(this, this.model, this.context);
- this.dataset.fop = this;
- this.dataset.ids = [this.row_id];
- this.dataset.index = 0;
- this.dataset.parent_view = this.options.parent_view;
- this.setup_form_view();
- },
- on_write: function(id, data) {
- this.stop();
- if (!this.options.auto_write)
- return;
- var self = this;
- var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
- wdataset.parent_view = this.options.parent_view;
- wdataset.write(id, data, {}, function(r) {
- self.on_write_completed();
- });
- },
- on_write_completed: function() {},
- setup_form_view: function() {
- var self = this;
- this.view_form = new openerp.web.FormView(this, this.element_id + "_view_form", this.dataset, false);
- if (this.options.alternative_form_view) {
- this.view_form.set_embedded_view(this.options.alternative_form_view);
- }
- this.view_form.start();
- this.view_form.on_loaded.add_last(function() {
- var $buttons = self.view_form.$element.find(".oe_form_buttons");
- $buttons.html(QWeb.render("FormOpenPopup.form.buttons"));
- var $nbutton = $buttons.find(".oe_formopenpopup-form-save");
- $nbutton.click(function() {
- self.view_form.do_save();
- });
- var $cbutton = $buttons.find(".oe_formopenpopup-form-close");
- $cbutton.click(function() {
- self.stop();
- });
- self.view_form.do_show();
- });
- this.dataset.on_write.add(this.on_write);
- }
-});
-
-openerp.web.form.FormOpenDataset = openerp.web.ReadOnlyDataSetSearch.extend({
- read_ids: function() {
- if (this.fop.options.read_function) {
- return this.fop.options.read_function.apply(null, arguments);
- } else {
- return this._super.apply(this, arguments);
- }
- }
-});
-
-openerp.web.form.FieldReference = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldReference";
- this.fields_view = {
- fields: {
- selection: {
- selection: view.fields_view.fields[this.name].selection
- },
- m2o: {
- relation: null
- }
- }
- };
- this.get_fields_values = view.get_fields_values;
- this.do_onchange = this.on_form_changed = this.on_nop;
- this.widgets = {};
- this.fields = {};
- this.selection = new openerp.web.form.FieldSelection(this, { attrs: {
- name: 'selection',
- widget: 'selection'
- }});
- this.selection.on_value_changed.add_last(this.on_selection_changed);
- this.m2o = new openerp.web.form.FieldMany2One(this, { attrs: {
- name: 'm2o',
- widget: 'many2one'
- }});
- },
- on_nop: function() {
- },
- on_selection_changed: function() {
- this.m2o.field.relation = this.selection.get_value();
- this.m2o.set_value(null);
- },
- start: function() {
- this._super();
- this.selection.start();
- this.m2o.start();
- },
- is_valid: function() {
- return this.required === false || typeof(this.get_value()) === 'string';
- },
- is_dirty: function() {
- return this.selection.is_dirty() || this.m2o.is_dirty();
- },
- set_value: function(value) {
- this._super(value);
- if (typeof(value) === 'string') {
- var vals = value.split(',');
- this.selection.set_value(vals[0]);
- this.m2o.set_value(parseInt(vals[1], 10));
- }
- },
- get_value: function() {
- var model = this.selection.get_value(),
- id = this.m2o.get_value();
- if (typeof(model) === 'string' && typeof(id) === 'number') {
- return model + ',' + id;
- } else {
- return false;
- }
- }
-});
-
-openerp.web.form.FieldBinary = openerp.web.form.Field.extend({
- init: function(view, node) {
- this._super(view, node);
- this.iframe = this.element_id + '_iframe';
- this.binary_value = false;
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$element.find('input.oe-binary-file').change(this.on_file_change);
- this.$element.find('button.oe-binary-file-save').click(this.on_save_as);
- this.$element.find('.oe-binary-file-clear').click(this.on_clear);
- },
- update_dom: function() {
- this._super.apply(this, arguments);
- this.$element.find('.oe-binary').toggle(!this.readonly);
- },
- human_filesize : function(size) {
- var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
- var i = 0;
- while (size >= 1024) {
- size /= 1024;
- ++i;
- }
- return size.toFixed(2) + ' ' + units[i];
- },
- on_file_change: function(e) {
- // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper
- // http://www.html5rocks.com/tutorials/file/dndfiles/
- // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler
- window[this.iframe] = this.on_file_uploaded;
- if ($(e.target).val() != '') {
- this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id);
- this.$element.find('form.oe-binary-form').submit();
- this.toggle_progress();
- }
- },
- toggle_progress: function() {
- this.$element.find('.oe-binary-progress, .oe-binary').toggle();
- },
- on_file_uploaded: function(size, name, content_type, file_base64) {
- delete(window[this.iframe]);
- if (size === false) {
- this.notification.warn("File Upload", "There was a problem while uploading your file");
- // TODO: use openerp web crashmanager
- console.warn("Error while uploading file : ", name);
- } else {
- this.on_file_uploaded_and_valid.apply(this, arguments);
- this.on_ui_change();
- }
- this.toggle_progress();
- },
- on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
- },
- on_save_as: function() {
- if (!this.view.datarecord.id) {
- this.notification.warn("Can't save file", "The record has not yet been saved");
- } else {
- var url = '/web/binary/saveas?session_id=' + this.session.session_id + '&model=' +
- this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name +
- '&fieldname=' + (this.node.attrs.filename || '') + '&t=' + (new Date().getTime());
- window.open(url);
- }
- },
- on_clear: function() {
- if (this.value !== false) {
- this.value = false;
- this.binary_value = false;
- this.on_ui_change();
- }
- return false;
- }
-});
-
-openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldBinaryFile";
- },
- set_value: function(value) {
- this._super.apply(this, arguments);
- var show_value = (value != null && value !== false) ? value : '';
- this.$element.find('input').eq(0).val(show_value);
- },
- on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
- this.value = file_base64;
- this.binary_value = true;
- var show_value = this.human_filesize(size);
- this.$element.find('input').eq(0).val(show_value);
- this.set_filename(name);
- },
- set_filename: function(value) {
- var filename = this.node.attrs.filename;
- if (this.view.fields[filename]) {
- this.view.fields[filename].set_value(value);
- this.view.fields[filename].on_ui_change();
- }
- },
- on_clear: function() {
- this._super.apply(this, arguments);
- this.$element.find('input').eq(0).val('');
- this.set_filename('');
- }
-});
-
-openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
- init: function(view, node) {
- this._super(view, node);
- this.template = "FieldBinaryImage";
- },
- start: function() {
- this._super.apply(this, arguments);
- this.$image = this.$element.find('img.oe-binary-image');
- },
- set_value: function(value) {
- this._super.apply(this, arguments);
- this.set_image_maxwidth();
- var url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' +
- this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime());
- this.$image.attr('src', url);
- },
- set_image_maxwidth: function() {
- this.$image.css('max-width', this.$element.width());
- },
- on_file_change: function() {
- this.set_image_maxwidth();
- this._super.apply(this, arguments);
- },
- on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
- this.value = file_base64;
- this.binary_value = true;
- this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64);
- },
- on_clear: function() {
- this._super.apply(this, arguments);
- this.$image.attr('src', '/web/static/src/img/placeholder.png');
- }
-});
-
-openerp.web.form.FieldStatus = openerp.web.form.Field.extend({
- template: "FieldStatus",
- start: function() {
- this._super();
- this.selected_value = null;
-
- this.render_list();
- },
- set_value: function(value) {
- this._super(value);
- this.selected_value = value;
-
- this.render_list();
- },
- render_list: function() {
- var self = this;
- var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","),
- function(x) { return x.trim(); });
- shown = _.select(shown, function(x) { return x.length > 0; });
-
- if (shown.length == 0) {
- this.to_show = this.field.selection;
- } else {
- this.to_show = _.select(this.field.selection, function(x) {
- return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value;
- });
- }
-
- var content = openerp.web.qweb.render("FieldStatus.content", {widget: this, _:_});
- this.$element.html(content);
-
- var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}");
- var color = colors[this.selected_value];
- if (color) {
- var elem = this.$element.find("li.oe-arrow-list-selected span");
- elem.css("border-color", color);
- elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-before");
- elem.css("border-left-color", "rgba(0,0,0,0)");
- elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-after");
- elem.css("border-color", "rgba(0,0,0,0)");
- elem.css("border-left-color", color);
- }
- }
-});
-
-/**
- * Registry of form widgets, called by :js:`openerp.web.FormView`
- */
-openerp.web.form.widgets = new openerp.web.Registry({
- 'frame' : 'openerp.web.form.WidgetFrame',
- 'group' : 'openerp.web.form.WidgetFrame',
- 'notebook' : 'openerp.web.form.WidgetNotebook',
- 'separator' : 'openerp.web.form.WidgetSeparator',
- 'label' : 'openerp.web.form.WidgetLabel',
- 'button' : 'openerp.web.form.WidgetButton',
- 'char' : 'openerp.web.form.FieldChar',
- 'email' : 'openerp.web.form.FieldEmail',
- 'url' : 'openerp.web.form.FieldUrl',
- 'text' : 'openerp.web.form.FieldText',
- 'text_wiki' : 'openerp.web.form.FieldText',
- 'date' : 'openerp.web.form.FieldDate',
- 'datetime' : 'openerp.web.form.FieldDatetime',
- 'selection' : 'openerp.web.form.FieldSelection',
- 'many2one' : 'openerp.web.form.FieldMany2One',
- 'many2many' : 'openerp.web.form.FieldMany2Many',
- 'one2many' : 'openerp.web.form.FieldOne2Many',
- 'one2many_list' : 'openerp.web.form.FieldOne2Many',
- 'reference' : 'openerp.web.form.FieldReference',
- 'boolean' : 'openerp.web.form.FieldBoolean',
- 'float' : 'openerp.web.form.FieldFloat',
- 'integer': 'openerp.web.form.FieldFloat',
- 'float_time': 'openerp.web.form.FieldFloat',
- 'progressbar': 'openerp.web.form.FieldProgressBar',
- 'image': 'openerp.web.form.FieldBinaryImage',
- 'binary': 'openerp.web.form.FieldBinaryFile',
- 'statusbar': 'openerp.web.form.FieldStatus'
-});
-
-};
-
-// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
+++ /dev/null
-/**
- * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
- * @namespace
- */
-openerp.web.list_editable = function (openerp) {
- var KEY_RETURN = 13,
- KEY_ESCAPE = 27;
-
- // editability status of list rows
- openerp.web.ListView.prototype.defaults.editable = null;
-
- // TODO: not sure second @lends on existing item is correct, to check
- openerp.web.ListView.include(/** @lends openerp.web.ListView# */{
- init: function () {
- var self = this;
- this._super.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.web.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']);
- this._super(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 {
- this._super();
- }
- },
- on_loaded: function (data, grouped) {
- // tree/@editable takes priority on everything else if present.
- this.options.editable = data.arch.attrs.editable || this.options.editable;
- return this._super(data, grouped);
- }
- });
-
- openerp.web.ListView.Groups.include(/** @lends openerp.web.ListView.Groups# */{
- passtrough_events: openerp.web.ListView.Groups.prototype.passtrough_events + " edit saved",
- new_record: function () {
- // TODO: handle multiple children
- this.children[null].new_record();
- }
- });
-
- openerp.web.ListView.List.include(/** @lends openerp.web.ListView.List# */{
- row_clicked: function (event) {
- if (!this.options.editable) {
- return this._super(event);
- }
- this.edit_record($(event.currentTarget).data('id'));
- },
- /**
- * Checks if a record is being edited, and if so cancels it
- */
- cancel_pending_edition: function () {
- var self = this, cancelled = $.Deferred();
- if (!this.edition) {
- cancelled.resolve();
- return cancelled.promise();
- }
-
- if (this.edition_id != null) {
- this.reload_record(self.records.get(this.edition_id)).then(function () {
- cancelled.resolve();
- });
- } else {
- cancelled.resolve();
- }
- cancelled.then(function () {
- self.view.unpad_columns();
- self.edition_form.stop();
- self.edition_form.$element.remove();
- delete self.edition_form;
- delete self.edition_id;
- delete self.edition;
- });
- return cancelled.promise();
- },
- /**
- * Adapts this list's view description to be suitable to the inner form
- * view of a row being edited.
- *
- * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows.
- */
- get_form_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;
- },
- render_row_as_form: function (row) {
- var self = this;
- this.cancel_pending_edition().then(function () {
- var record_id = $(row).data('id');
- var $new_row = $('<tr>', {
- id: _.uniqueId('oe-editable-row-'),
- 'data-id': record_id,
- 'class': $(row).attr('class') + ' oe_forms',
- click: function (e) {e.stopPropagation();}
- })
- .delegate('button.oe-edit-row-save', 'click', function () {
- self.save_row();
- })
- .delegate('button.oe-edit-row-cancel', 'click', function () {
- self.cancel_edition();
- })
- .delegate('button', 'keyup', function (e) {
- e.stopImmediatePropagation();
- })
- .keyup(function (e) {
- switch (e.which) {
- case KEY_RETURN:
- self.save_row(true);
- break;
- case KEY_ESCAPE:
- self.cancel_edition();
- break;
- default:
- return;
- }
- });
- if (row) {
- $new_row.replaceAll(row);
- } else if (self.options.editable === 'top') {
- self.$current.prepend($new_row);
- } else if (self.options.editable) {
- self.$current.append($new_row);
- }
- self.edition = true;
- self.edition_id = record_id;
- self.edition_form = _.extend(new openerp.web.FormView(
- self, $new_row.attr('id'), self.dataset, false), {
- template: 'ListView.row.form',
- registry: openerp.web.list.form.widgets
- });
- $.when(self.edition_form.on_loaded(self.get_form_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>');
- }
- });
- // Add columns for the cancel and save buttons, if
- // there are none in the list
- if (!self.options.selectable) {
- self.view.pad_columns(
- 1, {except: $new_row, position: 'before'});
- }
- if (!self.options.deletable) {
- self.view.pad_columns(
- 1, {except: $new_row});
- }
-
- self.edition_form.do_show();
- });
- });
- },
- handle_onwrite: function (source_record_id) {
- var self = this;
- var on_write_callback = self.view.fields_view.arch.attrs.on_write;
- if (!on_write_callback) { return; }
- this.dataset.call(on_write_callback, [source_record_id], function (ids) {
- _(ids).each(function (id) {
- var record = self.records.get(id);
- if (!record) {
- // insert after the source record
- var index = self.records.indexOf(
- self.records.get(source_record_id)) + 1;
- record = new openerp.web.list.Record({id: id});
- self.records.add(record, {at: index});
- self.dataset.ids.splice(index, 0, id);
- }
- self.reload_record(record);
- });
- });
- },
- /**
- * 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) {
- if (result.created && !self.edition_id) {
- self.records.add({id: result.result},
- {at: self.options.editable === 'top' ? 0 : null});
- self.edition_id = result.result;
- }
- var edited_record = self.records.get(self.edition_id),
- next_record = self.records.at(
- self.records.indexOf(edited_record) + 1);
-
- self.handle_onwrite(self.edition_id);
- self.cancel_pending_edition().then(function () {
- $(self).trigger('saved', [self.dataset]);
- if (!edit_next) {
- return;
- }
- if (result.created) {
- self.new_record();
- return;
- }
- var next_record_id;
- if (next_record) {
- next_record_id = next_record.get('id');
- self.dataset.index = _(self.dataset.ids)
- .indexOf(next_record_id);
- } else {
- self.dataset.index = 0;
- next_record_id = self.records.at(0).get('id');
- }
- self.edit_record(next_record_id);
- });
- }, 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 (record_id) {
- this.render_row_as_form(
- this.$current.find('[data-id=' + record_id + ']'));
- $(this).trigger(
- 'edit',
- [record_id, this.dataset]);
- },
- new_record: function () {
- this.dataset.index = null;
- this.render_row_as_form();
- }
- });
- if (!openerp.web.list) {
- openerp.web.list = {};
- }
- if (!openerp.web.list.form) {
- openerp.web.list.form = {};
- }
- openerp.web.list.form.WidgetFrame = openerp.web.form.WidgetFrame.extend({
- template: 'ListView.row.frame'
- });
- var form_widgets = openerp.web.form.widgets;
- openerp.web.list.form.widgets = form_widgets.clone({
- 'frame': 'openerp.web.list.form.WidgetFrame'
- });
- // All form widgets inherit a problematic behavior from
- // openerp.web.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.web.list.form.widgets;
- _(list_form_widgets.map).each(function (widget_path, key) {
- if (key === 'frame') { return; }
- var new_path = 'openerp.web.list.form.' + key;
-
- openerp.web.list.form[key] = (form_widgets.get_object(key)).extend({
- update_dom: function () {
- this.$element.children().css('visibility', '');
- if (this.modifiers.tree_invisible) {
- var old_invisible = this.invisible;
- this.invisible = !!this.modifiers.tree_invisible;
- this._super();
- this.invisible = old_invisible;
- } else if (this.invisible) {
- this.$element.children().css('visibility', 'hidden');
- }
- }
- });
- list_form_widgets.add(key, new_path);
- });
-};
+++ /dev/null
-openerp.web.list = function (openerp) {
-var QWeb = openerp.web.qweb;
-openerp.web.views.add('list', 'openerp.web.ListView');
-openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# */ {
- defaults: {
- // records can be selected one by one
- 'selectable': true,
- // list rows can be deleted
- 'deletable': true,
- // whether the column headers should be displayed
- 'header': true,
- // display addition button, with that label
- 'addable': "New",
- // whether the list view can be sorted, note that once a view has been
- // sorted it can not be reordered anymore
- 'sortable': true,
- // whether the view rows can be reordered (via vertical drag & drop)
- 'reorderable': true
- },
- /**
- * Core class for list-type displays.
- *
- * As a view, needs a number of view-related parameters to be correctly
- * instantiated, provides options and overridable methods for behavioral
- * customization.
- *
- * See constructor parameters and method documentations for information on
- * the default behaviors and possible options for the list view.
- *
- * @constructs openerp.web.ListView
- * @extends openerp.web.View
- *
- * @param parent parent object
- * @param element_id the id of the DOM elements this view should link itself to
- * @param {openerp.web.DataSet} dataset the dataset the view should work with
- * @param {String} view_id the listview's identifier, if any
- * @param {Object} options A set of options used to configure the view
- * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox)
- * @param {Boolean} [options.header=true] should the list's header be displayed
- * @param {Boolean} [options.deletable=true] are the list rows deletable
- * @param {void|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button.
- * @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers
- * @param {Boolean} [options.reorderable=true] is it possible to reorder list rows
- */
- init: function(parent, element_id, dataset, view_id, options) {
- var self = this;
- this._super(parent, element_id);
- this.set_default_options(_.extend({}, this.defaults, options || {}));
- this.dataset = dataset;
- this.model = dataset.model;
- this.view_id = view_id;
- this.previous_colspan = null;
-
- this.columns = [];
-
- this.records = new Collection();
-
- this.set_groups(new openerp.web.ListView.Groups(this));
-
- if (this.dataset instanceof openerp.web.DataSetStatic) {
- this.groups.datagroup = new openerp.web.StaticDataGroup(this.dataset);
- } else {
- this.groups.datagroup = new openerp.web.DataGroup(
- this, this.model,
- dataset.get_domain(),
- dataset.get_context(),
- {});
- this.groups.datagroup.sort = this.dataset._sort;
- }
-
- this.page = 0;
- this.records.bind('change', function (event, record, key) {
- if (!_(self.aggregate_columns).chain()
- .pluck('name').contains(key).value()) {
- return;
- }
- self.compute_aggregates();
- });
- },
- /**
- * Retrieves the view's number of records per page (|| section)
- *
- * options > defaults > parent.action.limit > indefinite
- *
- * @returns {Number|null}
- */
- limit: function () {
- if (this._limit === undefined) {
- this._limit = (this.options.limit
- || this.defaults.limit
- || (this.widget_parent.action || {}).limit
- || null);
- }
- return this._limit;
- },
- /**
- * Set a custom Group construct as the root of the List View.
- *
- * @param {openerp.web.ListView.Groups} groups
- */
- set_groups: function (groups) {
- var self = this;
- if (this.groups) {
- $(this.groups).unbind("selected deleted action row_link");
- delete this.groups;
- }
-
- this.groups = groups;
- $(this.groups).bind({
- 'selected': function (e, ids, records) {
- self.do_select(ids, records);
- },
- 'deleted': function (e, ids) {
- self.do_delete(ids);
- },
- 'action': function (e, action_name, id, callback) {
- self.do_button_action(action_name, id, callback);
- },
- 'row_link': function (e, id, dataset) {
- self.do_activate_record(dataset.index, id, dataset);
- }
- });
- },
- /**
- * View startup method, the default behavior is to set the ``oe-listview``
- * class on its root element and to perform an RPC load call.
- *
- * @returns {$.Deferred} loading promise
- */
- start: function() {
- this.$element.addClass('oe-listview');
- return this.reload_view(null, null, true);
- },
- /**
- * Called after loading the list view's description, sets up such things
- * as the view table's columns, renders the table itself and hooks up the
- * various table-level and row-level DOM events (action buttons, deletion
- * buttons, selection of records, [New] button, selection of a given
- * record, ...)
- *
- * Sets up the following:
- *
- * * Processes arch and fields to generate a complete field descriptor for each field
- * * Create the table itself and allocate visible columns
- * * Hook in the top-level (header) [New|Add] and [Delete] button
- * * Sets up showing/hiding the top-level [Delete] button based on records being selected or not
- * * Sets up event handlers for action buttons and per-row deletion button
- * * Hooks global callback for clicking on a row
- * * Sets up its sidebar, if any
- *
- * @param {Object} data wrapped fields_view_get result
- * @param {Object} data.fields_view fields_view_get result (processed)
- * @param {Object} data.fields_view.fields mapping of fields for the current model
- * @param {Object} data.fields_view.arch current list view descriptor
- * @param {Boolean} grouped Is the list view grouped
- */
- on_loaded: function(data, grouped) {
- var self = this;
- this.fields_view = data;
- this.name = "" + this.fields_view.arch.attrs.string;
-
- this.setup_columns(this.fields_view.fields, grouped);
-
- this.$element.html(QWeb.render("ListView", this));
-
- // Head hook
- this.$element.find('.oe-list-add')
- .click(this.do_add_record)
- .attr('disabled', grouped && this.options.editable);
- this.$element.find('.oe-list-delete')
- .attr('disabled', true)
- .click(this.do_delete_selected);
- this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) {
- e.stopPropagation();
-
- var $this = $(this);
- self.dataset.sort($this.data('id'));
- if ($this.find('span').length) {
- $this.find('span').toggleClass(
- 'ui-icon-triangle-1-s ui-icon-triangle-1-n');
- } else {
- $this.append('<span class="ui-icon ui-icon-triangle-1-s">')
- .siblings('.oe-sortable').find('span').remove();
- }
-
- self.reload_content();
- });
-
- this.$element.find('.oe-list-pager')
- .delegate('button', 'click', function () {
- var $this = $(this);
- switch ($this.data('pager-action')) {
- case 'first':
- self.page = 0; break;
- case 'last':
- self.page = Math.floor(
- self.dataset.ids.length / self.limit());
- break;
- case 'next':
- self.page += 1; break;
- case 'previous':
- self.page -= 1; break;
- }
- self.reload_content();
- }).find('.oe-pager-state')
- .click(function (e) {
- e.stopPropagation();
- var $this = $(this);
-
- var $select = $('<select>')
- .appendTo($this.empty())
- .click(function (e) {e.stopPropagation();})
- .append('<option value="80">80</option>' +
- '<option value="100">100</option>' +
- '<option value="200">200</option>' +
- '<option value="500">500</option>' +
- '<option value="NaN">Unlimited</option>')
- .change(function () {
- var val = parseInt($select.val(), 10);
- self._limit = (isNaN(val) ? null : val);
- self.page = 0;
- self.reload_content();
- })
- .val(self._limit || 'NaN');
- });
- if (!this.sidebar && this.options.sidebar && this.options.sidebar_id) {
- this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
- this.sidebar.start();
- this.sidebar.add_toolbar(this.fields_view.toolbar);
- this.set_common_sidebar_sections(this.sidebar);
- }
- },
- /**
- * Configures the ListView pager based on the provided dataset's information
- *
- * Horrifying side-effect: sets the dataset's data on this.dataset?
- *
- * @param {openerp.web.DataSet} dataset
- */
- configure_pager: function (dataset) {
- this.dataset.ids = dataset.ids;
-
- var limit = this.limit(),
- total = dataset.ids.length,
- first = (this.page * limit),
- last;
- if (!limit || (total - first) < limit) {
- last = total;
- } else {
- last = first + limit;
- }
- this.$element.find('span.oe-pager-state').empty().text(_.sprintf(
- "[%d to %d] of %d", first + 1, last, total));
-
- this.$element
- .find('button[data-pager-action=first], button[data-pager-action=previous]')
- .attr('disabled', this.page === 0)
- .end()
- .find('button[data-pager-action=last], button[data-pager-action=next]')
- .attr('disabled', last === total);
- },
- /**
- * Sets up the listview's columns: merges view and fields data, move
- * grouped-by columns to the front of the columns list and make them all
- * visible.
- *
- * @param {Object} fields fields_view_get's fields section
- * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
- */
- setup_columns: function (fields, grouped) {
- var domain_computer = openerp.web.form.compute_domain;
-
- var noop = function () { return {}; };
- var field_to_column = function (field) {
- var name = field.attrs.name;
- var column = _.extend({id: name, tag: field.tag},
- field.attrs, fields[name]);
- // modifiers computer
- if (column.modifiers) {
- var modifiers = JSON.parse(column.modifiers);
- column.modifiers_for = function (fields) {
- if (!modifiers.invisible) {
- return {};
- }
- return {
- 'invisible': domain_computer(modifiers.invisible, fields)
- };
- };
- if (modifiers['tree_invisible']) {
- column.invisible = '1';
- }
- } else {
- column.modifiers_for = noop;
- }
- return column;
- };
-
- this.columns.splice(0, this.columns.length);
- this.columns.push.apply(
- this.columns,
- _(this.fields_view.arch.children).map(field_to_column));
- if (grouped) {
- this.columns.unshift({
- id: '_group', tag: '', string: "Group", meta: true,
- modifiers_for: function () { return {}; }
- }, {
- id: '_count', tag: '', string: '#', meta: true,
- modifiers_for: function () { return {}; }
- });
- }
-
- this.visible_columns = _.filter(this.columns, function (column) {
- return column.invisible !== '1';
- });
-
- this.aggregate_columns = _(this.visible_columns)
- .map(function (column) {
- if (column.type !== 'integer' && column.type !== 'float') {
- return {};
- }
- var aggregation_func = column['group_operator'] || 'sum';
-
- return _.extend({}, column, {
- 'function': aggregation_func,
- label: column[aggregation_func]
- });
- });
- },
- /**
- * Used to handle a click on a table row, if no other handler caught the
- * event.
- *
- * The default implementation asks the list view's view manager to switch
- * to a different view (by calling
- * :js:func:`~openerp.web.ViewManager.on_mode_switch`), using the
- * provided record index (within the current list view's dataset).
- *
- * If the index is null, ``switch_to_record`` asks for the creation of a
- * new record.
- *
- * @param {Number|void} index the record index (in the current dataset) to switch to
- * @param {String} [view="form"] the view type to switch to
- */
- select_record:function (index, view) {
- view = view || 'form';
- this.dataset.index = index;
- _.delay(_.bind(function () {
- this.do_switch_view(view);
- }, this));
- },
- do_show: function () {
- this.$element.show();
- if (this.sidebar) {
- this.sidebar.$element.show();
- }
- if (!_(this.dataset.ids).isEmpty()) {
- this.reload_content();
- }
- },
- do_hide: function () {
- this.$element.hide();
- if (this.sidebar) {
- this.sidebar.$element.hide();
- }
- },
- /**
- * Reloads the list view based on the current settings (dataset & al)
- *
- * @param {Boolean} [grouped] Should the list be displayed grouped
- * @param {Object} [context] context to send the server while loading the view
- */
- reload_view: function (grouped, context, initial) {
- var self = this;
- var callback = function (field_view_get) {
- self.on_loaded(field_view_get, grouped);
- };
- if (this.embedded_view) {
- return $.Deferred().then(callback).resolve(this.embedded_view);
- } else {
- return this.rpc('/web/listview/load', {
- model: this.model,
- view_id: this.view_id,
- view_type: "tree",
- context: this.dataset.get_context(context),
- toolbar: this.options.sidebar
- }, callback);
- }
- },
- /**
- * re-renders the content of the list view
- */
- reload_content: function () {
- var self = this;
- this.records.reset();
- this.$element.find('.oe-listview-content').append(
- this.groups.render(function () {
- if (self.dataset.index == null) {
- var has_one = false;
- self.records.each(function () { has_one = true; });
- if (has_one) {
- self.dataset.index = 0;
- }
- }
- self.compute_aggregates();
- }));
- },
- /**
- * Event handler for a search, asks for the computation/folding of domains
- * and contexts (and group-by), then reloads the view's content.
- *
- * @param {Array} domains a sequence of literal and non-literal domains
- * @param {Array} contexts a sequence of literal and non-literal contexts
- * @param {Array} groupbys a sequence of literal and non-literal group-by contexts
- * @returns {$.Deferred} fold request evaluation promise
- */
- do_search: function (domains, contexts, groupbys) {
- return this.rpc('/web/session/eval_domain_and_context', {
- domains: [this.dataset.get_domain()].concat(domains),
- contexts: [this.dataset.get_context()].concat(contexts),
- group_by_seq: groupbys
- }, $.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.groups.datagroup = new openerp.web.DataGroup(
- this, this.model,
- results.domain,
- results.context,
- results.group_by);
- this.groups.datagroup.sort = this.dataset._sort;
-
- if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) {
- results.group_by = null;
- }
-
- this.reload_view(!!results.group_by, results.context).then(
- $.proxy(this, 'reload_content'));
- },
- /**
- * Handles the signal to delete lines from the records list
- *
- * @param {Array} ids the ids of the records to delete
- */
- do_delete: function (ids) {
- if (!ids.length) {
- return;
- }
- var self = this;
- return $.when(this.dataset.unlink(ids)).then(function () {
- _(ids).each(function (id) {
- self.records.remove(self.records.get(id));
- });
- self.compute_aggregates();
- });
- },
- /**
- * Handles the signal indicating that a new record has been selected
- *
- * @param {Array} ids selected record ids
- * @param {Array} records selected record values
- */
- do_select: function (ids, records) {
- this.$element.find('.oe-list-delete')
- .attr('disabled', !ids.length);
-
- if (!records.length) {
- this.compute_aggregates();
- return;
- }
- this.compute_aggregates(_(records).map(function (record) {
- return {count: 1, values: record};
- }));
- },
- /**
- * Handles action button signals on a record
- *
- * @param {String} name action name
- * @param {Object} id id of the record the action should be called on
- * @param {Function} callback should be called after the action is executed, if non-null
- */
- do_button_action: function (name, id, callback) {
- var action = _.detect(this.columns, function (field) {
- return field.name === name;
- });
- if (!action) { return; }
- this.do_execute_action(action, this.dataset, id, callback);
- },
- /**
- * Handles the activation of a record (clicking on it)
- *
- * @param {Number} index index of the record in the dataset
- * @param {Object} id identifier of the activated record
- * @param {openerp.web.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;
- // TODO is it needed ?
- this.dataset.read_slice([],{
- context: dataset.get_context(),
- domain: dataset.get_domain()
- }, function () {
- self.select_record(index);
- });
- },
- /**
- * Handles signal for the addition of a new record (can be a creation,
- * can be the addition from a remote source, ...)
- *
- * The default implementation is to switch to a new record on the form view
- */
- do_add_record: function () {
- this.select_record(null);
- },
- /**
- * Handles deletion of all selected lines
- */
- do_delete_selected: function () {
- this.do_delete(this.groups.get_selection().ids);
- },
- /**
- * Computes the aggregates for the current list view, either on the
- * records provided or on the records of the internal
- * :js:class:`~openerp.web.ListView.Group`, by calling
- * :js:func:`~openerp.web.ListView.group.get_records`.
- *
- * Then displays the aggregates in the table through
- * :js:method:`~openerp.web.ListView.display_aggregates`.
- *
- * @param {Array} [records]
- */
- compute_aggregates: function (records) {
- var columns = _(this.aggregate_columns).filter(function (column) {
- return column['function']; });
- if (_.isEmpty(columns)) { return; }
-
- if (_.isEmpty(records)) {
- records = this.groups.get_records();
- }
-
- var count = 0, sums = {};
- _(columns).each(function (column) {
- switch (column['function']) {
- case 'max':
- sums[column.id] = -Infinity;
- break;
- case 'min':
- sums[column.id] = Infinity;
- break;
- default:
- sums[column.id] = 0;
- }
- });
- _(records).each(function (record) {
- count += record.count || 1;
- _(columns).each(function (column) {
- var field = column.id,
- value = record.values[field];
- switch (column['function']) {
- case 'sum':
- sums[field] += value;
- break;
- case 'avg':
- sums[field] += record.count * value;
- break;
- case 'min':
- if (sums[field] > value) {
- sums[field] = value;
- }
- break;
- case 'max':
- if (sums[field] < value) {
- sums[field] = value;
- }
- break;
- }
- });
- });
-
- var aggregates = {};
- _(columns).each(function (column) {
- var field = column.id;
- switch (column['function']) {
- case 'avg':
- aggregates[field] = {value: sums[field] / count};
- break;
- default:
- aggregates[field] = {value: sums[field]};
- }
- });
-
- this.display_aggregates(aggregates);
- },
- display_aggregates: function (aggregation) {
- var $footer_cells = this.$element.find('.oe-list-footer');
- _(this.aggregate_columns).each(function (column) {
- if (!column['function']) {
- return;
- }
-
- $footer_cells.filter(_.sprintf('[data-field=%s]', column.id))
- .html(openerp.web.format_cell(aggregation, column));
- });
- },
- get_selected_ids: function() {
- var ids = this.groups.get_selection().ids;
- return ids;
- },
- /**
- * Adds padding columns at the start or end of all table rows (including
- * field names row)
- *
- * @param {Number} count number of columns to add
- * @param {Object} options
- * @param {"before"|"after"} [position="after"] insertion position for the new columns
- * @param {Object} [except] content row to not pad
- */
- pad_columns: function (count, options) {
- options = options || {};
- // padding for action/pager header
- var $first_header = this.$element.find('thead tr:first th');
- var colspan = $first_header.attr('colspan');
- if (colspan) {
- if (!this.previous_colspan) {
- this.previous_colspan = colspan;
- }
- $first_header.attr('colspan', parseInt(colspan, 10) + count);
- }
- // Padding for column titles, footer and data rows
- var $rows = this.$element
- .find('.oe-listview-header-columns, tr:not(thead tr)')
- .not(options['except']);
- var newcols = new Array(count+1).join('<td class="oe-listview-padding"></td>');
- if (options.position === 'before') {
- $rows.prepend(newcols);
- } else {
- $rows.append(newcols);
- }
- },
- /**
- * Removes all padding columns of the table
- */
- unpad_columns: function () {
- this.$element.find('.oe-listview-padding').remove();
- if (this.previous_colspan) {
- this.$element
- .find('thead tr:first th')
- .attr('colspan', this.previous_colspan);
- this.previous_colspan = null;
- }
- }
-});
-openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.ListView.List# */{
- /**
- * List display for the ListView, handles basic DOM events and transforms
- * them in the relevant higher-level events, to which the list view (or
- * other consumers) can subscribe.
- *
- * Events on this object are registered via jQuery.
- *
- * Available events:
- *
- * `selected`
- * Triggered when a row is selected (using check boxes), provides an
- * array of ids of all the selected records.
- * `deleted`
- * Triggered when deletion buttons are hit, provide an array of ids of
- * all the records being marked for suppression.
- * `action`
- * Triggered when an action button is clicked, provides two parameters:
- *
- * * The name of the action to execute (as a string)
- * * The id of the record to execute the action on
- * `row_link`
- * Triggered when a row of the table is clicked, provides the index (in
- * the rows array) and id of the selected record to the handle function.
- *
- * @constructs openerp.web.ListView.List
- * @extends openerp.web.Class
- *
- * @param {Object} opts display options, identical to those of :js:class:`openerp.web.ListView`
- */
- init: function (group, opts) {
- var self = this;
- this.group = group;
- this.view = group.view;
- this.session = this.view.session;
-
- this.options = opts.options;
- this.columns = opts.columns;
- this.dataset = opts.dataset;
- this.records = opts.records;
-
- this.record_callbacks = {
- 'remove': function (event, record) {
- var $row = self.$current.find(
- '[data-id=' + record.get('id') + ']');
- var index = $row.data('index');
- $row.remove();
- self.refresh_zebra(index);
- },
- 'reset': $.proxy(this, 'on_records_reset'),
- 'change': function (event, record) {
- var $row = self.$current.find('[data-id=' + record.get('id') + ']');
- $row.replaceWith(self.render_record(record));
- },
- 'add': function (ev, records, record, index) {
- var $new_row = $('<tr>').attr({
- 'data-id': record.get('id')
- });
-
- if (index === 0) {
- $new_row.prependTo(self.$current);
- } else {
- var previous_record = records.at(index-1),
- $previous_sibling = self.$current.find(
- '[data-id=' + previous_record.get('id') + ']');
- $new_row.insertAfter($previous_sibling);
- }
-
- self.refresh_zebra(index, 1);
- }
- };
- _(this.record_callbacks).each(function (callback, event) {
- this.records.bind(event, callback);
- }, this);
-
- this.$_element = $('<tbody class="ui-widget-content">')
- .appendTo(document.body)
- .delegate('th.oe-record-selector', 'click', function (e) {
- e.stopPropagation();
- var selection = self.get_selection();
- $(self).trigger(
- 'selected', [selection.ids, selection.records]);
- })
- .delegate('td.oe-record-delete button', 'click', function (e) {
- e.stopPropagation();
- var $row = $(e.target).closest('tr');
- $(self).trigger('deleted', [[self.row_id($row)]]);
- })
- .delegate('td.oe-field-cell button', 'click', function (e) {
- e.stopPropagation();
- var $target = $(e.currentTarget),
- field = $target.closest('td').data('field'),
- $row = $target.closest('tr'),
- record_id = self.row_id($row);
-
- $(self).trigger('action', [field, record_id, function () {
- return self.reload_record(self.records.get(record_id));
- }]);
- })
- .delegate('tr', 'click', function (e) {
- e.stopPropagation();
- self.dataset.index = self.records.indexOf(
- self.records.get(
- self.row_id(e.currentTarget)));
- self.row_clicked(e);
- });
- },
- row_clicked: function () {
- $(this).trigger(
- 'row_link',
- [this.records.at(this.dataset.index).get('id'),
- this.dataset]);
- },
- render: function () {
- if (this.$current) {
- this.$current.remove();
- }
- this.$current = this.$_element.clone(true);
- this.$current.empty().append(
- QWeb.render('ListView.rows', _.extend({
- render_cell: openerp.web.format_cell}, this)));
- this.pad_table_to(5);
- },
- pad_table_to: function (count) {
- if (this.records.length >= count ||
- _(this.columns).any(function(column) { return column.meta; })) {
- return;
- }
- var cells = [];
- if (this.options.selectable) {
- cells.push('<td title="selection"></td>');
- }
- _(this.columns).each(function(column) {
- if (column.invisible !== '1') {
- cells.push('<td title="' + column.string + '"> </td>');
- }
- });
- if (this.options.deletable) {
- cells.push('<td><button type="button" style="visibility: hidden"> </button></td>');
- }
- cells.unshift('<tr>');
- cells.push('</tr>');
-
- var row = cells.join('');
- this.$current.append(new Array(count - this.records.length + 1).join(row));
- this.refresh_zebra(this.records.length);
- },
- /**
- * 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.
- */
- get_selection: function () {
- if (!this.options.selectable) {
- return [];
- }
- var records = this.records;
- var result = {ids: [], records: []};
- this.$current.find('th.oe-record-selector input:checked')
- .closest('tr').each(function () {
- var record = records.get($(this).data('id'));
- result.ids.push(record.get('id'));
- result.records.push(record.attributes);
- });
- return result;
- },
- /**
- * Returns the identifier of the object displayed in the provided table
- * row
- *
- * @param {Object} row the selected table row
- * @returns {Number|String} the identifier of the row's object
- */
- row_id: function (row) {
- return $(row).data('id');
- },
- /**
- * Death signal, cleans up list display
- */
- on_records_reset: function () {
- _(this.record_callbacks).each(function (callback, event) {
- this.records.unbind(event, callback);
- }, this);
- if (!this.$current) { return; }
- this.$current.remove();
- this.$current = null;
- this.$_element.remove();
- },
- get_records: function () {
- return this.records.map(function (record) {
- return {count: 1, values: record.attributes};
- });
- },
- /**
- * Reloads the provided record by re-reading its content from the server.
- *
- * @param {Record} record
- * @returns {$.Deferred} promise to the finalization of the reloading
- */
- reload_record: function (record) {
- return this.dataset.read_ids(
- [record.get('id')],
- _.pluck(_(this.columns).filter(function (r) {
- return r.tag === 'field';
- }), 'name'),
- function (records) {
- _(records[0]).each(function (value, key) {
- record.set(key, value, {silent: true});
- });
- record.trigger('change', record);
- }
- );
- },
- /**
- * Renders a list record to HTML
- *
- * @param {Record} record index of the record to render in ``this.rows``
- * @returns {String} QWeb rendering of the selected record
- */
- render_record: function (record) {
- var index = this.records.indexOf(record);
- return QWeb.render('ListView.row', {
- columns: this.columns,
- options: this.options,
- record: record,
- row_parity: (index % 2 === 0) ? 'even' : 'odd',
- render_cell: openerp.web.format_cell
- });
- },
- /**
- * Fixes fixes the even/odd classes
- *
- * @param {Number} [from_index] index from which to resequence
- * @param {Number} [offset = 0] selection offset for DOM, in case there are rows to ignore in the table
- */
- refresh_zebra: function (from_index, offset) {
- offset = offset || 0;
- from_index = from_index || 0;
- var dom_offset = offset + from_index;
- var sel = dom_offset ? ':gt(' + (dom_offset - 1) + ')' : null;
- this.$current.children(sel).each(function (i, e) {
- var index = from_index + i;
- // reset record-index accelerators on rows and even/odd
- var even = index%2 === 0;
- $(e).toggleClass('even', even)
- .toggleClass('odd', !even);
- });
- }
-});
-openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.ListView.Groups# */{
- passtrough_events: 'action deleted row_link',
- /**
- * Grouped display for the ListView. Handles basic DOM events and interacts
- * with the :js:class:`~openerp.web.DataGroup` bound to it.
- *
- * Provides events similar to those of
- * :js:class:`~openerp.web.ListView.List`
- *
- * @constructs openerp.web.ListView.Groups
- * @extends openerp.web.Class
- *
- * @param {openerp.web.ListView} view
- * @param {Object} [options]
- * @param {Collection} [options.records]
- * @param {Object} [options.options]
- * @param {Array} [options.columns]
- */
- init: function (view, options) {
- options = options || {};
- this.view = view;
- this.records = options.records || view.records;
- this.options = options.options || view.options;
- this.columns = options.columns || view.columns;
- this.datagroup = null;
-
- this.$row = null;
- this.children = {};
-
- this.page = 0;
-
- this.records.bind('reset', $.proxy(this, 'on_records_reset'));
- },
- make_fragment: function () {
- return document.createDocumentFragment();
- },
- /**
- * Returns a DOM node after which a new tbody can be inserted, so that it
- * follows the provided row.
- *
- * Necessary to insert the result of a new group or list view within an
- * existing groups render, without losing track of the groups's own
- * elements
- *
- * @param {HTMLTableRowElement} row the row after which the caller wants to insert a body
- * @returns {HTMLTableSectionElement} element after which a tbody can be inserted
- */
- point_insertion: function (row) {
- var $row = $(row);
- var red_letter_tboday = $row.closest('tbody')[0];
-
- var $next_siblings = $row.nextAll();
- if ($next_siblings.length) {
- var $root_kanal = $('<tbody>').insertAfter(red_letter_tboday);
-
- $root_kanal.append($next_siblings);
- this.elements.splice(
- _.indexOf(this.elements, red_letter_tboday),
- 0,
- $root_kanal[0]);
- }
- return red_letter_tboday;
- },
- make_paginator: function () {
- var self = this;
- var $prev = $('<button type="button" data-pager-action="previous"><</button>')
- .click(function (e) {
- e.stopPropagation();
- self.page -= 1;
-
- self.$row.closest('tbody').next()
- .replaceWith(self.render());
- });
- var $next = $('<button type="button" data-pager-action="next">></button>')
- .click(function (e) {
- e.stopPropagation();
- self.page += 1;
-
- self.$row.closest('tbody').next()
- .replaceWith(self.render());
- });
- this.$row.children().last()
- .append($prev)
- .append('<span class="oe-pager-state"></span>')
- .append($next);
- },
- open: function (point_insertion) {
- this.render().insertAfter(point_insertion);
- this.make_paginator();
- },
- close: function () {
- this.$row.children().last().empty();
- this.records.reset();
- },
- /**
- * Prefixes ``$node`` with floated spaces in order to indent it relative
- * to its own left margin/baseline
- *
- * @param {jQuery} $node jQuery object to indent
- * @param {Number} level current nesting level, >= 1
- * @returns {jQuery} the indentation node created
- */
- indent: function ($node, level) {
- return $('<span>')
- .css({'float': 'left', 'white-space': 'pre'})
- .text(new Array(level).join(' '))
- .prependTo($node);
- },
- render_groups: function (datagroups) {
- var self = this;
- var placeholder = this.make_fragment();
- _(datagroups).each(function (group) {
- if (self.children[group.value]) {
- self.records.proxy(group.value).reset();
- delete self.children[group.value];
- }
- var child = self.children[group.value] = new openerp.web.ListView.Groups(self.view, {
- records: self.records.proxy(group.value),
- options: self.options,
- columns: self.columns
- });
- self.bind_child_events(child);
- child.datagroup = group;
-
- var $row = child.$row = $('<tr>');
- if (group.openable) {
- $row.click(function (e) {
- if (!$row.data('open')) {
- $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');
- child.close();
- }
- });
- }
- placeholder.appendChild($row[0]);
-
- var $group_column = $('<th class="oe-group-name">').appendTo($row);
- // Don't fill this if group_by_no_leaf but no group_by
- if (group.grouped_on) {
- var row_data = {};
- row_data[group.grouped_on] = group;
- var group_column = _(self.columns).detect(function (column) {
- return column.id === group.grouped_on; });
- $group_column.html(openerp.web.format_cell(
- row_data, group_column, "Undefined"
- ));
- if (group.openable) {
- // Make openable if not terminal group & group_by_no_leaf
- $group_column
- .prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">');
- }
- }
- self.indent($group_column, group.level);
- // count column
- $('<td>').text(group.length).appendTo($row);
-
- if (self.options.selectable) {
- $row.append('<td>');
- }
- _(self.columns).chain()
- .filter(function (column) {return !column.invisible;})
- .each(function (column) {
- if (column.meta) {
- // do not do anything
- } else if (column.id in group.aggregates) {
- var value = group.aggregates[column.id];
- var format;
- if (column.type === 'integer') {
- format = "%.0f";
- } else if (column.type === 'float') {
- format = "%.2f";
- }
- $('<td>')
- .text(_.sprintf(format, value))
- .appendTo($row);
- } else {
- $row.append('<td>');
- }
- });
- if (self.options.deletable) {
- $row.append('<td class="oe-group-pagination">');
- }
- });
- return placeholder;
- },
- bind_child_events: function (child) {
- var $this = $(this),
- self = this;
- $(child).bind('selected', function (e) {
- // can have selections spanning multiple links
- var selection = self.get_selection();
- $this.trigger(e, [selection.ids, selection.records]);
- }).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.
- // Convert our *args back into an Array in order to trigger them
- // on the group itself, so it can ultimately be forwarded wherever
- // it's supposed to go.
- var args = Array.prototype.slice.call(arguments, 1);
- $this.trigger.call($this, e, args);
- });
- },
- render_dataset: function (dataset) {
- var self = this,
- list = new openerp.web.ListView.List(this, {
- options: this.options,
- columns: this.columns,
- dataset: dataset,
- records: this.records
- });
- this.bind_child_events(list);
-
- var view = this.view,
- limit = view.limit(),
- d = new $.Deferred(),
- page = this.datagroup.openable ? this.page : view.page;
-
- var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field"}), 'name');
- var options = { offset: page * limit, limit: limit };
- dataset.read_slice(fields, options , function (records) {
- if (!self.datagroup.openable) {
- view.configure_pager(dataset);
- } else {
- var pages = Math.ceil(dataset.ids.length / limit);
- self.$row
- .find('.oe-pager-state')
- .text(_.sprintf('%d/%d', page + 1, pages))
- .end()
- .find('button[data-pager-action=previous]')
- .attr('disabled', page === 0)
- .end()
- .find('button[data-pager-action=next]')
- .attr('disabled', page === pages - 1);
- }
-
- self.records.add(records, {silent: true});
- list.render();
- d.resolve(list);
- });
- return d.promise();
- },
- setup_resequence_rows: function (list, dataset) {
- // drag and drop enabled if list is not sorted and there is a
- // "sequence" column in the view.
- if ((dataset.sort && dataset.sort())
- || !_(this.columns).any(function (column) {
- return column.name === 'sequence'; })) {
- return;
- }
- // ondrop, move relevant record & fix sequences
- list.$current.sortable({
- axis: 'y',
- items: '> tr[data-id]',
- stop: function (event, ui) {
- var to_move = list.records.get(ui.item.data('id')),
- target_id = ui.item.prev().data('id');
-
- list.records.remove(to_move);
- var to = target_id ? list.records.indexOf(list.records.get(target_id)) + 1 : 0;
- list.records.add(to_move, { at: to });
-
- // resequencing time!
- var record, index = to,
- // if drag to 1st row (to = 0), start sequencing from 0
- // (exclusive lower bound)
- seq = to ? list.records.at(to - 1).get('sequence') : 0;
- while (++seq, record = list.records.at(index++)) {
- // write are independent from one another, so we can just
- // launch them all at the same time and we don't really
- // give a fig about when they're done
- dataset.write(record.get('id'), {sequence: seq});
- record.set('sequence', seq);
- }
-
- list.refresh_zebra();
- }
- });
- },
- render: function (post_render) {
- var self = this;
- var $element = $('<tbody>');
- this.elements = [$element[0]];
-
- this.datagroup.list(
- _(this.view.visible_columns).chain()
- .filter(function (column) { return column.tag === 'field' })
- .pluck('name').value(),
- function (groups) {
- $element[0].appendChild(
- self.render_groups(groups));
- if (post_render) { post_render(); }
- }, function (dataset) {
- self.render_dataset(dataset).then(function (list) {
- self.children[null] = list;
- self.elements =
- [list.$current.replaceAll($element)[0]];
- self.setup_resequence_rows(list, dataset);
- if (post_render) { post_render(); }
- });
- });
- return $element;
- },
- /**
- * Returns the ids of all selected records for this group, and the records
- * themselves
- */
- get_selection: function () {
- var ids = [], records = [];
-
- _(this.children)
- .each(function (child) {
- var selection = child.get_selection();
- ids.push.apply(ids, selection.ids);
- records.push.apply(records, selection.records);
- });
-
- return {ids: ids, records: records};
- },
- on_records_reset: function () {
- this.children = {};
- $(this.elements).remove();
- },
- 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();
- }).flatten().value();
- }
-});
-
-/**
- * @mixin Events
- */
-var Events = /** @lends Events# */{
- /**
- * @param {String} event event to listen to on the current object, null for all events
- * @param {Function} handler event handler to bind to the relevant event
- * @returns this
- */
- bind: function (event, handler) {
- var calls = this['_callbacks'] || (this._callbacks = {});
-
- if (event in calls) {
- calls[event].push(handler);
- } else {
- calls[event] = [handler];
- }
- return this;
- },
- /**
- * @param {String} event event to unbind on the current object
- * @param {function} [handler] specific event handler to remove (otherwise unbind all handlers for the event)
- * @returns this
- */
- unbind: function (event, handler) {
- var calls = this._callbacks || {};
- if (!(event in calls)) { return this; }
- if (!handler) {
- delete calls[event];
- } else {
- var handlers = calls[event];
- handlers.splice(
- _(handlers).indexOf(handler),
- 1);
- }
- return this;
- },
- /**
- * @param {String} event
- * @returns this
- */
- trigger: function (event) {
- var calls;
- if (!(calls = this._callbacks)) { return this; }
- var callbacks = (calls[event] || []).concat(calls[null] || []);
- for(var i=0, length=callbacks.length; i<length; ++i) {
- callbacks[i].apply(this, arguments);
- }
- return this;
- }
-};
-var Record = openerp.web.Class.extend(/** @lends Record# */{
- /**
- * @constructs Record
- * @extends openerp.web.Class
- *
- * @mixes Events
- * @param {Object} [data]
- */
- init: function (data) {
- this.attributes = data || {};
- },
- /**
- * @param {String} key
- * @returns {Object}
- */
- get: function (key) {
- return this.attributes[key];
- },
- /**
- * @param key
- * @param value
- * @param {Object} [options]
- * @param {Boolean} [options.silent=false]
- * @returns {Record}
- */
- set: function (key, value, options) {
- options = options || {};
- var old_value = this.attributes[key];
- if (old_value === value) {
- return this;
- }
- this.attributes[key] = value;
- if (!options.silent) {
- this.trigger('change:' + key, this, value, old_value);
- this.trigger('change', this, key, value, old_value);
- }
- return this;
- },
- /**
- * Converts the current record to the format expected by form views:
- *
- * .. code-block:: javascript
- *
- * data: {
- * $fieldname: {
- * value: $value
- * }
- * }
- *
- *
- * @returns {Object} record displayable in a form view
- */
- toForm: function () {
- var form_data = {};
- _(this.attributes).each(function (value, key) {
- form_data[key] = {value: value};
- });
-
- return {data: form_data};
- }
-});
-Record.include(Events);
-var Collection = openerp.web.Class.extend(/** @lends Collection# */{
- /**
- * Smarter collections, with events, very strongly inspired by Backbone's.
- *
- * Using a "dumb" array of records makes synchronization between the
- * various serious
- *
- * @constructs Collection
- * @extends openerp.web.Class
- *
- * @mixes Events
- * @param {Array} [records] records to initialize the collection with
- * @param {Object} [options]
- */
- init: function (records, options) {
- options = options || {};
- _.bindAll(this, '_onRecordEvent');
- this.length = 0;
- this.records = [];
- this._byId = {};
- this._proxies = {};
- this._key = options.key;
- this._parent = options.parent;
-
- if (records) {
- this.add(records);
- }
- },
- /**
- * @param {Object|Array} record
- * @param {Object} [options]
- * @param {Number} [options.at]
- * @param {Boolean} [options.silent=false]
- * @returns this
- */
- add: function (record, options) {
- options = options || {};
- var records = record instanceof Array ? record : [record];
-
- for(var i=0, length=records.length; i<length; ++i) {
- var instance = (records[i] instanceof Record) ? records[i] : new Record(records[i]);
- instance.bind(null, this._onRecordEvent);
- this._byId[instance.get('id')] = instance;
- if (options.at == undefined) {
- this.records.push(instance);
- if (!options.silent) {
- this.trigger('add', this, instance, this.records.length-1);
- }
- } else {
- var insertion_index = options.at + i;
- this.records.splice(insertion_index, 0, instance);
- if (!options.silent) {
- this.trigger('add', this, instance, insertion_index);
- }
- }
- this.length++;
- }
- return this;
- },
-
- /**
- * Get a record by its index in the collection, can also take a group if
- * the collection is not degenerate
- *
- * @param {Number} index
- * @param {String} [group]
- * @returns {Record|undefined}
- */
- at: function (index, group) {
- if (group) {
- var groups = group.split('.');
- return this._proxies[groups[0]].at(index, groups.join('.'));
- }
- return this.records[index];
- },
- /**
- * Get a record by its database id
- *
- * @param {Number} id
- * @returns {Record|undefined}
- */
- get: function (id) {
- if (!_(this._proxies).isEmpty()) {
- var record = null;
- _(this._proxies).detect(function (proxy) {
- return record = proxy.get(id);
- });
- return record;
- }
- return this._byId[id];
- },
- /**
- * Builds a proxy (insert/retrieve) to a subtree of the collection, by
- * the subtree's group
- *
- * @param {String} section group path section
- * @returns {Collection}
- */
- proxy: function (section) {
- return this._proxies[section] = new Collection(null, {
- parent: this,
- key: section
- }).bind(null, this._onRecordEvent);
- },
- /**
- * @param {Array} [records]
- * @returns this
- */
- reset: function (records) {
- _(this._proxies).each(function (proxy) {
- proxy.reset();
- });
- this._proxies = {};
- this.length = 0;
- this.records = [];
- this._byId = {};
- if (records) {
- this.add(records);
- }
- this.trigger('reset', this);
- return this;
- },
- /**
- * Removes the provided record from the collection
- *
- * @param {Record} record
- * @returns this
- */
- remove: function (record) {
- var self = this;
- var index = _(this.records).indexOf(record);
- if (index === -1) {
- _(this._proxies).each(function (proxy) {
- proxy.remove(record);
- });
- return this;
- }
-
- this.records.splice(index, 1);
- delete this._byId[record.get('id')];
- this.length--;
- this.trigger('remove', record, this);
- return this;
- },
-
- _onRecordEvent: function (event, record, options) {
- // don't propagate reset events
- if (event === 'reset') { return; }
- this.trigger.apply(this, arguments);
- },
-
- // underscore-type methods
- each: function (callback) {
- for(var section in this._proxies) {
- if (this._proxies.hasOwnProperty(section)) {
- this._proxies[section].each(callback);
- }
- }
- for(var i=0; i<this.length; ++i) {
- callback(this.records[i]);
- }
- },
- map: function (callback) {
- var results = [];
- this.each(function (record) {
- results.push(callback(record));
- });
- return results;
- },
- pluck: function (fieldname) {
- return this.map(function (record) {
- return record.get(fieldname);
- });
- },
- indexOf: function (record) {
- return _(this.records).indexOf(record);
- }
-});
-Collection.include(Events);
-openerp.web.list = {
- Events: Events,
- Record: Record,
- Collection: Collection
-}
-};
-// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
--- /dev/null
+openerp.web.form = function (openerp) {
+
+var _t = openerp.web._t;
+var QWeb = openerp.web.qweb;
+
+openerp.web.views.add('form', 'openerp.web.FormView');
+openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# */{
+ /**
+ * Indicates that this view is not searchable, and thus that no search
+ * view should be displayed (if there is one active).
+ */
+ searchable: false,
+ template: "FormView",
+ /**
+ * @constructs openerp.web.FormView
+ * @extends openerp.web.View
+ *
+ * @param {openerp.web.Session} session the current openerp session
+ * @param {String} element_id this view's root element id
+ * @param {openerp.web.DataSet} dataset the dataset this view will work with
+ * @param {String} view_id the identifier of the OpenERP view object
+ *
+ * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance
+ */
+ init: function(parent, element_id, dataset, view_id, options) {
+ this._super(parent, element_id);
+ this.set_default_options(options);
+ this.dataset = dataset;
+ this.model = dataset.model;
+ this.view_id = view_id;
+ this.fields_view = {};
+ this.widgets = {};
+ this.widgets_counter = 0;
+ this.fields = {};
+ this.datarecord = {};
+ this.ready = false;
+ this.show_invalid = true;
+ this.dirty = false;
+ this.default_focus_field = null;
+ this.default_focus_button = null;
+ this.registry = openerp.web.form.widgets;
+ this.has_been_loaded = $.Deferred();
+ this.$form_header = null;
+ this.translatable_fields = [];
+ _.defaults(this.options, {"always_show_new_button": true});
+ },
+ start: function() {
+ if (this.embedded_view) {
+ var def = $.Deferred().then(this.on_loaded);
+ var self = this;
+ setTimeout(function() {def.resolve(self.embedded_view);}, 0);
+ return def.promise();
+ } else {
+ var context = new openerp.web.CompoundContext(this.dataset.get_context());
+ return this.rpc("/web/view/load", {
+ "model": this.model,
+ "view_id": this.view_id,
+ "view_type": "form",
+ toolbar: this.options.sidebar,
+ context: context
+ }, this.on_loaded);
+ }
+ },
+ stop: function() {
+ if (this.sidebar) {
+ this.sidebar.attachments.stop();
+ this.sidebar.stop();
+ }
+ _.each(this.widgets, function(w) {
+ w.stop();
+ });
+ },
+ on_loaded: function(data) {
+ var self = this;
+ this.fields_view = data;
+ var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
+
+ this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this }));
+ _.each(this.widgets, function(w) {
+ w.start();
+ });
+ this.$form_header = this.$element.find('#' + this.element_id + '_header');
+ this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() {
+ var action = $(this).data('pager-action');
+ self.on_pager_action(action);
+ });
+
+ this.$form_header.find('button.oe_form_button_save').click(this.do_save);
+ this.$form_header.find('button.oe_form_button_save_edit').click(this.do_save_edit);
+ this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel);
+ this.$form_header.find('button.oe_form_button_new').click(this.on_button_new);
+
+ if (this.options.sidebar && this.options.sidebar_id) {
+ this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
+ this.sidebar.start();
+ this.sidebar.do_unfold();
+ this.sidebar.attachments = new openerp.web.form.SidebarAttachments(this.sidebar, this.sidebar.add_section('attachments', "Attachments"), this);
+ this.sidebar.add_toolbar(this.fields_view.toolbar);
+ this.set_common_sidebar_sections(this.sidebar);
+ }
+ this.has_been_loaded.resolve();
+ },
+ do_show: function () {
+ var promise;
+ if (this.dataset.index === null) {
+ // null index means we should start a new record
+ promise = this.on_button_new();
+ } else {
+ promise = this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
+ }
+ this.$element.show();
+ if (this.sidebar) {
+ this.sidebar.$element.show();
+ }
+ return promise;
+ },
+ do_hide: function () {
+ this.$element.hide();
+ if (this.sidebar) {
+ this.sidebar.$element.hide();
+ }
+ },
+ on_record_loaded: function(record) {
+ if (!record) {
+ throw("Form: No record received");
+ }
+ if (!record.id) {
+ this.$form_header.find('.oe_form_on_create').show();
+ this.$form_header.find('.oe_form_on_update').hide();
+ if (!this.options["always_show_new_button"]) {
+ this.$form_header.find('button.oe_form_button_new').hide();
+ }
+ } else {
+ this.$form_header.find('.oe_form_on_create').hide();
+ this.$form_header.find('.oe_form_on_update').show();
+ this.$form_header.find('button.oe_form_button_new').show();
+ }
+ this.dirty = false;
+ this.datarecord = record;
+ for (var f in this.fields) {
+ var field = this.fields[f];
+ field.dirty = false;
+ field.set_value(this.datarecord[f] || false);
+ field.validate();
+ }
+ if (!record.id) {
+ // New record: Second pass in order to trigger the onchanges
+ this.dirty = true;
+ this.show_invalid = false;
+ for (var f in record) {
+ var field = this.fields[f];
+ if (field) {
+ field.dirty = true;
+ this.do_onchange(field);
+ }
+ }
+ }
+ this.on_form_changed();
+ this.show_invalid = this.ready = true;
+ this.do_update_pager(record.id == null);
+ if (this.sidebar) {
+ this.sidebar.attachments.do_update();
+ this.sidebar.$element.find('.oe_sidebar_translate').toggleClass('oe_hide', !record.id);
+ }
+ if (this.default_focus_field && !this.embedded_view) {
+ this.default_focus_field.focus();
+ }
+ },
+ on_form_changed: function() {
+ for (var w in this.widgets) {
+ w = this.widgets[w];
+ w.process_modifiers();
+ w.update_dom();
+ }
+ },
+ on_pager_action: function(action) {
+ switch (action) {
+ case 'first':
+ this.dataset.index = 0;
+ break;
+ case 'previous':
+ this.dataset.previous();
+ break;
+ case 'next':
+ this.dataset.next();
+ break;
+ case 'last':
+ this.dataset.index = this.dataset.ids.length - 1;
+ break;
+ }
+ this.reload();
+ },
+ do_update_pager: function(hide_index) {
+ var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager');
+ var index = hide_index ? '-' : this.dataset.index + 1;
+ $pager.find('span.oe_pager_index').html(index);
+ $pager.find('span.oe_pager_count').html(this.dataset.ids.length);
+ },
+ do_onchange: function(widget, processed) {
+ processed = processed || [];
+ if (widget.node.attrs.on_change) {
+ var self = this;
+ this.ready = false;
+ var onchange = _.trim(widget.node.attrs.on_change);
+ var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
+ if (call) {
+ var method = call[1], args = [];
+ var context_index = null;
+ var argument_replacement = {
+ 'False' : function() {return false;},
+ 'True' : function() {return true;},
+ 'None' : function() {return null;},
+ 'context': function(i) {
+ context_index = i;
+ var ctx = widget.build_context ? widget.build_context() : {};
+ return ctx;
+ }
+ };
+ var parent_fields = null;
+ _.each(call[2].split(','), function(a, i) {
+ var field = _.trim(a);
+ if (field in argument_replacement) {
+ args.push(argument_replacement[field](i));
+ return;
+ } else if (self.fields[field]) {
+ var value = self.fields[field].get_on_change_value();
+ args.push(value == null ? false : value);
+ return;
+ } else {
+ var splitted = field.split('.');
+ if (splitted.length > 1 && _.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
+ if (parent_fields === null) {
+ parent_fields = self.dataset.parent_view.get_fields_values();
+ }
+ var p_val = parent_fields[_.trim(splitted[1])];
+ if (p_val !== undefined) {
+ args.push(p_val == null ? false : p_val);
+ return;
+ }
+ }
+ }
+ throw "Could not get field with name '" + field +
+ "' for onchange '" + onchange + "'";
+ });
+ var ajax = {
+ url: '/web/dataset/call',
+ async: false
+ };
+ return this.rpc(ajax, {
+ model: this.dataset.model,
+ method: method,
+ args: [(this.datarecord.id == null ? [] : [this.datarecord.id])].concat(args),
+ context_id: context_index === null ? null : context_index + 1
+ }, function(response) {
+ self.on_processed_onchange(response, processed);
+ });
+ } else {
+ console.log("Wrong on_change format", on_change);
+ }
+ }
+ },
+ on_processed_onchange: function(response, processed) {
+ var result = response;
+ if (result.value) {
+ for (var f in result.value) {
+ var field = this.fields[f];
+ // If field is not defined in the view, just ignore it
+ if (field) {
+ var value = result.value[f];
+ processed.push(field.name);
+ if (field.get_value() != value) {
+ field.set_value(value);
+ field.dirty = true;
+ if (_.indexOf(processed, field.name) < 0) {
+ this.do_onchange(field, processed);
+ }
+ }
+ }
+ }
+ this.on_form_changed();
+ }
+ if (!_.isEmpty(result.warning)) {
+ $(QWeb.render("DialogWarning", result.warning)).dialog({
+ modal: true,
+ buttons: {
+ Ok: function() {
+ $(this).dialog("close");
+ }
+ }
+ });
+ }
+ if (result.domain) {
+ // TODO:
+ }
+ this.ready = true;
+ },
+ on_button_new: function() {
+ var self = this;
+ var def = $.Deferred();
+ $.when(this.has_been_loaded).then(function() {
+ self.dataset.default_get(
+ _.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() {
+ def.resolve();
+ });
+ });
+ return def.promise();
+ },
+ /**
+ * 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;
+ }
+ var form_dirty = false,
+ form_invalid = false,
+ values = {},
+ first_invalid_field = null;
+ for (var f in this.fields) {
+ f = this.fields[f];
+ if (!f.is_valid()) {
+ form_invalid = true;
+ f.update_dom();
+ if (!first_invalid_field) {
+ first_invalid_field = f;
+ }
+ } else if (f.is_dirty()) {
+ form_dirty = true;
+ values[f.name] = f.get_value();
+ }
+ }
+ if (form_invalid) {
+ first_invalid_field.focus();
+ this.on_invalid();
+ return false;
+ } else if (form_dirty) {
+ console.log("About to save", values);
+ if (!this.datarecord.id) {
+ return this.dataset.create(values, function(r) {
+ self.on_created(r, success, prepend_on_create);
+ });
+ } else {
+ return this.dataset.write(this.datarecord.id, values, {}, function(r) {
+ self.on_saved(r, success);
+ });
+ }
+ } else {
+ setTimeout(function() {
+ self.on_saved({ result: true }, success);
+ });
+ return true;
+ }
+ },
+ do_save_edit: function() {
+ this.do_save();
+ //this.switch_readonly(); Use promises
+ },
+ switch_readonly: function() {
+ },
+ switch_editable: function() {
+ },
+ on_invalid: function() {
+ var msg = "<ul>";
+ _.each(this.fields, function(f) {
+ if (!f.is_valid()) {
+ msg += "<li>" + f.string + "</li>";
+ }
+ });
+ msg += "</ul>";
+ this.notification.warn("The following fields are invalid :", msg);
+ },
+ on_saved: function(r, success) {
+ if (!r.result) {
+ // should not happen in the server, but may happen for internal purpose
+ } else {
+ console.debug(_.sprintf("The record #%s has been saved.", this.datarecord.id));
+ if (success) {
+ success(r);
+ }
+ this.reload();
+ }
+ },
+ /**
+ * 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) {
+ // should not happen in the server, but may happen for internal purpose
+ } else {
+ 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.do_update_pager();
+ if (this.sidebar) {
+ this.sidebar.attachments.do_update();
+ }
+ console.debug("The record has been created with id #" + this.datarecord.id);
+ if (success) {
+ success(_.extend(r, {created: true}));
+ }
+ this.reload();
+ }
+ },
+ do_search: function (domains, contexts, groupbys) {
+ console.debug("Searching form");
+ },
+ on_action: function (action) {
+ console.debug('Executing action', action);
+ },
+ do_cancel: function () {
+ console.debug("Cancelling form");
+ },
+ reload: function() {
+ if (this.dataset.index == null || this.dataset.index < 0) {
+ this.on_button_new();
+ } else {
+ this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
+ }
+ },
+ get_fields_values: function() {
+ var values = {};
+ _.each(this.fields, function(value, key) {
+ var val = value.get_value();
+ values[key] = val;
+ });
+ return values;
+ },
+ get_selected_ids: function() {
+ var id = this.dataset.ids[this.dataset.index];
+ return id ? [id] : [];
+ }
+});
+openerp.web.FormDialog = openerp.web.Dialog.extend({
+ init: function(parent, options, view_id, dataset) {
+ this._super(parent, options);
+ this.dataset = dataset;
+ this.view_id = view_id;
+ return this;
+ },
+ start: function() {
+ this._super();
+ this.form = new openerp.web.FormView(this, this.element_id, this.dataset, this.view_id, {
+ sidebar: false,
+ pager: false
+ });
+ this.form.start();
+ this.form.on_created.add_last(this.on_form_dialog_saved);
+ this.form.on_saved.add_last(this.on_form_dialog_saved);
+ return this;
+ },
+ load_id: function(id) {
+ var self = this;
+ return this.dataset.read_ids([id], _.keys(this.form.fields_view.fields), function(records) {
+ self.form.on_record_loaded(records[0]);
+ });
+ },
+ on_form_dialog_saved: function(r) {
+ this.close();
+ }
+});
+
+/** @namespace */
+openerp.web.form = {};
+
+openerp.web.form.SidebarAttachments = openerp.web.Widget.extend({
+ init: function(parent, element_id, form_view) {
+ this._super(parent, element_id);
+ this.view = form_view;
+ },
+ do_update: function() {
+ if (!this.view.datarecord.id) {
+ this.on_attachments_loaded([]);
+ } else {
+ (new openerp.web.DataSetSearch(
+ this, 'ir.attachment', this.view.dataset.get_context(),
+ [
+ ['res_model', '=', this.view.dataset.model],
+ ['res_id', '=', this.view.datarecord.id],
+ ['type', 'in', ['binary', 'url']]
+ ])).read_slice(['name', 'url', 'type'], {}, this.on_attachments_loaded);
+ }
+ },
+ on_attachments_loaded: function(attachments) {
+ this.attachments = attachments;
+ this.$element.html(QWeb.render('FormView.sidebar.attachments', this));
+ this.$element.find('.oe-binary-file').change(this.on_attachment_changed);
+ this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete);
+ },
+ on_attachment_changed: function(e) {
+ window[this.element_id + '_iframe'] = this.do_update;
+ var $e = $(e.target);
+ if ($e.val() != '') {
+ this.$element.find('form.oe-binary-form').submit();
+ $e.parent().find('input[type=file]').attr('disabled', 'true');
+ $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle();
+ }
+ },
+ on_attachment_delete: function(e) {
+ var self = this, $e = $(e.currentTarget);
+ var name = _.trim($e.parent().find('a.oe-sidebar-attachments-link').text());
+ if (confirm("Do you really want to delete the attachment " + name + " ?")) {
+ this.rpc('/web/dataset/unlink', {
+ model: 'ir.attachment',
+ ids: [parseInt($e.attr('data-id'))]
+ }, function(r) {
+ $e.parent().remove();
+ self.notification.notify("Delete an attachment", "The attachment '" + name + "' has been deleted");
+ });
+ }
+ }
+});
+
+openerp.web.form.compute_domain = function(expr, fields) {
+ var stack = [];
+ for (var i = expr.length - 1; i >= 0; i--) {
+ var ex = expr[i];
+ if (ex.length == 1) {
+ var top = stack.pop();
+ switch (ex) {
+ case '|':
+ stack.push(stack.pop() || top);
+ continue;
+ case '&':
+ stack.push(stack.pop() && top);
+ continue;
+ case '!':
+ stack.push(!top);
+ continue;
+ default:
+ throw new Error('Unknown domain operator ' + ex);
+ }
+ }
+
+ var field = fields[ex[0]];
+ if (!field) {
+ throw new Error("Domain references unknown field : " + ex[0]);
+ }
+ var field_value = field.get_value ? fields[ex[0]].get_value() : fields[ex[0]].value;
+ var op = ex[1];
+ var val = ex[2];
+
+ switch (op.toLowerCase()) {
+ case '=':
+ case '==':
+ stack.push(field_value == val);
+ break;
+ case '!=':
+ case '<>':
+ stack.push(field_value != val);
+ break;
+ case '<':
+ stack.push(field_value < val);
+ break;
+ case '>':
+ stack.push(field_value > val);
+ break;
+ case '<=':
+ stack.push(field_value <= val);
+ break;
+ case '>=':
+ stack.push(field_value >= val);
+ break;
+ case 'in':
+ stack.push(_(val).contains(field_value));
+ break;
+ case 'not in':
+ stack.push(!_(val).contains(field_value));
+ break;
+ default:
+ console.log("Unsupported operator in modifiers :", op);
+ }
+ }
+ return _.all(stack, _.identity);
+};
+
+openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{
+ template: 'Widget',
+ /**
+ * @constructs openerp.web.form.Widget
+ * @extends openerp.web.Widget
+ *
+ * @param view
+ * @param node
+ */
+ init: function(view, node) {
+ this.view = view;
+ this.node = node;
+ this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
+ this.type = this.type || node.tag;
+ this.element_name = this.element_name || this.type;
+ this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_");
+
+ this._super(view, this.element_id);
+
+ this.view.widgets[this.element_id] = this;
+ this.children = node.children;
+ this.colspan = parseInt(node.attrs.colspan || 1, 10);
+ this.decrease_max_width = 0;
+
+ this.string = this.string || node.attrs.string;
+ this.help = this.help || node.attrs.help;
+ this.invisible = this.modifiers['invisible'] === true;
+ this.classname = 'oe_form_' + this.type;
+
+ this.width = this.node.attrs.width;
+ },
+ start: function() {
+ this.$element = $('#' + this.element_id);
+ },
+ stop: function() {
+ if (this.$element) {
+ this.$element.remove();
+ }
+ },
+ process_modifiers: function() {
+ var compute_domain = openerp.web.form.compute_domain;
+ for (var a in this.modifiers) {
+ this[a] = compute_domain(this.modifiers[a], this.view.fields);
+ }
+ },
+ update_dom: function() {
+ this.$element.toggle(!this.invisible);
+ },
+ render: function() {
+ var template = this.template;
+ return QWeb.render(template, { "widget": this });
+ }
+});
+
+openerp.web.form.WidgetFrame = openerp.web.form.Widget.extend({
+ template: 'WidgetFrame',
+ init: function(view, node) {
+ this._super(view, node);
+ this.columns = parseInt(node.attrs.col || 4, 10);
+ this.x = 0;
+ this.y = 0;
+ this.table = [];
+ this.add_row();
+ for (var i = 0; i < node.children.length; i++) {
+ var n = node.children[i];
+ if (n.tag == "newline") {
+ this.add_row();
+ } else {
+ this.handle_node(n);
+ }
+ }
+ this.set_row_cells_with(this.table[this.table.length - 1]);
+ },
+ add_row: function(){
+ if (this.table.length) {
+ this.set_row_cells_with(this.table[this.table.length - 1]);
+ }
+ var row = [];
+ this.table.push(row);
+ this.x = 0;
+ this.y += 1;
+ return row;
+ },
+ set_row_cells_with: function(row) {
+ var bypass = 0,
+ max_width = 100;
+ for (var i = 0; i < row.length; i++) {
+ bypass += row[i].width === undefined ? 0 : 1;
+ max_width -= row[i].decrease_max_width;
+ }
+ var size_unit = Math.round(max_width / (this.columns - bypass)),
+ colspan_sum = 0;
+ for (var i = 0; i < row.length; i++) {
+ var w = row[i];
+ colspan_sum += w.colspan;
+ if (w.width === undefined) {
+ var width = (i === row.length - 1 && colspan_sum === this.columns) ? max_width : Math.round(size_unit * w.colspan);
+ max_width -= width;
+ w.width = width + '%';
+ }
+ }
+ },
+ handle_node: function(node) {
+ var type = {};
+ if (node.tag == 'field') {
+ type = this.view.fields_view.fields[node.attrs.name] || {};
+ }
+ var widget = new (this.view.registry.get_any(
+ [node.attrs.widget, type.type, node.tag])) (this.view, node);
+ if (node.tag == 'field') {
+ if (!this.view.default_focus_field || node.attrs.default_focus == '1') {
+ this.view.default_focus_field = widget;
+ }
+ if (node.attrs.nolabel != '1') {
+ var label = new (this.view.registry.get_object('label')) (this.view, node);
+ label["for"] = widget;
+ this.add_widget(label, widget.colspan + 1);
+ }
+ }
+ this.add_widget(widget);
+ },
+ add_widget: function(widget, colspan) {
+ colspan = colspan || widget.colspan;
+ var current_row = this.table[this.table.length - 1];
+ if (current_row.length && (this.x + colspan) > this.columns) {
+ current_row = this.add_row();
+ }
+ current_row.push(widget);
+ this.x += widget.colspan;
+ return widget;
+ }
+});
+
+openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({
+ template: 'WidgetNotebook',
+ init: function(view, node) {
+ this._super(view, node);
+ this.pages = [];
+ for (var i = 0; i < node.children.length; i++) {
+ var n = node.children[i];
+ if (n.tag == "page") {
+ var page = new openerp.web.form.WidgetNotebookPage(this.view, n, this, this.pages.length);
+ this.pages.push(page);
+ }
+ }
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.tabs();
+ this.view.on_button_new.add_last(this.do_select_first_visible_tab);
+ },
+ do_select_first_visible_tab: function() {
+ for (var i = 0; i < this.pages.length; i++) {
+ var page = this.pages[i];
+ if (page.invisible === false) {
+ this.$element.tabs('select', page.index);
+ break;
+ }
+ }
+ }
+});
+
+openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({
+ template: 'WidgetNotebookPage',
+ init: function(view, node, notebook, index) {
+ this.notebook = notebook;
+ this.index = index;
+ this.element_name = 'page_' + index;
+ this._super(view, node);
+ this.element_tab_id = this.element_id + '_tab';
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element_tab = $('#' + this.element_tab_id);
+ },
+ update_dom: function() {
+ if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) {
+ this.notebook.do_select_first_visible_tab();
+ }
+ this.$element_tab.toggle(!this.invisible);
+ this.$element.toggle(!this.invisible);
+ }
+});
+
+openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "WidgetSeparator";
+ this.orientation = node.attrs.orientation || 'horizontal';
+ if (this.orientation === 'vertical') {
+ this.width = '1';
+ }
+ this.classname += '_' + this.orientation;
+ }
+});
+
+openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "WidgetButton";
+ if (this.string) {
+ // We don't have button key bindings in the webclient
+ this.string = this.string.replace(/_/g, '');
+ }
+ if (node.attrs.default_focus == '1') {
+ // TODO fme: provide enter key binding to widgets
+ this.view.default_focus_button = this;
+ }
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.click(this.on_click);
+ },
+ on_click: function(saved) {
+ var self = this;
+ if (!this.node.attrs.special && this.view.dirty && saved !== true) {
+ this.view.do_save(function() {
+ self.on_click(true);
+ });
+ } else {
+ if (this.node.attrs.confirm) {
+ var dialog = $('<div>' + this.node.attrs.confirm + '</div>').dialog({
+ title: 'Confirm',
+ modal: true,
+ buttons: {
+ Ok: function() {
+ self.on_confirmed();
+ $(this).dialog("close");
+ },
+ Cancel: function() {
+ $(this).dialog("close");
+ }
+ }
+ });
+ } else {
+ this.on_confirmed();
+ }
+ }
+ },
+ on_confirmed: function() {
+ var self = this;
+
+ this.view.do_execute_action(
+ this.node.attrs, this.view.dataset, this.view.datarecord.id, function () {
+ self.view.reload();
+ });
+ }
+});
+
+openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
+ init: function(view, node) {
+ this.element_name = 'label_' + node.attrs.name;
+
+ this._super(view, node);
+
+ // TODO fme: support for attrs.align
+ if (this.node.tag == 'label' && (this.node.attrs.colspan || (this.string && this.string.length > 32))) {
+ this.template = "WidgetParagraph";
+ this.colspan = parseInt(this.node.attrs.colspan || 1, 10);
+ } else {
+ this.template = "WidgetLabel";
+ this.colspan = 1;
+ this.width = '1%';
+ this.decrease_max_width = 1;
+ this.nowrap = true;
+ }
+ },
+ render: function () {
+ if (this['for'] && this.type !== 'label') {
+ return QWeb.render(this.template, {widget: this['for']});
+ }
+ // Actual label widgets should not have a false and have type label
+ return QWeb.render(this.template, {widget: this});
+ },
+ start: function() {
+ this._super();
+ var self = this;
+ this.$element.find("label").dblclick(function() {
+ var widget = self['for'] || self;
+ console.log(widget.element_id , widget);
+ window.w = widget;
+ });
+ }
+});
+
+openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.form.Field# */{
+ /**
+ * @constructs openerp.web.form.Field
+ * @extends openerp.web.form.Widget
+ *
+ * @param view
+ * @param node
+ */
+ init: function(view, node) {
+ this.name = node.attrs.name;
+ this.value = undefined;
+ view.fields[this.name] = this;
+ this.type = node.attrs.widget || view.fields_view.fields[node.attrs.name].type;
+ this.element_name = "field_" + this.name + "_" + this.type;
+
+ this._super(view, node);
+
+ if (node.attrs.nolabel != '1' && this.colspan > 1) {
+ this.colspan--;
+ }
+ this.field = view.fields_view.fields[node.attrs.name] || {};
+ this.string = node.attrs.string || this.field.string;
+ this.help = node.attrs.help || this.field.help;
+ this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1';
+ this.readonly = this.modifiers['readonly'] === true;
+ this.required = this.modifiers['required'] === true;
+ this.invalid = false;
+ this.dirty = false;
+
+ this.classname = 'oe_form_field_' + this.type;
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ if (this.field.translate) {
+ this.view.translatable_fields.push(this);
+ this.$element.find('.oe_field_translate').click(this.on_translate);
+ }
+ },
+ set_value: function(value) {
+ this.value = value;
+ this.invalid = false;
+ this.update_dom();
+ this.on_value_changed();
+ },
+ set_value_from_ui: function() {
+ this.on_value_changed();
+ },
+ on_value_changed: function() {
+ },
+ on_translate: function() {
+ this.view.open_translate_dialog(this);
+ },
+ get_value: function() {
+ return this.value;
+ },
+ is_valid: function() {
+ return !this.invalid;
+ },
+ is_dirty: function() {
+ return this.dirty;
+ },
+ get_on_change_value: function() {
+ return this.get_value();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ if (this.field.translate) {
+ this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id);
+ }
+ if (!this.disable_utility_classes) {
+ this.$element.toggleClass('disabled', this.readonly);
+ this.$element.toggleClass('required', this.required);
+ if (this.view.show_invalid) {
+ this.$element.toggleClass('invalid', !this.is_valid());
+ }
+ }
+ },
+ on_ui_change: function() {
+ this.dirty = this.view.dirty = true;
+ this.validate();
+ if (this.is_valid()) {
+ this.set_value_from_ui();
+ this.view.do_onchange(this);
+ this.view.on_form_changed();
+ } else {
+ this.update_dom();
+ }
+ },
+ validate: function() {
+ this.invalid = false;
+ },
+ focus: function() {
+ },
+ _build_view_fields_values: function() {
+ var a_dataset = this.view.dataset || {};
+ var fields_values = this.view.get_fields_values();
+ var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {};
+ fields_values.parent = parent_values;
+ return fields_values;
+ },
+ /**
+ * Builds a new context usable for operations related to fields by merging
+ * the fields'context with the action's context.
+ */
+ build_context: function() {
+ // I previously belevied contexts should be herrited, but now I doubt it
+ //var a_context = this.view.dataset.get_context() || {};
+ var f_context = this.field.context || null;
+ // maybe the default_get should only be used when we do a default_get?
+ var v_context1 = this.node.attrs.default_get || {};
+ var v_context2 = this.node.attrs.context || {};
+ var v_context = new openerp.web.CompoundContext(v_context1, v_context2);
+ if (v_context1.__ref || v_context2.__ref || true) { //TODO niv: remove || true
+ var fields_values = this._build_view_fields_values();
+ v_context.set_eval_context(fields_values);
+ }
+ // if there is a context on the node, overrides the model's context
+ var ctx = f_context || v_context;
+ return ctx;
+ },
+ build_domain: function() {
+ var f_domain = this.field.domain || null;
+ var v_domain = this.node.attrs.domain || [];
+ if (!(v_domain instanceof Array) || true) { //TODO niv: remove || true
+ var fields_values = this._build_view_fields_values();
+ v_domain = new openerp.web.CompoundDomain(v_domain).set_eval_context(fields_values);
+ }
+ // if there is a domain on the node, overrides the model's domain
+ return f_domain || v_domain;
+ }
+});
+
+openerp.web.form.FieldChar = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldChar";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').change(this.on_ui_change);
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ var show_value = openerp.web.format_value(value, this, '');
+ this.$element.find('input').val(show_value);
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').attr('disabled', this.readonly);
+ },
+ set_value_from_ui: function() {
+ this.value = openerp.web.parse_value(this.$element.find('input').val(), this);
+ this._super();
+ },
+ validate: function() {
+ this.invalid = false;
+ try {
+ var value = openerp.web.parse_value(this.$element.find('input').val(), this, '');
+ this.invalid = this.required && value === '';
+ } catch(e) {
+ this.invalid = true;
+ }
+ },
+ focus: function() {
+ this.$element.find('input').focus();
+ }
+});
+
+openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldEmail";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('button').click(this.on_button_clicked);
+ },
+ on_button_clicked: function() {
+ if (!this.value || !this.is_valid()) {
+ this.notification.warn("E-mail error", "Can't send email to invalid e-mail address");
+ } else {
+ location.href = 'mailto:' + this.value;
+ }
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ this.$element.find('a').attr('href', 'mailto:' + this.$element.find('input').val());
+ }
+});
+
+openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldUrl";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('button').click(this.on_button_clicked);
+ },
+ on_button_clicked: function() {
+ if (!this.value) {
+ this.notification.warn("Resource error", "This resource is empty");
+ } else {
+ window.open(this.value);
+ }
+ }
+});
+
+openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({
+ set_value: function(value) {
+ if (value === false || value === undefined) {
+ // As in GTK client, floats default to 0
+ value = 0;
+ this.dirty = true;
+ }
+ this._super.apply(this, [value]);
+ }
+});
+
+openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldDate";
+ this.jqueryui_object = 'datetimepicker';
+ },
+ start: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+ this.$element.find('input').change(this.on_ui_change);
+ this.picker({
+ onSelect: this.on_picker_select,
+ changeMonth: true,
+ changeYear: true,
+ showWeek: true,
+ showButtonPanel: false
+ });
+ this.$element.find('img.oe_datepicker_trigger').click(function() {
+ if (!self.readonly) {
+ self.picker('setDate', self.value || new Date());
+ self.$element.find('.oe_datepicker').toggle();
+ }
+ });
+ this.$element.find('.ui-datepicker-inline').removeClass('ui-widget-content ui-corner-all');
+ this.$element.find('button.oe_datepicker_close').click(function() {
+ self.$element.find('.oe_datepicker').hide();
+ });
+ },
+ picker: function() {
+ return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments);
+ },
+ on_picker_select: function(text, instance) {
+ var date = this.picker('getDate');
+ this.$element.find('input').val(date ? this.format_client(date) : '').change();
+ },
+ set_value: function(value) {
+ value = this.parse(value);
+ this._super(value);
+ this.$element.find('input').val(value ? this.format_client(value) : '');
+ },
+ get_value: function() {
+ return this.format(this.value);
+ },
+ set_value_from_ui: function() {
+ var value = this.$element.find('input').val() || false;
+ this.value = this.parse_client(value);
+ this._super();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').attr('disabled', this.readonly);
+ this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', this.readonly);
+ },
+ validate: function() {
+ this.invalid = false;
+ var value = this.$element.find('input').val();
+ if (value === "") {
+ this.invalid = this.required;
+ } else {
+ try {
+ this.parse_client(value);
+ this.invalid = false;
+ } catch(e) {
+ this.invalid = true;
+ }
+ }
+ },
+ focus: function() {
+ this.$element.find('input').focus();
+ },
+ parse: openerp.web.auto_str_to_date,
+ parse_client: function(v) {
+ return openerp.web.parse_value(v, this.field);
+ },
+ format: function(val) {
+ return openerp.web.auto_date_to_str(val, this.field.type);
+ },
+ format_client: function(v) {
+ return openerp.web.format_value(v, this.field);
+ }
+});
+
+openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.jqueryui_object = 'datepicker';
+ },
+ on_picker_select: function(text, instance) {
+ this._super(text, instance);
+ this.$element.find('.oe_datepicker').hide();
+ }
+});
+
+openerp.web.form.FieldText = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldText";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('textarea').change(this.on_ui_change);
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ var show_value = openerp.web.format_value(value, this, '');
+ this.$element.find('textarea').val(show_value);
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('textarea').attr('disabled', this.readonly);
+ },
+ set_value_from_ui: function() {
+ this.value = openerp.web.parse_value(this.$element.find('textarea').val(), this);
+ this._super();
+ },
+ validate: function() {
+ this.invalid = false;
+ try {
+ var value = openerp.web.parse_value(this.$element.find('textarea').val(), this, '');
+ this.invalid = this.required && value === '';
+ } catch(e) {
+ this.invalid = true;
+ }
+ },
+ focus: function() {
+ this.$element.find('textarea').focus();
+ }
+});
+
+openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldBoolean";
+ },
+ start: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+ this.$element.find('input').click(function() {
+ if ($(this).is(':checked') != self.value) {
+ self.on_ui_change();
+ }
+ });
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ this.$element.find('input')[0].checked = value;
+ },
+ set_value_from_ui: function() {
+ this.value = this.$element.find('input').is(':checked');
+ this._super();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').attr('disabled', this.readonly);
+ },
+ validate: function() {
+ this.invalid = this.required && !this.$element.find('input').is(':checked');
+ },
+ focus: function() {
+ this.$element.find('input').focus();
+ }
+});
+
+openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldProgressBar";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('div').progressbar({
+ value: this.value,
+ disabled: this.readonly
+ });
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ var show_value = Number(value);
+ if (isNaN(show_value)) {
+ show_value = 0;
+ }
+ this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
+ }
+});
+
+openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({
+// to replace view editor
+});
+
+openerp.web.form.FieldSelection = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ var self = this;
+ this._super(view, node);
+ this.template = "FieldSelection";
+ this.values = this.field.selection;
+ _.each(this.values, function(v, i) {
+ if (v[0] === false && v[1] === '') {
+ self.values.splice(i, 1);
+ }
+ });
+ this.values.unshift([false, '']);
+ },
+ start: function() {
+ // Flag indicating whether we're in an event chain containing a change
+ // event on the select, in order to know what to do on keyup[RETURN]:
+ // * If the user presses [RETURN] as part of changing the value of a
+ // selection, we should just let the value change and not let the
+ // event broadcast further (e.g. to validating the current state of
+ // the form in editable list view, which would lead to saving the
+ // current row or switching to the next one)
+ // * If the user presses [RETURN] with a select closed (side-effect:
+ // also if the user opened the select and pressed [RETURN] without
+ // changing the selected value), takes the action as validating the
+ // row
+ var ischanging = false;
+ this._super.apply(this, arguments);
+ this.$element.find('select')
+ .change(this.on_ui_change)
+ .change(function () { ischanging = true; })
+ .click(function () { ischanging = false; })
+ .keyup(function (e) {
+ if (e.which !== 13 || !ischanging) { return; }
+ e.stopPropagation();
+ ischanging = false;
+ });
+ },
+ set_value: function(value) {
+ value = value === null ? false : value;
+ value = value instanceof Array ? value[0] : value;
+ this._super(value);
+ var index = 0;
+ for (var i = 0, ii = this.values.length; i < ii; i++) {
+ if (this.values[i][0] === value) index = i;
+ }
+ this.$element.find('select')[0].selectedIndex = index;
+ },
+ set_value_from_ui: function() {
+ this.value = this.values[this.$element.find('select')[0].selectedIndex][0];
+ this._super();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('select').attr('disabled', this.readonly);
+ },
+ validate: function() {
+ var value = this.values[this.$element.find('select')[0].selectedIndex];
+ this.invalid = !(value && !(this.required && value[0] === false));
+ },
+ focus: function() {
+ this.$element.find('select').focus();
+ }
+});
+
+// jquery autocomplete tweak to allow html
+(function() {
+ var proto = $.ui.autocomplete.prototype,
+ initSource = proto._initSource;
+
+ function filter( array, term ) {
+ var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
+ return $.grep( array, function(value) {
+ return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() );
+ });
+ }
+
+ $.extend( proto, {
+ _initSource: function() {
+ if ( this.options.html && $.isArray(this.options.source) ) {
+ this.source = function( request, response ) {
+ response( filter( this.options.source, request.term ) );
+ };
+ } else {
+ initSource.call( this );
+ }
+ },
+
+ _renderItem: function( ul, item) {
+ return $( "<li></li>" )
+ .data( "item.autocomplete", item )
+ .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
+ .appendTo( ul );
+ }
+ });
+})();
+
+openerp.web.form.dialog = function(content, options) {
+ options = _.extend({
+ autoOpen: true,
+ width: '90%',
+ height: '90%',
+ min_width: '800px',
+ min_height: '600px'
+ }, options || {});
+ options.autoOpen = true;
+ var dialog = new openerp.web.Dialog(null, options);
+ dialog.$dialog = $(content).dialog(dialog.dialog_options);
+ return dialog.$dialog;
+};
+
+openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldMany2One";
+ this.limit = 7;
+ this.value = null;
+ this.cm_id = _.uniqueId('m2o_cm_');
+ this.last_search = [];
+ this.tmp_value = undefined;
+ },
+ start: function() {
+ this._super();
+ var self = this;
+ this.$input = this.$element.find("input");
+ this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
+ this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
+
+ // context menu
+ var init_context_menu_def = $.Deferred().then(function(e) {
+ var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context());
+ rdataset.call("get", ['action', 'client_action_relate',
+ [[self.field.relation, false]], false, rdataset.get_context()], false, 0)
+ .then(function(result) {
+ self.related_entries = result;
+
+ var $cmenu = $("#" + self.cm_id);
+ $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self}));
+ var bindings = {};
+ bindings[self.cm_id + "_search"] = function() {
+ self._search_create_popup("search");
+ };
+ bindings[self.cm_id + "_create"] = function() {
+ self._search_create_popup("form");
+ };
+ bindings[self.cm_id + "_open"] = function() {
+ if (!self.value) {
+ return;
+ }
+ var pop = new openerp.web.form.FormOpenPopup(self.view);
+ pop.show_element(self.field.relation, self.value[0],self.build_context(), {});
+ pop.on_write_completed.add_last(function() {
+ self.set_value(self.value[0]);
+ });
+ };
+ _.each(_.range(self.related_entries.length), function(i) {
+ bindings[self.cm_id + "_related_" + i] = function() {
+ self.open_related(self.related_entries[i]);
+ };
+ });
+ var cmenu = self.$menu_btn.contextMenu(self.cm_id, {'leftClickToo': true,
+ bindings: bindings, itemStyle: {"color": ""},
+ onContextMenu: function() {
+ if(self.value) {
+ $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").removeClass("oe-m2o-disabled-cm");
+ } else {
+ $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").addClass("oe-m2o-disabled-cm");
+ }
+ return true;
+ }, menuStyle: {width: "200px"}
+ });
+ setTimeout(function() {self.$menu_btn.trigger(e);}, 0);
+ });
+ });
+ var ctx_callback = function(e) {init_context_menu_def.resolve(e); e.preventDefault()};
+ this.$menu_btn.bind('contextmenu', ctx_callback);
+ this.$menu_btn.click(ctx_callback);
+
+ // some behavior for input
+ this.$input.keyup(function() {
+ if (self.$input.val() === "") {
+ self._change_int_value(null);
+ } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) {
+ self._change_int_value(undefined);
+ }
+ });
+ this.$drop_down.click(function() {
+ if (self.$input.autocomplete("widget").is(":visible")) {
+ self.$input.autocomplete("close");
+ } else {
+ if (self.value) {
+ self.$input.autocomplete("search", "");
+ } else {
+ self.$input.autocomplete("search");
+ }
+ self.$input.focus();
+ }
+ });
+ var anyoneLoosesFocus = function() {
+ if (!self.$input.is(":focus") &&
+ !self.$input.autocomplete("widget").is(":visible") &&
+ !self.value) {
+ if (self.value === undefined && self.last_search.length > 0) {
+ self._change_int_ext_value(self.last_search[0]);
+ } else {
+ self._change_int_ext_value(null);
+ }
+ }
+ };
+ this.$input.focusout(anyoneLoosesFocus);
+
+ var isSelecting = false;
+ // autocomplete
+ this.$input.autocomplete({
+ source: function(req, resp) { self.get_search_result(req, resp); },
+ select: function(event, ui) {
+ isSelecting = true;
+ var item = ui.item;
+ if (item.id) {
+ self._change_int_value([item.id, item.name]);
+ } else if (item.action) {
+ self._change_int_value(undefined);
+ item.action();
+ return false;
+ }
+ },
+ focus: function(e, ui) {
+ e.preventDefault();
+ },
+ html: true,
+ close: anyoneLoosesFocus,
+ minLength: 0,
+ delay: 0
+ });
+ // used to correct a bug when selecting an element by pushing 'enter' in an editable list
+ this.$input.keyup(function(e) {
+ if (e.which === 13) {
+ if (isSelecting)
+ e.stopPropagation();
+ }
+ isSelecting = false;
+ });
+ },
+ // autocomplete component content handling
+ get_search_result: function(request, response) {
+ var search_val = request.term;
+ var self = this;
+
+ var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
+
+ dataset.name_search(search_val, self.build_domain(), 'ilike',
+ this.limit + 1, function(data) {
+ self.last_search = data;
+ // possible selections for the m2o
+ var values = _.map(data, function(x) {
+ return {label: $('<span />').text(x[1]).html(), name:x[1], id:x[0]};
+ });
+
+ // search more... if more results that max
+ if (values.length > self.limit) {
+ values = values.slice(0, self.limit);
+ values.push({label: _t("<em>Â Â Â Search More...</em>"), action: function() {
+ dataset.name_search(search_val, self.build_domain(), 'ilike'
+ , false, function(data) {
+ self._change_int_value(null);
+ self._search_create_popup("search", data);
+ });
+ }});
+ }
+ // quick create
+ var raw_result = _(data.result).map(function(x) {return x[1];});
+ if (search_val.length > 0 &&
+ !_.include(raw_result, search_val) &&
+ (!self.value || search_val !== self.value[1])) {
+ values.push({label: _.sprintf(_t('<em>Â Â Â Create "<strong>%s</strong>"</em>'),
+ $('<span />').text(search_val).html()), action: function() {
+ self._quick_create(search_val);
+ }});
+ }
+ // create...
+ values.push({label: _t("<em>Â Â Â Create and Edit...</em>"), action: function() {
+ self._change_int_value(null);
+ self._search_create_popup("form", undefined, {"default_name": search_val});
+ }});
+
+ response(values);
+ });
+ },
+ _quick_create: function(name) {
+ var self = this;
+ var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
+ dataset.name_create(name, function(data) {
+ self._change_int_ext_value(data);
+ }).fail(function(error, event) {
+ event.preventDefault();
+ self._change_int_value(null);
+ self._search_create_popup("form", undefined, {"default_name": name});
+ });
+ },
+ // all search/create popup handling
+ _search_create_popup: function(view, ids, context) {
+ var self = this;
+ var pop = new openerp.web.form.SelectCreatePopup(this);
+ pop.select_element(self.field.relation,{
+ initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
+ initial_view: view,
+ disable_multiple_selection: true
+ }, self.build_domain(),
+ new openerp.web.CompoundContext(self.build_context(), context || {}));
+ pop.on_select_elements.add(function(element_ids) {
+ var dataset = new openerp.web.DataSetStatic(self, self.field.relation, self.build_context());
+ dataset.name_get([element_ids[0]], function(data) {
+ self._change_int_ext_value(data[0]);
+ });
+ });
+ },
+ _change_int_ext_value: function(value) {
+ this._change_int_value(value);
+ this.$input.val(this.value ? this.value[1] : "");
+ },
+ _change_int_value: function(value) {
+ this.value = value;
+ var back_orig_value = this.original_value;
+ if (this.value === null || this.value) {
+ this.original_value = this.value;
+ }
+ if (back_orig_value === undefined) { // first use after a set_value()
+ return;
+ }
+ if (this.value !== undefined && ((back_orig_value ? back_orig_value[0] : null)
+ !== (this.value ? this.value[0] : null))) {
+ this.on_ui_change();
+ }
+ },
+ set_value: function(value) {
+ value = value || null;
+ this.invalid = false;
+ var self = this;
+ this.tmp_value = value;
+ self.update_dom();
+ self.on_value_changed();
+ var real_set_value = function(rval) {
+ self.tmp_value = undefined;
+ self.value = rval;
+ self.original_value = undefined;
+ self._change_int_ext_value(rval);
+ };
+ if(typeof(value) === "number") {
+ var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
+ dataset.name_get([value], function(data) {
+ real_set_value(data[0]);
+ }).fail(function() {self.tmp_value = undefined;});
+ } else {
+ setTimeout(function() {real_set_value(value);}, 0);
+ }
+ },
+ get_value: function() {
+ if (this.tmp_value !== undefined) {
+ if (this.tmp_value instanceof Array) {
+ return this.tmp_value[0];
+ }
+ return this.tmp_value ? this.tmp_value : false;
+ }
+ if (this.value === undefined)
+ return this.original_value ? this.original_value[0] : false;
+ return this.value ? this.value[0] : false;
+ },
+ validate: function() {
+ this.invalid = false;
+ var val = this.tmp_value !== undefined ? this.tmp_value : this.value;
+ if (val === null) {
+ this.invalid = this.required;
+ }
+ },
+ open_related: function(related) {
+ var self = this;
+ if (!self.value)
+ return;
+ var additional_context = {
+ active_id: self.value[0],
+ active_ids: [self.value[0]],
+ active_model: self.field.relation
+ };
+ self.rpc("/web/action/load", {
+ action_id: related[2].id,
+ context: additional_context
+ }, function(result) {
+ result.result.context = _.extend(result.result.context || {}, additional_context);
+ self.do_action(result.result);
+ });
+ }
+});
+
+/*
+# Values: (0, 0, { fields }) create
+# (1, ID, { fields }) update
+# (2, ID) remove (delete)
+# (3, ID) unlink one (target id or target of relation)
+# (4, ID) link
+# (5) unlink all (only valid for one2many)
+*/
+var commands = {
+ // (0, _, {values})
+ CREATE: 0,
+ 'create': function (values) {
+ return [commands.CREATE, false, values];
+ },
+ // (1, id, {values})
+ UPDATE: 1,
+ 'update': function (id, values) {
+ return [commands.UPDATE, id, values];
+ },
+ // (2, id[, _])
+ DELETE: 2,
+ 'delete': function (id) {
+ return [commands.DELETE, id, false];
+ },
+ // (3, id[, _]) removes relation, but not linked record itself
+ FORGET: 3,
+ 'forget': function (id) {
+ return [commands.FORGET, id, false];
+ },
+ // (4, id[, _])
+ LINK_TO: 4,
+ 'link_to': function (id) {
+ return [commands.LINK_TO, id, false];
+ },
+ // (5[, _[, _]])
+ DELETE_ALL: 5,
+ 'delete_all': function () {
+ return [5, false, false];
+ },
+ // (6, _, ids) replaces all linked records with provided ids
+ REPLACE_WITH: 6,
+ 'replace_with': function (ids) {
+ return [6, false, ids];
+ }
+};
+openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
+ multi_selection: false,
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldOne2Many";
+ this.is_started = $.Deferred();
+ this.form_last_update = $.Deferred();
+ this.disable_utility_classes = true;
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+
+ var self = this;
+
+ this.dataset = new openerp.web.form.One2ManyDataSet(this, this.field.relation);
+ this.dataset.o2m = this;
+ this.dataset.parent_view = this.view;
+ this.dataset.on_change.add_last(function() {
+ self.on_ui_change();
+ });
+
+ var modes = this.node.attrs.mode;
+ modes = !!modes ? modes.split(",") : ["tree", "form"];
+ var views = [];
+ _.each(modes, function(mode) {
+ var view = {
+ view_id: false,
+ view_type: mode == "tree" ? "list" : mode,
+ options: { sidebar : false }
+ };
+ if (self.field.views && self.field.views[mode]) {
+ view.embedded_view = self.field.views[mode];
+ }
+ if(view.view_type === "list") {
+ view.options.selectable = self.multi_selection;
+ }
+ views.push(view);
+ });
+ this.views = views;
+
+ this.viewmanager = new openerp.web.ViewManager(this, this.dataset, views);
+ this.viewmanager.registry = openerp.web.views.clone({
+ list: 'openerp.web.form.One2ManyListView',
+ form: 'openerp.web.form.One2ManyFormView'
+ });
+ var once = $.Deferred().then(function() {
+ self.form_last_update.resolve();
+ });
+ this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
+ if (view_type == "list") {
+ controller.o2m = self;
+ } else if (view_type == "form") {
+ controller.on_record_loaded.add_last(function() {
+ once.resolve();
+ });
+ controller.on_pager_action.add_first(function() {
+ self.save_form_view();
+ });
+ controller.$element.find(".oe_form_button_save_edit").hide();
+ }
+ self.is_started.resolve();
+ });
+ this.viewmanager.on_mode_switch.add_first(function() {
+ self.save_form_view();
+ });
+ setTimeout(function () {
+ self.viewmanager.appendTo(self.$element);
+ }, 0);
+ },
+ reload_current_view: function() {
+ var self = this;
+ var view = self.viewmanager.views[self.viewmanager.active_view].controller;
+ if(self.viewmanager.active_view === "list") {
+ view.reload_content();
+ } else if (self.viewmanager.active_view === "form") {
+ if (this.dataset.index === null && this.dataset.ids.length >= 1) {
+ this.dataset.index = 0;
+ }
+ this.form_last_update.then(function() {
+ this.form_last_update = view.do_show();
+ });
+ }
+ },
+ set_value: function(value) {
+ value = value || [];
+ var self = this;
+ this.dataset.reset_ids([]);
+ if(value.length >= 1 && value[0] instanceof Array) {
+ var ids = [];
+ _.each(value, function(command) {
+ var obj = {values: command[2]};
+ switch (command[0]) {
+ case commands.CREATE:
+ obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
+ self.dataset.to_create.push(obj);
+ self.dataset.cache.push(_.clone(obj));
+ ids.push(obj.id);
+ return;
+ case commands.UPDATE:
+ obj['id'] = command[1];
+ self.dataset.to_write.push(obj);
+ self.dataset.cache.push(_.clone(obj));
+ ids.push(obj.id);
+ return;
+ case commands.DELETE:
+ self.dataset.to_delete.push({id: command[1]});
+ return;
+ case commands.LINK_TO:
+ ids.push(command[1]);
+ return;
+ case commands.DELETE_ALL:
+ self.dataset.delete_all = true;
+ return;
+ }
+ });
+ this._super(ids);
+ this.dataset.set_ids(ids);
+ } else if (value.length >= 1 && typeof(value[0]) === "object") {
+ var ids = [];
+ this.dataset.delete_all = true;
+ _.each(value, function(command) {
+ var obj = {values: command};
+ obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
+ self.dataset.to_create.push(obj);
+ self.dataset.cache.push(_.clone(obj));
+ ids.push(obj.id);
+ });
+ this._super(ids);
+ this.dataset.set_ids(ids);
+ } else {
+ this._super(value);
+ this.dataset.reset_ids(value);
+ }
+ if (this.dataset.index === null && this.dataset.ids.length > 0) {
+ this.dataset.index = 0;
+ }
+ $.when(this.is_started).then(function() {
+ self.reload_current_view();
+ });
+ },
+ get_value: function() {
+ var self = this;
+ if (!this.dataset)
+ return [];
+ var val = this.dataset.delete_all ? [commands.delete_all()] : [];
+ val = val.concat(_.map(this.dataset.ids, function(id) {
+ var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
+ if (alter_order) {
+ return commands.create(alter_order.values);
+ }
+ alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
+ if (alter_order) {
+ return commands.update(alter_order.id, alter_order.values);
+ }
+ return commands.link_to(id);
+ }));
+ return val.concat(_.map(
+ this.dataset.to_delete, function(x) {
+ return commands['delete'](x.id);}));
+ },
+ save_form_view: function() {
+ if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
+ this.viewmanager.views[this.viewmanager.active_view] &&
+ this.viewmanager.views[this.viewmanager.active_view].controller) {
+ var view = this.viewmanager.views[this.viewmanager.active_view].controller;
+ if (this.viewmanager.active_view === "form") {
+ var res = $.when(view.do_save());
+ if (res === false) {
+ // ignore
+ } else if (res.isRejected()) {
+ throw "Save or create on one2many dataset is not supposed to fail.";
+ } else if (!res.isResolved()) {
+ throw "Asynchronous get_value() is not supported in form view.";
+ }
+ return res;
+ }
+ }
+ return false;
+ },
+ is_valid: function() {
+ this.validate();
+ return this._super();
+ },
+ validate: function() {
+ this.invalid = false;
+ var self = this;
+ var view = self.viewmanager.views[self.viewmanager.active_view].controller;
+ if (self.viewmanager.active_view === "form") {
+ for (var f in view.fields) {
+ f = view.fields[f];
+ if (!f.is_valid()) {
+ this.invalid = true;
+ return;
+ }
+ }
+ }
+ },
+ is_dirty: function() {
+ this.save_form_view();
+ return this._super();
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.toggleClass('disabled', this.readonly);
+ }
+});
+
+openerp.web.form.One2ManyDataSet = openerp.web.BufferedDataSet.extend({
+ get_context: function() {
+ this.context = this.o2m.build_context();
+ return this.context;
+ }
+});
+
+openerp.web.form.One2ManyFormView = openerp.web.FormView.extend({
+});
+
+openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
+ do_add_record: function () {
+ if (this.options.editable) {
+ this._super.apply(this, arguments);
+ } else {
+ var self = this;
+ var pop = new openerp.web.form.SelectCreatePopup(this);
+ pop.select_element(self.o2m.field.relation,{
+ initial_view: "form",
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
+ create_function: function(data) {
+ return self.o2m.dataset.create(data, function(r) {
+ self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result]));
+ self.o2m.dataset.on_change();
+ });
+ },
+ parent_view: self.o2m.view
+ }, self.o2m.build_domain(), self.o2m.build_context());
+ pop.on_select_elements.add_last(function() {
+ self.o2m.reload_current_view();
+ });
+ }
+ },
+ do_activate_record: function(index, id) {
+ var self = this;
+ var pop = new openerp.web.form.FormOpenPopup(self.o2m.view);
+ pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{
+ auto_write: false,
+ alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
+ parent_view: self.o2m.view,
+ read_function: function() {
+ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
+ }
+ });
+ pop.on_write.add(function(id, data) {
+ self.o2m.dataset.write(id, data, {}, function(r) {
+ self.o2m.reload_current_view();
+ });
+ });
+ }
+});
+
+openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({
+ multi_selection: false,
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldMany2Many";
+ this.list_id = _.uniqueId("many2many");
+ this.is_started = $.Deferred();
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+
+ var self = this;
+
+ this.dataset = new openerp.web.form.Many2ManyDataSet(this, this.field.relation);
+ this.dataset.m2m = this;
+ this.dataset.on_unlink.add_last(function(ids) {
+ self.on_ui_change();
+ });
+
+ this.list_view = new openerp.web.form.Many2ManyListView(this, this.list_id, this.dataset, false, {
+ 'addable': 'Add',
+ 'selectable': self.multi_selection
+ });
+ this.list_view.m2m_field = this;
+ this.list_view.on_loaded.add_last(function() {
+ self.is_started.resolve();
+ });
+ setTimeout(function () {
+ self.list_view.start();
+ }, 0);
+ },
+ set_value: function(value) {
+ value = value || [];
+ if (value.length >= 1 && value[0] instanceof Array) {
+ value = value[0][2];
+ }
+ this._super(value);
+ this.dataset.set_ids(value);
+ var self = this;
+ $.when(this.is_started).then(function() {
+ self.list_view.reload_content();
+ });
+ },
+ get_value: function() {
+ return [commands.replace_with(this.dataset.ids)];
+ },
+ validate: function() {
+ this.invalid = false;
+ // TODO niv
+ }
+});
+
+openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({
+ get_context: function() {
+ this.context = this.m2m.build_context();
+ return this.context;
+ }
+});
+
+/**
+ * @class
+ * @extends openerp.web.ListView
+ */
+openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends openerp.web.form.Many2ManyListView# */{
+ do_add_record: function () {
+ var pop = new openerp.web.form.SelectCreatePopup(this);
+ pop.select_element(this.model, {},
+ new openerp.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
+ this.m2m_field.build_context());
+ var self = this;
+ pop.on_select_elements.add(function(element_ids) {
+ _.each(element_ids, function(element_id) {
+ if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) {
+ self.dataset.set_ids([].concat(self.dataset.ids, [element_id]));
+ self.m2m_field.on_ui_change();
+ self.reload_content();
+ }
+ });
+ });
+ },
+ do_activate_record: function(index, id) {
+ var self = this;
+ var pop = new openerp.web.form.FormOpenPopup(this);
+ pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {});
+ pop.on_write_completed.add_last(function() {
+ self.reload_content();
+ });
+ }
+});
+
+/**
+ * @class
+ * @extends openerp.web.OldWidget
+ */
+openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.SelectCreatePopup# */{
+ identifier_prefix: "selectcreatepopup",
+ template: "SelectCreatePopup",
+ /**
+ * options:
+ * - initial_ids
+ * - initial_view: form or search (default search)
+ * - disable_multiple_selection
+ * - alternative_form_view
+ * - create_function (defaults to a naive saving behavior)
+ * - parent_view
+ */
+ select_element: function(model, options, domain, context) {
+ var self = this;
+ this.model = model;
+ this.domain = domain || [];
+ this.context = context || {};
+ this.options = _.defaults(options || {}, {"initial_view": "search", "create_function": function() {
+ return self.create_row.apply(self, arguments);
+ }});
+ this.initial_ids = this.options.initial_ids;
+ this.created_elements = [];
+ openerp.web.form.dialog(this.render(), {close:function() {
+ self.check_exit();
+ }});
+ this.start();
+ },
+ start: function() {
+ this._super();
+ this.dataset = new openerp.web.ReadOnlyDataSetSearch(this, this.model,
+ this.context);
+ this.dataset.parent_view = this.options.parent_view;
+ if (this.options.initial_view == "search") {
+ this.setup_search_view();
+ } else { // "form"
+ this.new_object();
+ }
+ },
+ setup_search_view: function() {
+ var self = this;
+ if (this.searchview) {
+ this.searchview.stop();
+ }
+ this.searchview = new openerp.web.SearchView(this,
+ this.element_id + "_search", this.dataset, false, {
+ "selectable": !this.options.disable_multiple_selection,
+ "deletable": false
+ });
+ this.searchview.on_search.add(function(domains, contexts, groupbys) {
+ if (self.initial_ids) {
+ self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]], self.domain]),
+ contexts, groupbys);
+ self.initial_ids = undefined;
+ } else {
+ self.view_list.do_search.call(self, domains.concat([self.domain]), contexts, groupbys);
+ }
+ });
+ this.searchview.on_loaded.add_last(function () {
+ var $buttons = self.searchview.$element.find(".oe_search-view-buttons");
+ $buttons.append(QWeb.render("SelectCreatePopup.search.buttons"));
+ var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close");
+ $cbutton.click(function() {
+ self.stop();
+ });
+ var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select");
+ if(self.options.disable_multiple_selection) {
+ $sbutton.hide();
+ }
+ $sbutton.click(function() {
+ self.on_select_elements(self.selected_ids);
+ self.stop();
+ });
+ self.view_list = new openerp.web.form.SelectCreateListView(self,
+ self.element_id + "_view_list", self.dataset, false,
+ {'deletable': false});
+ self.view_list.popup = self;
+ self.view_list.do_show();
+ self.view_list.start().then(function() {
+ self.searchview.do_search();
+ });
+ });
+ this.searchview.start();
+ },
+ create_row: function(data) {
+ var self = this;
+ var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
+ wdataset.parent_view = this.options.parent_view;
+ return wdataset.create(data);
+ },
+ on_select_elements: function(element_ids) {
+ },
+ on_click_element: function(ids) {
+ this.selected_ids = ids || [];
+ if(this.selected_ids.length > 0) {
+ this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
+ } else {
+ this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
+ }
+ },
+ new_object: function() {
+ var self = this;
+ if (this.searchview) {
+ this.searchview.hide();
+ }
+ if (this.view_list) {
+ this.view_list.$element.hide();
+ }
+ this.dataset.index = null;
+ this.view_form = new openerp.web.FormView(this, this.element_id + "_view_form", this.dataset, false);
+ if (this.options.alternative_form_view) {
+ this.view_form.set_embedded_view(this.options.alternative_form_view);
+ }
+ this.view_form.start();
+ this.view_form.on_loaded.add_last(function() {
+ var $buttons = self.view_form.$element.find(".oe_form_buttons");
+ $buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self}));
+ var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save-new");
+ $nbutton.click(function() {
+ self._created = $.Deferred().then(function() {
+ self._created = undefined;
+ self.view_form.on_button_new();
+ });
+ self.view_form.do_save();
+ });
+ var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save");
+ $nbutton.click(function() {
+ self._created = $.Deferred().then(function() {
+ self._created = undefined;
+ self.check_exit();
+ });
+ self.view_form.do_save();
+ });
+ var $cbutton = $buttons.find(".oe_selectcreatepopup-form-close");
+ $cbutton.click(function() {
+ self.check_exit();
+ });
+ });
+ this.dataset.on_create.add(function(data) {
+ self.options.create_function(data).then(function(r) {
+ self.created_elements.push(r.result);
+ if (self._created) {
+ self._created.resolve();
+ }
+ });
+ });
+ this.view_form.do_show();
+ },
+ check_exit: function() {
+ if (this.created_elements.length > 0) {
+ this.on_select_elements(this.created_elements);
+ }
+ this.stop();
+ }
+});
+
+openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({
+ do_add_record: function () {
+ this.popup.new_object();
+ },
+ select_record: function(index) {
+ this.popup.on_select_elements([this.dataset.ids[index]]);
+ this.popup.stop();
+ },
+ do_select: function(ids, records) {
+ this._super(ids, records);
+ this.popup.on_click_element(ids);
+ }
+});
+
+/**
+ * @class
+ * @extends openerp.web.OldWidget
+ */
+openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.FormOpenPopup# */{
+ identifier_prefix: "formopenpopup",
+ template: "FormOpenPopup",
+ /**
+ * options:
+ * - alternative_form_view
+ * - auto_write (default true)
+ * - read_function
+ * - parent_view
+ */
+ show_element: function(model, row_id, context, options) {
+ this.model = model;
+ this.row_id = row_id;
+ this.context = context || {};
+ this.options = _.defaults(options || {}, {"auto_write": true});
+ jQuery(this.render()).dialog({title: '',
+ modal: true,
+ width: 960,
+ height: 600});
+ this.start();
+ },
+ start: function() {
+ this._super();
+ this.dataset = new openerp.web.form.FormOpenDataset(this, this.model, this.context);
+ this.dataset.fop = this;
+ this.dataset.ids = [this.row_id];
+ this.dataset.index = 0;
+ this.dataset.parent_view = this.options.parent_view;
+ this.setup_form_view();
+ },
+ on_write: function(id, data) {
+ this.stop();
+ if (!this.options.auto_write)
+ return;
+ var self = this;
+ var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
+ wdataset.parent_view = this.options.parent_view;
+ wdataset.write(id, data, {}, function(r) {
+ self.on_write_completed();
+ });
+ },
+ on_write_completed: function() {},
+ setup_form_view: function() {
+ var self = this;
+ this.view_form = new openerp.web.FormView(this, this.element_id + "_view_form", this.dataset, false);
+ if (this.options.alternative_form_view) {
+ this.view_form.set_embedded_view(this.options.alternative_form_view);
+ }
+ this.view_form.start();
+ this.view_form.on_loaded.add_last(function() {
+ var $buttons = self.view_form.$element.find(".oe_form_buttons");
+ $buttons.html(QWeb.render("FormOpenPopup.form.buttons"));
+ var $nbutton = $buttons.find(".oe_formopenpopup-form-save");
+ $nbutton.click(function() {
+ self.view_form.do_save();
+ });
+ var $cbutton = $buttons.find(".oe_formopenpopup-form-close");
+ $cbutton.click(function() {
+ self.stop();
+ });
+ self.view_form.do_show();
+ });
+ this.dataset.on_write.add(this.on_write);
+ }
+});
+
+openerp.web.form.FormOpenDataset = openerp.web.ReadOnlyDataSetSearch.extend({
+ read_ids: function() {
+ if (this.fop.options.read_function) {
+ return this.fop.options.read_function.apply(null, arguments);
+ } else {
+ return this._super.apply(this, arguments);
+ }
+ }
+});
+
+openerp.web.form.FieldReference = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldReference";
+ this.fields_view = {
+ fields: {
+ selection: {
+ selection: view.fields_view.fields[this.name].selection
+ },
+ m2o: {
+ relation: null
+ }
+ }
+ };
+ this.get_fields_values = view.get_fields_values;
+ this.do_onchange = this.on_form_changed = this.on_nop;
+ this.widgets = {};
+ this.fields = {};
+ this.selection = new openerp.web.form.FieldSelection(this, { attrs: {
+ name: 'selection',
+ widget: 'selection'
+ }});
+ this.selection.on_value_changed.add_last(this.on_selection_changed);
+ this.m2o = new openerp.web.form.FieldMany2One(this, { attrs: {
+ name: 'm2o',
+ widget: 'many2one'
+ }});
+ },
+ on_nop: function() {
+ },
+ on_selection_changed: function() {
+ this.m2o.field.relation = this.selection.get_value();
+ this.m2o.set_value(null);
+ },
+ start: function() {
+ this._super();
+ this.selection.start();
+ this.m2o.start();
+ },
+ is_valid: function() {
+ return this.required === false || typeof(this.get_value()) === 'string';
+ },
+ is_dirty: function() {
+ return this.selection.is_dirty() || this.m2o.is_dirty();
+ },
+ set_value: function(value) {
+ this._super(value);
+ if (typeof(value) === 'string') {
+ var vals = value.split(',');
+ this.selection.set_value(vals[0]);
+ this.m2o.set_value(parseInt(vals[1], 10));
+ }
+ },
+ get_value: function() {
+ var model = this.selection.get_value(),
+ id = this.m2o.get_value();
+ if (typeof(model) === 'string' && typeof(id) === 'number') {
+ return model + ',' + id;
+ } else {
+ return false;
+ }
+ }
+});
+
+openerp.web.form.FieldBinary = openerp.web.form.Field.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.iframe = this.element_id + '_iframe';
+ this.binary_value = false;
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input.oe-binary-file').change(this.on_file_change);
+ this.$element.find('button.oe-binary-file-save').click(this.on_save_as);
+ this.$element.find('.oe-binary-file-clear').click(this.on_clear);
+ },
+ update_dom: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('.oe-binary').toggle(!this.readonly);
+ },
+ human_filesize : function(size) {
+ var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+ var i = 0;
+ while (size >= 1024) {
+ size /= 1024;
+ ++i;
+ }
+ return size.toFixed(2) + ' ' + units[i];
+ },
+ on_file_change: function(e) {
+ // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper
+ // http://www.html5rocks.com/tutorials/file/dndfiles/
+ // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler
+ window[this.iframe] = this.on_file_uploaded;
+ if ($(e.target).val() != '') {
+ this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id);
+ this.$element.find('form.oe-binary-form').submit();
+ this.toggle_progress();
+ }
+ },
+ toggle_progress: function() {
+ this.$element.find('.oe-binary-progress, .oe-binary').toggle();
+ },
+ on_file_uploaded: function(size, name, content_type, file_base64) {
+ delete(window[this.iframe]);
+ if (size === false) {
+ this.notification.warn("File Upload", "There was a problem while uploading your file");
+ // TODO: use openerp web crashmanager
+ console.warn("Error while uploading file : ", name);
+ } else {
+ this.on_file_uploaded_and_valid.apply(this, arguments);
+ this.on_ui_change();
+ }
+ this.toggle_progress();
+ },
+ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
+ },
+ on_save_as: function() {
+ if (!this.view.datarecord.id) {
+ this.notification.warn("Can't save file", "The record has not yet been saved");
+ } else {
+ var url = '/web/binary/saveas?session_id=' + this.session.session_id + '&model=' +
+ this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name +
+ '&fieldname=' + (this.node.attrs.filename || '') + '&t=' + (new Date().getTime());
+ window.open(url);
+ }
+ },
+ on_clear: function() {
+ if (this.value !== false) {
+ this.value = false;
+ this.binary_value = false;
+ this.on_ui_change();
+ }
+ return false;
+ }
+});
+
+openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldBinaryFile";
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ var show_value = (value != null && value !== false) ? value : '';
+ this.$element.find('input').eq(0).val(show_value);
+ },
+ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
+ this.value = file_base64;
+ this.binary_value = true;
+ var show_value = this.human_filesize(size);
+ this.$element.find('input').eq(0).val(show_value);
+ this.set_filename(name);
+ },
+ set_filename: function(value) {
+ var filename = this.node.attrs.filename;
+ if (this.view.fields[filename]) {
+ this.view.fields[filename].set_value(value);
+ this.view.fields[filename].on_ui_change();
+ }
+ },
+ on_clear: function() {
+ this._super.apply(this, arguments);
+ this.$element.find('input').eq(0).val('');
+ this.set_filename('');
+ }
+});
+
+openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
+ init: function(view, node) {
+ this._super(view, node);
+ this.template = "FieldBinaryImage";
+ },
+ start: function() {
+ this._super.apply(this, arguments);
+ this.$image = this.$element.find('img.oe-binary-image');
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ this.set_image_maxwidth();
+ var url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' +
+ this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime());
+ this.$image.attr('src', url);
+ },
+ set_image_maxwidth: function() {
+ this.$image.css('max-width', this.$element.width());
+ },
+ on_file_change: function() {
+ this.set_image_maxwidth();
+ this._super.apply(this, arguments);
+ },
+ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
+ this.value = file_base64;
+ this.binary_value = true;
+ this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64);
+ },
+ on_clear: function() {
+ this._super.apply(this, arguments);
+ this.$image.attr('src', '/web/static/src/img/placeholder.png');
+ }
+});
+
+openerp.web.form.FieldStatus = openerp.web.form.Field.extend({
+ template: "FieldStatus",
+ start: function() {
+ this._super();
+ this.selected_value = null;
+
+ this.render_list();
+ },
+ set_value: function(value) {
+ this._super(value);
+ this.selected_value = value;
+
+ this.render_list();
+ },
+ render_list: function() {
+ var self = this;
+ var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","),
+ function(x) { return x.trim(); });
+ shown = _.select(shown, function(x) { return x.length > 0; });
+
+ if (shown.length == 0) {
+ this.to_show = this.field.selection;
+ } else {
+ this.to_show = _.select(this.field.selection, function(x) {
+ return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value;
+ });
+ }
+
+ var content = openerp.web.qweb.render("FieldStatus.content", {widget: this, _:_});
+ this.$element.html(content);
+
+ var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}");
+ var color = colors[this.selected_value];
+ if (color) {
+ var elem = this.$element.find("li.oe-arrow-list-selected span");
+ elem.css("border-color", color);
+ elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-before");
+ elem.css("border-left-color", "rgba(0,0,0,0)");
+ elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-after");
+ elem.css("border-color", "rgba(0,0,0,0)");
+ elem.css("border-left-color", color);
+ }
+ }
+});
+
+/**
+ * Registry of form widgets, called by :js:`openerp.web.FormView`
+ */
+openerp.web.form.widgets = new openerp.web.Registry({
+ 'frame' : 'openerp.web.form.WidgetFrame',
+ 'group' : 'openerp.web.form.WidgetFrame',
+ 'notebook' : 'openerp.web.form.WidgetNotebook',
+ 'separator' : 'openerp.web.form.WidgetSeparator',
+ 'label' : 'openerp.web.form.WidgetLabel',
+ 'button' : 'openerp.web.form.WidgetButton',
+ 'char' : 'openerp.web.form.FieldChar',
+ 'email' : 'openerp.web.form.FieldEmail',
+ 'url' : 'openerp.web.form.FieldUrl',
+ 'text' : 'openerp.web.form.FieldText',
+ 'text_wiki' : 'openerp.web.form.FieldText',
+ 'date' : 'openerp.web.form.FieldDate',
+ 'datetime' : 'openerp.web.form.FieldDatetime',
+ 'selection' : 'openerp.web.form.FieldSelection',
+ 'many2one' : 'openerp.web.form.FieldMany2One',
+ 'many2many' : 'openerp.web.form.FieldMany2Many',
+ 'one2many' : 'openerp.web.form.FieldOne2Many',
+ 'one2many_list' : 'openerp.web.form.FieldOne2Many',
+ 'reference' : 'openerp.web.form.FieldReference',
+ 'boolean' : 'openerp.web.form.FieldBoolean',
+ 'float' : 'openerp.web.form.FieldFloat',
+ 'integer': 'openerp.web.form.FieldFloat',
+ 'float_time': 'openerp.web.form.FieldFloat',
+ 'progressbar': 'openerp.web.form.FieldProgressBar',
+ 'image': 'openerp.web.form.FieldBinaryImage',
+ 'binary': 'openerp.web.form.FieldBinaryFile',
+ 'statusbar': 'openerp.web.form.FieldStatus'
+});
+
+};
+
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
--- /dev/null
+openerp.web.list = function (openerp) {
+var QWeb = openerp.web.qweb;
+openerp.web.views.add('list', 'openerp.web.ListView');
+openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# */ {
+ defaults: {
+ // records can be selected one by one
+ 'selectable': true,
+ // list rows can be deleted
+ 'deletable': true,
+ // whether the column headers should be displayed
+ 'header': true,
+ // display addition button, with that label
+ 'addable': "New",
+ // whether the list view can be sorted, note that once a view has been
+ // sorted it can not be reordered anymore
+ 'sortable': true,
+ // whether the view rows can be reordered (via vertical drag & drop)
+ 'reorderable': true
+ },
+ /**
+ * Core class for list-type displays.
+ *
+ * As a view, needs a number of view-related parameters to be correctly
+ * instantiated, provides options and overridable methods for behavioral
+ * customization.
+ *
+ * See constructor parameters and method documentations for information on
+ * the default behaviors and possible options for the list view.
+ *
+ * @constructs openerp.web.ListView
+ * @extends openerp.web.View
+ *
+ * @param parent parent object
+ * @param element_id the id of the DOM elements this view should link itself to
+ * @param {openerp.web.DataSet} dataset the dataset the view should work with
+ * @param {String} view_id the listview's identifier, if any
+ * @param {Object} options A set of options used to configure the view
+ * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox)
+ * @param {Boolean} [options.header=true] should the list's header be displayed
+ * @param {Boolean} [options.deletable=true] are the list rows deletable
+ * @param {void|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button.
+ * @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers
+ * @param {Boolean} [options.reorderable=true] is it possible to reorder list rows
+ */
+ init: function(parent, element_id, dataset, view_id, options) {
+ var self = this;
+ this._super(parent, element_id);
+ this.set_default_options(_.extend({}, this.defaults, options || {}));
+ this.dataset = dataset;
+ this.model = dataset.model;
+ this.view_id = view_id;
+ this.previous_colspan = null;
+
+ this.columns = [];
+
+ this.records = new Collection();
+
+ this.set_groups(new openerp.web.ListView.Groups(this));
+
+ if (this.dataset instanceof openerp.web.DataSetStatic) {
+ this.groups.datagroup = new openerp.web.StaticDataGroup(this.dataset);
+ } else {
+ this.groups.datagroup = new openerp.web.DataGroup(
+ this, this.model,
+ dataset.get_domain(),
+ dataset.get_context(),
+ {});
+ this.groups.datagroup.sort = this.dataset._sort;
+ }
+
+ this.page = 0;
+ this.records.bind('change', function (event, record, key) {
+ if (!_(self.aggregate_columns).chain()
+ .pluck('name').contains(key).value()) {
+ return;
+ }
+ self.compute_aggregates();
+ });
+ },
+ /**
+ * Retrieves the view's number of records per page (|| section)
+ *
+ * options > defaults > parent.action.limit > indefinite
+ *
+ * @returns {Number|null}
+ */
+ limit: function () {
+ if (this._limit === undefined) {
+ this._limit = (this.options.limit
+ || this.defaults.limit
+ || (this.widget_parent.action || {}).limit
+ || null);
+ }
+ return this._limit;
+ },
+ /**
+ * Set a custom Group construct as the root of the List View.
+ *
+ * @param {openerp.web.ListView.Groups} groups
+ */
+ set_groups: function (groups) {
+ var self = this;
+ if (this.groups) {
+ $(this.groups).unbind("selected deleted action row_link");
+ delete this.groups;
+ }
+
+ this.groups = groups;
+ $(this.groups).bind({
+ 'selected': function (e, ids, records) {
+ self.do_select(ids, records);
+ },
+ 'deleted': function (e, ids) {
+ self.do_delete(ids);
+ },
+ 'action': function (e, action_name, id, callback) {
+ self.do_button_action(action_name, id, callback);
+ },
+ 'row_link': function (e, id, dataset) {
+ self.do_activate_record(dataset.index, id, dataset);
+ }
+ });
+ },
+ /**
+ * View startup method, the default behavior is to set the ``oe-listview``
+ * class on its root element and to perform an RPC load call.
+ *
+ * @returns {$.Deferred} loading promise
+ */
+ start: function() {
+ this.$element.addClass('oe-listview');
+ return this.reload_view(null, null, true);
+ },
+ /**
+ * Called after loading the list view's description, sets up such things
+ * as the view table's columns, renders the table itself and hooks up the
+ * various table-level and row-level DOM events (action buttons, deletion
+ * buttons, selection of records, [New] button, selection of a given
+ * record, ...)
+ *
+ * Sets up the following:
+ *
+ * * Processes arch and fields to generate a complete field descriptor for each field
+ * * Create the table itself and allocate visible columns
+ * * Hook in the top-level (header) [New|Add] and [Delete] button
+ * * Sets up showing/hiding the top-level [Delete] button based on records being selected or not
+ * * Sets up event handlers for action buttons and per-row deletion button
+ * * Hooks global callback for clicking on a row
+ * * Sets up its sidebar, if any
+ *
+ * @param {Object} data wrapped fields_view_get result
+ * @param {Object} data.fields_view fields_view_get result (processed)
+ * @param {Object} data.fields_view.fields mapping of fields for the current model
+ * @param {Object} data.fields_view.arch current list view descriptor
+ * @param {Boolean} grouped Is the list view grouped
+ */
+ on_loaded: function(data, grouped) {
+ var self = this;
+ this.fields_view = data;
+ this.name = "" + this.fields_view.arch.attrs.string;
+
+ this.setup_columns(this.fields_view.fields, grouped);
+
+ this.$element.html(QWeb.render("ListView", this));
+
+ // Head hook
+ this.$element.find('.oe-list-add')
+ .click(this.do_add_record)
+ .attr('disabled', grouped && this.options.editable);
+ this.$element.find('.oe-list-delete')
+ .attr('disabled', true)
+ .click(this.do_delete_selected);
+ this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) {
+ e.stopPropagation();
+
+ var $this = $(this);
+ self.dataset.sort($this.data('id'));
+ if ($this.find('span').length) {
+ $this.find('span').toggleClass(
+ 'ui-icon-triangle-1-s ui-icon-triangle-1-n');
+ } else {
+ $this.append('<span class="ui-icon ui-icon-triangle-1-s">')
+ .siblings('.oe-sortable').find('span').remove();
+ }
+
+ self.reload_content();
+ });
+
+ this.$element.find('.oe-list-pager')
+ .delegate('button', 'click', function () {
+ var $this = $(this);
+ switch ($this.data('pager-action')) {
+ case 'first':
+ self.page = 0; break;
+ case 'last':
+ self.page = Math.floor(
+ self.dataset.ids.length / self.limit());
+ break;
+ case 'next':
+ self.page += 1; break;
+ case 'previous':
+ self.page -= 1; break;
+ }
+ self.reload_content();
+ }).find('.oe-pager-state')
+ .click(function (e) {
+ e.stopPropagation();
+ var $this = $(this);
+
+ var $select = $('<select>')
+ .appendTo($this.empty())
+ .click(function (e) {e.stopPropagation();})
+ .append('<option value="80">80</option>' +
+ '<option value="100">100</option>' +
+ '<option value="200">200</option>' +
+ '<option value="500">500</option>' +
+ '<option value="NaN">Unlimited</option>')
+ .change(function () {
+ var val = parseInt($select.val(), 10);
+ self._limit = (isNaN(val) ? null : val);
+ self.page = 0;
+ self.reload_content();
+ })
+ .val(self._limit || 'NaN');
+ });
+ if (!this.sidebar && this.options.sidebar && this.options.sidebar_id) {
+ this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
+ this.sidebar.start();
+ this.sidebar.add_toolbar(this.fields_view.toolbar);
+ this.set_common_sidebar_sections(this.sidebar);
+ }
+ },
+ /**
+ * Configures the ListView pager based on the provided dataset's information
+ *
+ * Horrifying side-effect: sets the dataset's data on this.dataset?
+ *
+ * @param {openerp.web.DataSet} dataset
+ */
+ configure_pager: function (dataset) {
+ this.dataset.ids = dataset.ids;
+
+ var limit = this.limit(),
+ total = dataset.ids.length,
+ first = (this.page * limit),
+ last;
+ if (!limit || (total - first) < limit) {
+ last = total;
+ } else {
+ last = first + limit;
+ }
+ this.$element.find('span.oe-pager-state').empty().text(_.sprintf(
+ "[%d to %d] of %d", first + 1, last, total));
+
+ this.$element
+ .find('button[data-pager-action=first], button[data-pager-action=previous]')
+ .attr('disabled', this.page === 0)
+ .end()
+ .find('button[data-pager-action=last], button[data-pager-action=next]')
+ .attr('disabled', last === total);
+ },
+ /**
+ * Sets up the listview's columns: merges view and fields data, move
+ * grouped-by columns to the front of the columns list and make them all
+ * visible.
+ *
+ * @param {Object} fields fields_view_get's fields section
+ * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
+ */
+ setup_columns: function (fields, grouped) {
+ var domain_computer = openerp.web.form.compute_domain;
+
+ var noop = function () { return {}; };
+ var field_to_column = function (field) {
+ var name = field.attrs.name;
+ var column = _.extend({id: name, tag: field.tag},
+ field.attrs, fields[name]);
+ // modifiers computer
+ if (column.modifiers) {
+ var modifiers = JSON.parse(column.modifiers);
+ column.modifiers_for = function (fields) {
+ if (!modifiers.invisible) {
+ return {};
+ }
+ return {
+ 'invisible': domain_computer(modifiers.invisible, fields)
+ };
+ };
+ if (modifiers['tree_invisible']) {
+ column.invisible = '1';
+ }
+ } else {
+ column.modifiers_for = noop;
+ }
+ return column;
+ };
+
+ this.columns.splice(0, this.columns.length);
+ this.columns.push.apply(
+ this.columns,
+ _(this.fields_view.arch.children).map(field_to_column));
+ if (grouped) {
+ this.columns.unshift({
+ id: '_group', tag: '', string: "Group", meta: true,
+ modifiers_for: function () { return {}; }
+ }, {
+ id: '_count', tag: '', string: '#', meta: true,
+ modifiers_for: function () { return {}; }
+ });
+ }
+
+ this.visible_columns = _.filter(this.columns, function (column) {
+ return column.invisible !== '1';
+ });
+
+ this.aggregate_columns = _(this.visible_columns)
+ .map(function (column) {
+ if (column.type !== 'integer' && column.type !== 'float') {
+ return {};
+ }
+ var aggregation_func = column['group_operator'] || 'sum';
+
+ return _.extend({}, column, {
+ 'function': aggregation_func,
+ label: column[aggregation_func]
+ });
+ });
+ },
+ /**
+ * Used to handle a click on a table row, if no other handler caught the
+ * event.
+ *
+ * The default implementation asks the list view's view manager to switch
+ * to a different view (by calling
+ * :js:func:`~openerp.web.ViewManager.on_mode_switch`), using the
+ * provided record index (within the current list view's dataset).
+ *
+ * If the index is null, ``switch_to_record`` asks for the creation of a
+ * new record.
+ *
+ * @param {Number|void} index the record index (in the current dataset) to switch to
+ * @param {String} [view="form"] the view type to switch to
+ */
+ select_record:function (index, view) {
+ view = view || 'form';
+ this.dataset.index = index;
+ _.delay(_.bind(function () {
+ this.do_switch_view(view);
+ }, this));
+ },
+ do_show: function () {
+ this.$element.show();
+ if (this.sidebar) {
+ this.sidebar.$element.show();
+ }
+ if (!_(this.dataset.ids).isEmpty()) {
+ this.reload_content();
+ }
+ },
+ do_hide: function () {
+ this.$element.hide();
+ if (this.sidebar) {
+ this.sidebar.$element.hide();
+ }
+ },
+ /**
+ * Reloads the list view based on the current settings (dataset & al)
+ *
+ * @param {Boolean} [grouped] Should the list be displayed grouped
+ * @param {Object} [context] context to send the server while loading the view
+ */
+ reload_view: function (grouped, context, initial) {
+ var self = this;
+ var callback = function (field_view_get) {
+ self.on_loaded(field_view_get, grouped);
+ };
+ if (this.embedded_view) {
+ return $.Deferred().then(callback).resolve(this.embedded_view);
+ } else {
+ return this.rpc('/web/listview/load', {
+ model: this.model,
+ view_id: this.view_id,
+ view_type: "tree",
+ context: this.dataset.get_context(context),
+ toolbar: this.options.sidebar
+ }, callback);
+ }
+ },
+ /**
+ * re-renders the content of the list view
+ */
+ reload_content: function () {
+ var self = this;
+ this.records.reset();
+ this.$element.find('.oe-listview-content').append(
+ this.groups.render(function () {
+ if (self.dataset.index == null) {
+ var has_one = false;
+ self.records.each(function () { has_one = true; });
+ if (has_one) {
+ self.dataset.index = 0;
+ }
+ }
+ self.compute_aggregates();
+ }));
+ },
+ /**
+ * Event handler for a search, asks for the computation/folding of domains
+ * and contexts (and group-by), then reloads the view's content.
+ *
+ * @param {Array} domains a sequence of literal and non-literal domains
+ * @param {Array} contexts a sequence of literal and non-literal contexts
+ * @param {Array} groupbys a sequence of literal and non-literal group-by contexts
+ * @returns {$.Deferred} fold request evaluation promise
+ */
+ do_search: function (domains, contexts, groupbys) {
+ return this.rpc('/web/session/eval_domain_and_context', {
+ domains: [this.dataset.get_domain()].concat(domains),
+ contexts: [this.dataset.get_context()].concat(contexts),
+ group_by_seq: groupbys
+ }, $.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.groups.datagroup = new openerp.web.DataGroup(
+ this, this.model,
+ results.domain,
+ results.context,
+ results.group_by);
+ this.groups.datagroup.sort = this.dataset._sort;
+
+ if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) {
+ results.group_by = null;
+ }
+
+ this.reload_view(!!results.group_by, results.context).then(
+ $.proxy(this, 'reload_content'));
+ },
+ /**
+ * Handles the signal to delete lines from the records list
+ *
+ * @param {Array} ids the ids of the records to delete
+ */
+ do_delete: function (ids) {
+ if (!ids.length) {
+ return;
+ }
+ var self = this;
+ return $.when(this.dataset.unlink(ids)).then(function () {
+ _(ids).each(function (id) {
+ self.records.remove(self.records.get(id));
+ });
+ self.compute_aggregates();
+ });
+ },
+ /**
+ * Handles the signal indicating that a new record has been selected
+ *
+ * @param {Array} ids selected record ids
+ * @param {Array} records selected record values
+ */
+ do_select: function (ids, records) {
+ this.$element.find('.oe-list-delete')
+ .attr('disabled', !ids.length);
+
+ if (!records.length) {
+ this.compute_aggregates();
+ return;
+ }
+ this.compute_aggregates(_(records).map(function (record) {
+ return {count: 1, values: record};
+ }));
+ },
+ /**
+ * Handles action button signals on a record
+ *
+ * @param {String} name action name
+ * @param {Object} id id of the record the action should be called on
+ * @param {Function} callback should be called after the action is executed, if non-null
+ */
+ do_button_action: function (name, id, callback) {
+ var action = _.detect(this.columns, function (field) {
+ return field.name === name;
+ });
+ if (!action) { return; }
+ this.do_execute_action(action, this.dataset, id, callback);
+ },
+ /**
+ * Handles the activation of a record (clicking on it)
+ *
+ * @param {Number} index index of the record in the dataset
+ * @param {Object} id identifier of the activated record
+ * @param {openerp.web.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;
+ // TODO is it needed ?
+ this.dataset.read_slice([],{
+ context: dataset.get_context(),
+ domain: dataset.get_domain()
+ }, function () {
+ self.select_record(index);
+ });
+ },
+ /**
+ * Handles signal for the addition of a new record (can be a creation,
+ * can be the addition from a remote source, ...)
+ *
+ * The default implementation is to switch to a new record on the form view
+ */
+ do_add_record: function () {
+ this.select_record(null);
+ },
+ /**
+ * Handles deletion of all selected lines
+ */
+ do_delete_selected: function () {
+ this.do_delete(this.groups.get_selection().ids);
+ },
+ /**
+ * Computes the aggregates for the current list view, either on the
+ * records provided or on the records of the internal
+ * :js:class:`~openerp.web.ListView.Group`, by calling
+ * :js:func:`~openerp.web.ListView.group.get_records`.
+ *
+ * Then displays the aggregates in the table through
+ * :js:method:`~openerp.web.ListView.display_aggregates`.
+ *
+ * @param {Array} [records]
+ */
+ compute_aggregates: function (records) {
+ var columns = _(this.aggregate_columns).filter(function (column) {
+ return column['function']; });
+ if (_.isEmpty(columns)) { return; }
+
+ if (_.isEmpty(records)) {
+ records = this.groups.get_records();
+ }
+
+ var count = 0, sums = {};
+ _(columns).each(function (column) {
+ switch (column['function']) {
+ case 'max':
+ sums[column.id] = -Infinity;
+ break;
+ case 'min':
+ sums[column.id] = Infinity;
+ break;
+ default:
+ sums[column.id] = 0;
+ }
+ });
+ _(records).each(function (record) {
+ count += record.count || 1;
+ _(columns).each(function (column) {
+ var field = column.id,
+ value = record.values[field];
+ switch (column['function']) {
+ case 'sum':
+ sums[field] += value;
+ break;
+ case 'avg':
+ sums[field] += record.count * value;
+ break;
+ case 'min':
+ if (sums[field] > value) {
+ sums[field] = value;
+ }
+ break;
+ case 'max':
+ if (sums[field] < value) {
+ sums[field] = value;
+ }
+ break;
+ }
+ });
+ });
+
+ var aggregates = {};
+ _(columns).each(function (column) {
+ var field = column.id;
+ switch (column['function']) {
+ case 'avg':
+ aggregates[field] = {value: sums[field] / count};
+ break;
+ default:
+ aggregates[field] = {value: sums[field]};
+ }
+ });
+
+ this.display_aggregates(aggregates);
+ },
+ display_aggregates: function (aggregation) {
+ var $footer_cells = this.$element.find('.oe-list-footer');
+ _(this.aggregate_columns).each(function (column) {
+ if (!column['function']) {
+ return;
+ }
+
+ $footer_cells.filter(_.sprintf('[data-field=%s]', column.id))
+ .html(openerp.web.format_cell(aggregation, column));
+ });
+ },
+ get_selected_ids: function() {
+ var ids = this.groups.get_selection().ids;
+ return ids;
+ },
+ /**
+ * Adds padding columns at the start or end of all table rows (including
+ * field names row)
+ *
+ * @param {Number} count number of columns to add
+ * @param {Object} options
+ * @param {"before"|"after"} [position="after"] insertion position for the new columns
+ * @param {Object} [except] content row to not pad
+ */
+ pad_columns: function (count, options) {
+ options = options || {};
+ // padding for action/pager header
+ var $first_header = this.$element.find('thead tr:first th');
+ var colspan = $first_header.attr('colspan');
+ if (colspan) {
+ if (!this.previous_colspan) {
+ this.previous_colspan = colspan;
+ }
+ $first_header.attr('colspan', parseInt(colspan, 10) + count);
+ }
+ // Padding for column titles, footer and data rows
+ var $rows = this.$element
+ .find('.oe-listview-header-columns, tr:not(thead tr)')
+ .not(options['except']);
+ var newcols = new Array(count+1).join('<td class="oe-listview-padding"></td>');
+ if (options.position === 'before') {
+ $rows.prepend(newcols);
+ } else {
+ $rows.append(newcols);
+ }
+ },
+ /**
+ * Removes all padding columns of the table
+ */
+ unpad_columns: function () {
+ this.$element.find('.oe-listview-padding').remove();
+ if (this.previous_colspan) {
+ this.$element
+ .find('thead tr:first th')
+ .attr('colspan', this.previous_colspan);
+ this.previous_colspan = null;
+ }
+ }
+});
+openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.ListView.List# */{
+ /**
+ * List display for the ListView, handles basic DOM events and transforms
+ * them in the relevant higher-level events, to which the list view (or
+ * other consumers) can subscribe.
+ *
+ * Events on this object are registered via jQuery.
+ *
+ * Available events:
+ *
+ * `selected`
+ * Triggered when a row is selected (using check boxes), provides an
+ * array of ids of all the selected records.
+ * `deleted`
+ * Triggered when deletion buttons are hit, provide an array of ids of
+ * all the records being marked for suppression.
+ * `action`
+ * Triggered when an action button is clicked, provides two parameters:
+ *
+ * * The name of the action to execute (as a string)
+ * * The id of the record to execute the action on
+ * `row_link`
+ * Triggered when a row of the table is clicked, provides the index (in
+ * the rows array) and id of the selected record to the handle function.
+ *
+ * @constructs openerp.web.ListView.List
+ * @extends openerp.web.Class
+ *
+ * @param {Object} opts display options, identical to those of :js:class:`openerp.web.ListView`
+ */
+ init: function (group, opts) {
+ var self = this;
+ this.group = group;
+ this.view = group.view;
+ this.session = this.view.session;
+
+ this.options = opts.options;
+ this.columns = opts.columns;
+ this.dataset = opts.dataset;
+ this.records = opts.records;
+
+ this.record_callbacks = {
+ 'remove': function (event, record) {
+ var $row = self.$current.find(
+ '[data-id=' + record.get('id') + ']');
+ var index = $row.data('index');
+ $row.remove();
+ self.refresh_zebra(index);
+ },
+ 'reset': $.proxy(this, 'on_records_reset'),
+ 'change': function (event, record) {
+ var $row = self.$current.find('[data-id=' + record.get('id') + ']');
+ $row.replaceWith(self.render_record(record));
+ },
+ 'add': function (ev, records, record, index) {
+ var $new_row = $('<tr>').attr({
+ 'data-id': record.get('id')
+ });
+
+ if (index === 0) {
+ $new_row.prependTo(self.$current);
+ } else {
+ var previous_record = records.at(index-1),
+ $previous_sibling = self.$current.find(
+ '[data-id=' + previous_record.get('id') + ']');
+ $new_row.insertAfter($previous_sibling);
+ }
+
+ self.refresh_zebra(index, 1);
+ }
+ };
+ _(this.record_callbacks).each(function (callback, event) {
+ this.records.bind(event, callback);
+ }, this);
+
+ this.$_element = $('<tbody class="ui-widget-content">')
+ .appendTo(document.body)
+ .delegate('th.oe-record-selector', 'click', function (e) {
+ e.stopPropagation();
+ var selection = self.get_selection();
+ $(self).trigger(
+ 'selected', [selection.ids, selection.records]);
+ })
+ .delegate('td.oe-record-delete button', 'click', function (e) {
+ e.stopPropagation();
+ var $row = $(e.target).closest('tr');
+ $(self).trigger('deleted', [[self.row_id($row)]]);
+ })
+ .delegate('td.oe-field-cell button', 'click', function (e) {
+ e.stopPropagation();
+ var $target = $(e.currentTarget),
+ field = $target.closest('td').data('field'),
+ $row = $target.closest('tr'),
+ record_id = self.row_id($row);
+
+ $(self).trigger('action', [field, record_id, function () {
+ return self.reload_record(self.records.get(record_id));
+ }]);
+ })
+ .delegate('tr', 'click', function (e) {
+ e.stopPropagation();
+ self.dataset.index = self.records.indexOf(
+ self.records.get(
+ self.row_id(e.currentTarget)));
+ self.row_clicked(e);
+ });
+ },
+ row_clicked: function () {
+ $(this).trigger(
+ 'row_link',
+ [this.records.at(this.dataset.index).get('id'),
+ this.dataset]);
+ },
+ render: function () {
+ if (this.$current) {
+ this.$current.remove();
+ }
+ this.$current = this.$_element.clone(true);
+ this.$current.empty().append(
+ QWeb.render('ListView.rows', _.extend({
+ render_cell: openerp.web.format_cell}, this)));
+ this.pad_table_to(5);
+ },
+ pad_table_to: function (count) {
+ if (this.records.length >= count ||
+ _(this.columns).any(function(column) { return column.meta; })) {
+ return;
+ }
+ var cells = [];
+ if (this.options.selectable) {
+ cells.push('<td title="selection"></td>');
+ }
+ _(this.columns).each(function(column) {
+ if (column.invisible !== '1') {
+ cells.push('<td title="' + column.string + '"> </td>');
+ }
+ });
+ if (this.options.deletable) {
+ cells.push('<td><button type="button" style="visibility: hidden"> </button></td>');
+ }
+ cells.unshift('<tr>');
+ cells.push('</tr>');
+
+ var row = cells.join('');
+ this.$current.append(new Array(count - this.records.length + 1).join(row));
+ this.refresh_zebra(this.records.length);
+ },
+ /**
+ * 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.
+ */
+ get_selection: function () {
+ if (!this.options.selectable) {
+ return [];
+ }
+ var records = this.records;
+ var result = {ids: [], records: []};
+ this.$current.find('th.oe-record-selector input:checked')
+ .closest('tr').each(function () {
+ var record = records.get($(this).data('id'));
+ result.ids.push(record.get('id'));
+ result.records.push(record.attributes);
+ });
+ return result;
+ },
+ /**
+ * Returns the identifier of the object displayed in the provided table
+ * row
+ *
+ * @param {Object} row the selected table row
+ * @returns {Number|String} the identifier of the row's object
+ */
+ row_id: function (row) {
+ return $(row).data('id');
+ },
+ /**
+ * Death signal, cleans up list display
+ */
+ on_records_reset: function () {
+ _(this.record_callbacks).each(function (callback, event) {
+ this.records.unbind(event, callback);
+ }, this);
+ if (!this.$current) { return; }
+ this.$current.remove();
+ this.$current = null;
+ this.$_element.remove();
+ },
+ get_records: function () {
+ return this.records.map(function (record) {
+ return {count: 1, values: record.attributes};
+ });
+ },
+ /**
+ * Reloads the provided record by re-reading its content from the server.
+ *
+ * @param {Record} record
+ * @returns {$.Deferred} promise to the finalization of the reloading
+ */
+ reload_record: function (record) {
+ return this.dataset.read_ids(
+ [record.get('id')],
+ _.pluck(_(this.columns).filter(function (r) {
+ return r.tag === 'field';
+ }), 'name'),
+ function (records) {
+ _(records[0]).each(function (value, key) {
+ record.set(key, value, {silent: true});
+ });
+ record.trigger('change', record);
+ }
+ );
+ },
+ /**
+ * Renders a list record to HTML
+ *
+ * @param {Record} record index of the record to render in ``this.rows``
+ * @returns {String} QWeb rendering of the selected record
+ */
+ render_record: function (record) {
+ var index = this.records.indexOf(record);
+ return QWeb.render('ListView.row', {
+ columns: this.columns,
+ options: this.options,
+ record: record,
+ row_parity: (index % 2 === 0) ? 'even' : 'odd',
+ render_cell: openerp.web.format_cell
+ });
+ },
+ /**
+ * Fixes fixes the even/odd classes
+ *
+ * @param {Number} [from_index] index from which to resequence
+ * @param {Number} [offset = 0] selection offset for DOM, in case there are rows to ignore in the table
+ */
+ refresh_zebra: function (from_index, offset) {
+ offset = offset || 0;
+ from_index = from_index || 0;
+ var dom_offset = offset + from_index;
+ var sel = dom_offset ? ':gt(' + (dom_offset - 1) + ')' : null;
+ this.$current.children(sel).each(function (i, e) {
+ var index = from_index + i;
+ // reset record-index accelerators on rows and even/odd
+ var even = index%2 === 0;
+ $(e).toggleClass('even', even)
+ .toggleClass('odd', !even);
+ });
+ }
+});
+openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.ListView.Groups# */{
+ passtrough_events: 'action deleted row_link',
+ /**
+ * Grouped display for the ListView. Handles basic DOM events and interacts
+ * with the :js:class:`~openerp.web.DataGroup` bound to it.
+ *
+ * Provides events similar to those of
+ * :js:class:`~openerp.web.ListView.List`
+ *
+ * @constructs openerp.web.ListView.Groups
+ * @extends openerp.web.Class
+ *
+ * @param {openerp.web.ListView} view
+ * @param {Object} [options]
+ * @param {Collection} [options.records]
+ * @param {Object} [options.options]
+ * @param {Array} [options.columns]
+ */
+ init: function (view, options) {
+ options = options || {};
+ this.view = view;
+ this.records = options.records || view.records;
+ this.options = options.options || view.options;
+ this.columns = options.columns || view.columns;
+ this.datagroup = null;
+
+ this.$row = null;
+ this.children = {};
+
+ this.page = 0;
+
+ this.records.bind('reset', $.proxy(this, 'on_records_reset'));
+ },
+ make_fragment: function () {
+ return document.createDocumentFragment();
+ },
+ /**
+ * Returns a DOM node after which a new tbody can be inserted, so that it
+ * follows the provided row.
+ *
+ * Necessary to insert the result of a new group or list view within an
+ * existing groups render, without losing track of the groups's own
+ * elements
+ *
+ * @param {HTMLTableRowElement} row the row after which the caller wants to insert a body
+ * @returns {HTMLTableSectionElement} element after which a tbody can be inserted
+ */
+ point_insertion: function (row) {
+ var $row = $(row);
+ var red_letter_tboday = $row.closest('tbody')[0];
+
+ var $next_siblings = $row.nextAll();
+ if ($next_siblings.length) {
+ var $root_kanal = $('<tbody>').insertAfter(red_letter_tboday);
+
+ $root_kanal.append($next_siblings);
+ this.elements.splice(
+ _.indexOf(this.elements, red_letter_tboday),
+ 0,
+ $root_kanal[0]);
+ }
+ return red_letter_tboday;
+ },
+ make_paginator: function () {
+ var self = this;
+ var $prev = $('<button type="button" data-pager-action="previous"><</button>')
+ .click(function (e) {
+ e.stopPropagation();
+ self.page -= 1;
+
+ self.$row.closest('tbody').next()
+ .replaceWith(self.render());
+ });
+ var $next = $('<button type="button" data-pager-action="next">></button>')
+ .click(function (e) {
+ e.stopPropagation();
+ self.page += 1;
+
+ self.$row.closest('tbody').next()
+ .replaceWith(self.render());
+ });
+ this.$row.children().last()
+ .append($prev)
+ .append('<span class="oe-pager-state"></span>')
+ .append($next);
+ },
+ open: function (point_insertion) {
+ this.render().insertAfter(point_insertion);
+ this.make_paginator();
+ },
+ close: function () {
+ this.$row.children().last().empty();
+ this.records.reset();
+ },
+ /**
+ * Prefixes ``$node`` with floated spaces in order to indent it relative
+ * to its own left margin/baseline
+ *
+ * @param {jQuery} $node jQuery object to indent
+ * @param {Number} level current nesting level, >= 1
+ * @returns {jQuery} the indentation node created
+ */
+ indent: function ($node, level) {
+ return $('<span>')
+ .css({'float': 'left', 'white-space': 'pre'})
+ .text(new Array(level).join(' '))
+ .prependTo($node);
+ },
+ render_groups: function (datagroups) {
+ var self = this;
+ var placeholder = this.make_fragment();
+ _(datagroups).each(function (group) {
+ if (self.children[group.value]) {
+ self.records.proxy(group.value).reset();
+ delete self.children[group.value];
+ }
+ var child = self.children[group.value] = new openerp.web.ListView.Groups(self.view, {
+ records: self.records.proxy(group.value),
+ options: self.options,
+ columns: self.columns
+ });
+ self.bind_child_events(child);
+ child.datagroup = group;
+
+ var $row = child.$row = $('<tr>');
+ if (group.openable) {
+ $row.click(function (e) {
+ if (!$row.data('open')) {
+ $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');
+ child.close();
+ }
+ });
+ }
+ placeholder.appendChild($row[0]);
+
+ var $group_column = $('<th class="oe-group-name">').appendTo($row);
+ // Don't fill this if group_by_no_leaf but no group_by
+ if (group.grouped_on) {
+ var row_data = {};
+ row_data[group.grouped_on] = group;
+ var group_column = _(self.columns).detect(function (column) {
+ return column.id === group.grouped_on; });
+ $group_column.html(openerp.web.format_cell(
+ row_data, group_column, "Undefined"
+ ));
+ if (group.openable) {
+ // Make openable if not terminal group & group_by_no_leaf
+ $group_column
+ .prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">');
+ }
+ }
+ self.indent($group_column, group.level);
+ // count column
+ $('<td>').text(group.length).appendTo($row);
+
+ if (self.options.selectable) {
+ $row.append('<td>');
+ }
+ _(self.columns).chain()
+ .filter(function (column) {return !column.invisible;})
+ .each(function (column) {
+ if (column.meta) {
+ // do not do anything
+ } else if (column.id in group.aggregates) {
+ var value = group.aggregates[column.id];
+ var format;
+ if (column.type === 'integer') {
+ format = "%.0f";
+ } else if (column.type === 'float') {
+ format = "%.2f";
+ }
+ $('<td>')
+ .text(_.sprintf(format, value))
+ .appendTo($row);
+ } else {
+ $row.append('<td>');
+ }
+ });
+ if (self.options.deletable) {
+ $row.append('<td class="oe-group-pagination">');
+ }
+ });
+ return placeholder;
+ },
+ bind_child_events: function (child) {
+ var $this = $(this),
+ self = this;
+ $(child).bind('selected', function (e) {
+ // can have selections spanning multiple links
+ var selection = self.get_selection();
+ $this.trigger(e, [selection.ids, selection.records]);
+ }).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.
+ // Convert our *args back into an Array in order to trigger them
+ // on the group itself, so it can ultimately be forwarded wherever
+ // it's supposed to go.
+ var args = Array.prototype.slice.call(arguments, 1);
+ $this.trigger.call($this, e, args);
+ });
+ },
+ render_dataset: function (dataset) {
+ var self = this,
+ list = new openerp.web.ListView.List(this, {
+ options: this.options,
+ columns: this.columns,
+ dataset: dataset,
+ records: this.records
+ });
+ this.bind_child_events(list);
+
+ var view = this.view,
+ limit = view.limit(),
+ d = new $.Deferred(),
+ page = this.datagroup.openable ? this.page : view.page;
+
+ var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field"}), 'name');
+ var options = { offset: page * limit, limit: limit };
+ dataset.read_slice(fields, options , function (records) {
+ if (!self.datagroup.openable) {
+ view.configure_pager(dataset);
+ } else {
+ var pages = Math.ceil(dataset.ids.length / limit);
+ self.$row
+ .find('.oe-pager-state')
+ .text(_.sprintf('%d/%d', page + 1, pages))
+ .end()
+ .find('button[data-pager-action=previous]')
+ .attr('disabled', page === 0)
+ .end()
+ .find('button[data-pager-action=next]')
+ .attr('disabled', page === pages - 1);
+ }
+
+ self.records.add(records, {silent: true});
+ list.render();
+ d.resolve(list);
+ });
+ return d.promise();
+ },
+ setup_resequence_rows: function (list, dataset) {
+ // drag and drop enabled if list is not sorted and there is a
+ // "sequence" column in the view.
+ if ((dataset.sort && dataset.sort())
+ || !_(this.columns).any(function (column) {
+ return column.name === 'sequence'; })) {
+ return;
+ }
+ // ondrop, move relevant record & fix sequences
+ list.$current.sortable({
+ axis: 'y',
+ items: '> tr[data-id]',
+ stop: function (event, ui) {
+ var to_move = list.records.get(ui.item.data('id')),
+ target_id = ui.item.prev().data('id');
+
+ list.records.remove(to_move);
+ var to = target_id ? list.records.indexOf(list.records.get(target_id)) + 1 : 0;
+ list.records.add(to_move, { at: to });
+
+ // resequencing time!
+ var record, index = to,
+ // if drag to 1st row (to = 0), start sequencing from 0
+ // (exclusive lower bound)
+ seq = to ? list.records.at(to - 1).get('sequence') : 0;
+ while (++seq, record = list.records.at(index++)) {
+ // write are independent from one another, so we can just
+ // launch them all at the same time and we don't really
+ // give a fig about when they're done
+ dataset.write(record.get('id'), {sequence: seq});
+ record.set('sequence', seq);
+ }
+
+ list.refresh_zebra();
+ }
+ });
+ },
+ render: function (post_render) {
+ var self = this;
+ var $element = $('<tbody>');
+ this.elements = [$element[0]];
+
+ this.datagroup.list(
+ _(this.view.visible_columns).chain()
+ .filter(function (column) { return column.tag === 'field' })
+ .pluck('name').value(),
+ function (groups) {
+ $element[0].appendChild(
+ self.render_groups(groups));
+ if (post_render) { post_render(); }
+ }, function (dataset) {
+ self.render_dataset(dataset).then(function (list) {
+ self.children[null] = list;
+ self.elements =
+ [list.$current.replaceAll($element)[0]];
+ self.setup_resequence_rows(list, dataset);
+ if (post_render) { post_render(); }
+ });
+ });
+ return $element;
+ },
+ /**
+ * Returns the ids of all selected records for this group, and the records
+ * themselves
+ */
+ get_selection: function () {
+ var ids = [], records = [];
+
+ _(this.children)
+ .each(function (child) {
+ var selection = child.get_selection();
+ ids.push.apply(ids, selection.ids);
+ records.push.apply(records, selection.records);
+ });
+
+ return {ids: ids, records: records};
+ },
+ on_records_reset: function () {
+ this.children = {};
+ $(this.elements).remove();
+ },
+ 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();
+ }).flatten().value();
+ }
+});
+
+/**
+ * @mixin Events
+ */
+var Events = /** @lends Events# */{
+ /**
+ * @param {String} event event to listen to on the current object, null for all events
+ * @param {Function} handler event handler to bind to the relevant event
+ * @returns this
+ */
+ bind: function (event, handler) {
+ var calls = this['_callbacks'] || (this._callbacks = {});
+
+ if (event in calls) {
+ calls[event].push(handler);
+ } else {
+ calls[event] = [handler];
+ }
+ return this;
+ },
+ /**
+ * @param {String} event event to unbind on the current object
+ * @param {function} [handler] specific event handler to remove (otherwise unbind all handlers for the event)
+ * @returns this
+ */
+ unbind: function (event, handler) {
+ var calls = this._callbacks || {};
+ if (!(event in calls)) { return this; }
+ if (!handler) {
+ delete calls[event];
+ } else {
+ var handlers = calls[event];
+ handlers.splice(
+ _(handlers).indexOf(handler),
+ 1);
+ }
+ return this;
+ },
+ /**
+ * @param {String} event
+ * @returns this
+ */
+ trigger: function (event) {
+ var calls;
+ if (!(calls = this._callbacks)) { return this; }
+ var callbacks = (calls[event] || []).concat(calls[null] || []);
+ for(var i=0, length=callbacks.length; i<length; ++i) {
+ callbacks[i].apply(this, arguments);
+ }
+ return this;
+ }
+};
+var Record = openerp.web.Class.extend(/** @lends Record# */{
+ /**
+ * @constructs Record
+ * @extends openerp.web.Class
+ *
+ * @mixes Events
+ * @param {Object} [data]
+ */
+ init: function (data) {
+ this.attributes = data || {};
+ },
+ /**
+ * @param {String} key
+ * @returns {Object}
+ */
+ get: function (key) {
+ return this.attributes[key];
+ },
+ /**
+ * @param key
+ * @param value
+ * @param {Object} [options]
+ * @param {Boolean} [options.silent=false]
+ * @returns {Record}
+ */
+ set: function (key, value, options) {
+ options = options || {};
+ var old_value = this.attributes[key];
+ if (old_value === value) {
+ return this;
+ }
+ this.attributes[key] = value;
+ if (!options.silent) {
+ this.trigger('change:' + key, this, value, old_value);
+ this.trigger('change', this, key, value, old_value);
+ }
+ return this;
+ },
+ /**
+ * Converts the current record to the format expected by form views:
+ *
+ * .. code-block:: javascript
+ *
+ * data: {
+ * $fieldname: {
+ * value: $value
+ * }
+ * }
+ *
+ *
+ * @returns {Object} record displayable in a form view
+ */
+ toForm: function () {
+ var form_data = {};
+ _(this.attributes).each(function (value, key) {
+ form_data[key] = {value: value};
+ });
+
+ return {data: form_data};
+ }
+});
+Record.include(Events);
+var Collection = openerp.web.Class.extend(/** @lends Collection# */{
+ /**
+ * Smarter collections, with events, very strongly inspired by Backbone's.
+ *
+ * Using a "dumb" array of records makes synchronization between the
+ * various serious
+ *
+ * @constructs Collection
+ * @extends openerp.web.Class
+ *
+ * @mixes Events
+ * @param {Array} [records] records to initialize the collection with
+ * @param {Object} [options]
+ */
+ init: function (records, options) {
+ options = options || {};
+ _.bindAll(this, '_onRecordEvent');
+ this.length = 0;
+ this.records = [];
+ this._byId = {};
+ this._proxies = {};
+ this._key = options.key;
+ this._parent = options.parent;
+
+ if (records) {
+ this.add(records);
+ }
+ },
+ /**
+ * @param {Object|Array} record
+ * @param {Object} [options]
+ * @param {Number} [options.at]
+ * @param {Boolean} [options.silent=false]
+ * @returns this
+ */
+ add: function (record, options) {
+ options = options || {};
+ var records = record instanceof Array ? record : [record];
+
+ for(var i=0, length=records.length; i<length; ++i) {
+ var instance = (records[i] instanceof Record) ? records[i] : new Record(records[i]);
+ instance.bind(null, this._onRecordEvent);
+ this._byId[instance.get('id')] = instance;
+ if (options.at == undefined) {
+ this.records.push(instance);
+ if (!options.silent) {
+ this.trigger('add', this, instance, this.records.length-1);
+ }
+ } else {
+ var insertion_index = options.at + i;
+ this.records.splice(insertion_index, 0, instance);
+ if (!options.silent) {
+ this.trigger('add', this, instance, insertion_index);
+ }
+ }
+ this.length++;
+ }
+ return this;
+ },
+
+ /**
+ * Get a record by its index in the collection, can also take a group if
+ * the collection is not degenerate
+ *
+ * @param {Number} index
+ * @param {String} [group]
+ * @returns {Record|undefined}
+ */
+ at: function (index, group) {
+ if (group) {
+ var groups = group.split('.');
+ return this._proxies[groups[0]].at(index, groups.join('.'));
+ }
+ return this.records[index];
+ },
+ /**
+ * Get a record by its database id
+ *
+ * @param {Number} id
+ * @returns {Record|undefined}
+ */
+ get: function (id) {
+ if (!_(this._proxies).isEmpty()) {
+ var record = null;
+ _(this._proxies).detect(function (proxy) {
+ return record = proxy.get(id);
+ });
+ return record;
+ }
+ return this._byId[id];
+ },
+ /**
+ * Builds a proxy (insert/retrieve) to a subtree of the collection, by
+ * the subtree's group
+ *
+ * @param {String} section group path section
+ * @returns {Collection}
+ */
+ proxy: function (section) {
+ return this._proxies[section] = new Collection(null, {
+ parent: this,
+ key: section
+ }).bind(null, this._onRecordEvent);
+ },
+ /**
+ * @param {Array} [records]
+ * @returns this
+ */
+ reset: function (records) {
+ _(this._proxies).each(function (proxy) {
+ proxy.reset();
+ });
+ this._proxies = {};
+ this.length = 0;
+ this.records = [];
+ this._byId = {};
+ if (records) {
+ this.add(records);
+ }
+ this.trigger('reset', this);
+ return this;
+ },
+ /**
+ * Removes the provided record from the collection
+ *
+ * @param {Record} record
+ * @returns this
+ */
+ remove: function (record) {
+ var self = this;
+ var index = _(this.records).indexOf(record);
+ if (index === -1) {
+ _(this._proxies).each(function (proxy) {
+ proxy.remove(record);
+ });
+ return this;
+ }
+
+ this.records.splice(index, 1);
+ delete this._byId[record.get('id')];
+ this.length--;
+ this.trigger('remove', record, this);
+ return this;
+ },
+
+ _onRecordEvent: function (event, record, options) {
+ // don't propagate reset events
+ if (event === 'reset') { return; }
+ this.trigger.apply(this, arguments);
+ },
+
+ // underscore-type methods
+ each: function (callback) {
+ for(var section in this._proxies) {
+ if (this._proxies.hasOwnProperty(section)) {
+ this._proxies[section].each(callback);
+ }
+ }
+ for(var i=0; i<this.length; ++i) {
+ callback(this.records[i]);
+ }
+ },
+ map: function (callback) {
+ var results = [];
+ this.each(function (record) {
+ results.push(callback(record));
+ });
+ return results;
+ },
+ pluck: function (fieldname) {
+ return this.map(function (record) {
+ return record.get(fieldname);
+ });
+ },
+ indexOf: function (record) {
+ return _(this.records).indexOf(record);
+ }
+});
+Collection.include(Events);
+openerp.web.list = {
+ Events: Events,
+ Record: Record,
+ Collection: Collection
+}
+};
+// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
--- /dev/null
+/**
+ * handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
+ * @namespace
+ */
+openerp.web.list_editable = function (openerp) {
+ var KEY_RETURN = 13,
+ KEY_ESCAPE = 27;
+
+ // editability status of list rows
+ openerp.web.ListView.prototype.defaults.editable = null;
+
+ // TODO: not sure second @lends on existing item is correct, to check
+ openerp.web.ListView.include(/** @lends openerp.web.ListView# */{
+ init: function () {
+ var self = this;
+ this._super.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.web.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']);
+ this._super(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 {
+ this._super();
+ }
+ },
+ on_loaded: function (data, grouped) {
+ // tree/@editable takes priority on everything else if present.
+ this.options.editable = data.arch.attrs.editable || this.options.editable;
+ return this._super(data, grouped);
+ }
+ });
+
+ openerp.web.ListView.Groups.include(/** @lends openerp.web.ListView.Groups# */{
+ passtrough_events: openerp.web.ListView.Groups.prototype.passtrough_events + " edit saved",
+ new_record: function () {
+ // TODO: handle multiple children
+ this.children[null].new_record();
+ }
+ });
+
+ openerp.web.ListView.List.include(/** @lends openerp.web.ListView.List# */{
+ row_clicked: function (event) {
+ if (!this.options.editable) {
+ return this._super(event);
+ }
+ this.edit_record($(event.currentTarget).data('id'));
+ },
+ /**
+ * Checks if a record is being edited, and if so cancels it
+ */
+ cancel_pending_edition: function () {
+ var self = this, cancelled = $.Deferred();
+ if (!this.edition) {
+ cancelled.resolve();
+ return cancelled.promise();
+ }
+
+ if (this.edition_id != null) {
+ this.reload_record(self.records.get(this.edition_id)).then(function () {
+ cancelled.resolve();
+ });
+ } else {
+ cancelled.resolve();
+ }
+ cancelled.then(function () {
+ self.view.unpad_columns();
+ self.edition_form.stop();
+ self.edition_form.$element.remove();
+ delete self.edition_form;
+ delete self.edition_id;
+ delete self.edition;
+ });
+ return cancelled.promise();
+ },
+ /**
+ * Adapts this list's view description to be suitable to the inner form
+ * view of a row being edited.
+ *
+ * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows.
+ */
+ get_form_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;
+ },
+ render_row_as_form: function (row) {
+ var self = this;
+ this.cancel_pending_edition().then(function () {
+ var record_id = $(row).data('id');
+ var $new_row = $('<tr>', {
+ id: _.uniqueId('oe-editable-row-'),
+ 'data-id': record_id,
+ 'class': $(row).attr('class') + ' oe_forms',
+ click: function (e) {e.stopPropagation();}
+ })
+ .delegate('button.oe-edit-row-save', 'click', function () {
+ self.save_row();
+ })
+ .delegate('button.oe-edit-row-cancel', 'click', function () {
+ self.cancel_edition();
+ })
+ .delegate('button', 'keyup', function (e) {
+ e.stopImmediatePropagation();
+ })
+ .keyup(function (e) {
+ switch (e.which) {
+ case KEY_RETURN:
+ self.save_row(true);
+ break;
+ case KEY_ESCAPE:
+ self.cancel_edition();
+ break;
+ default:
+ return;
+ }
+ });
+ if (row) {
+ $new_row.replaceAll(row);
+ } else if (self.options.editable === 'top') {
+ self.$current.prepend($new_row);
+ } else if (self.options.editable) {
+ self.$current.append($new_row);
+ }
+ self.edition = true;
+ self.edition_id = record_id;
+ self.edition_form = _.extend(new openerp.web.FormView(
+ self, $new_row.attr('id'), self.dataset, false), {
+ template: 'ListView.row.form',
+ registry: openerp.web.list.form.widgets
+ });
+ $.when(self.edition_form.on_loaded(self.get_form_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>');
+ }
+ });
+ // Add columns for the cancel and save buttons, if
+ // there are none in the list
+ if (!self.options.selectable) {
+ self.view.pad_columns(
+ 1, {except: $new_row, position: 'before'});
+ }
+ if (!self.options.deletable) {
+ self.view.pad_columns(
+ 1, {except: $new_row});
+ }
+
+ self.edition_form.do_show();
+ });
+ });
+ },
+ handle_onwrite: function (source_record_id) {
+ var self = this;
+ var on_write_callback = self.view.fields_view.arch.attrs.on_write;
+ if (!on_write_callback) { return; }
+ this.dataset.call(on_write_callback, [source_record_id], function (ids) {
+ _(ids).each(function (id) {
+ var record = self.records.get(id);
+ if (!record) {
+ // insert after the source record
+ var index = self.records.indexOf(
+ self.records.get(source_record_id)) + 1;
+ record = new openerp.web.list.Record({id: id});
+ self.records.add(record, {at: index});
+ self.dataset.ids.splice(index, 0, id);
+ }
+ self.reload_record(record);
+ });
+ });
+ },
+ /**
+ * 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) {
+ if (result.created && !self.edition_id) {
+ self.records.add({id: result.result},
+ {at: self.options.editable === 'top' ? 0 : null});
+ self.edition_id = result.result;
+ }
+ var edited_record = self.records.get(self.edition_id),
+ next_record = self.records.at(
+ self.records.indexOf(edited_record) + 1);
+
+ self.handle_onwrite(self.edition_id);
+ self.cancel_pending_edition().then(function () {
+ $(self).trigger('saved', [self.dataset]);
+ if (!edit_next) {
+ return;
+ }
+ if (result.created) {
+ self.new_record();
+ return;
+ }
+ var next_record_id;
+ if (next_record) {
+ next_record_id = next_record.get('id');
+ self.dataset.index = _(self.dataset.ids)
+ .indexOf(next_record_id);
+ } else {
+ self.dataset.index = 0;
+ next_record_id = self.records.at(0).get('id');
+ }
+ self.edit_record(next_record_id);
+ });
+ }, 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 (record_id) {
+ this.render_row_as_form(
+ this.$current.find('[data-id=' + record_id + ']'));
+ $(this).trigger(
+ 'edit',
+ [record_id, this.dataset]);
+ },
+ new_record: function () {
+ this.dataset.index = null;
+ this.render_row_as_form();
+ }
+ });
+ if (!openerp.web.list) {
+ openerp.web.list = {};
+ }
+ if (!openerp.web.list.form) {
+ openerp.web.list.form = {};
+ }
+ openerp.web.list.form.WidgetFrame = openerp.web.form.WidgetFrame.extend({
+ template: 'ListView.row.frame'
+ });
+ var form_widgets = openerp.web.form.widgets;
+ openerp.web.list.form.widgets = form_widgets.clone({
+ 'frame': 'openerp.web.list.form.WidgetFrame'
+ });
+ // All form widgets inherit a problematic behavior from
+ // openerp.web.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.web.list.form.widgets;
+ _(list_form_widgets.map).each(function (widget_path, key) {
+ if (key === 'frame') { return; }
+ var new_path = 'openerp.web.list.form.' + key;
+
+ openerp.web.list.form[key] = (form_widgets.get_object(key)).extend({
+ update_dom: function () {
+ this.$element.children().css('visibility', '');
+ if (this.modifiers.tree_invisible) {
+ var old_invisible = this.invisible;
+ this.invisible = !!this.modifiers.tree_invisible;
+ this._super();
+ this.invisible = old_invisible;
+ } else if (this.invisible) {
+ this.$element.children().css('visibility', 'hidden');
+ }
+ }
+ });
+ list_form_widgets.add(key, new_path);
+ });
+};