openerp.web.form = function (openerp) { var _t = openerp.web._t, _lt = openerp.web._lt; 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", display_name: _lt('Form'), /** * @constructs openerp.web.FormView * @extends openerp.web.View * * @param {openerp.web.Session} session the current openerp session * @param {openerp.web.DataSet} dataset the dataset this view will work with * @param {String} view_id the identifier of the OpenERP view object * @param {Object} options * - sidebar : [true|false] * - resize_textareas : [true|false|max_height] * * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance */ init: function(parent, dataset, view_id, options) { this._super(parent); this.set_default_options(options); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id || false; this.fields_view = {}; this.fields = {}; this.fields_order = []; this.datarecord = {}; 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, { "not_interactible_on_create": false }); this.is_initialized = $.Deferred(); this.mutating_mutex = new $.Mutex(); this.on_change_mutex = new $.Mutex(); this.reload_mutex = new $.Mutex(); this.set({"force_readonly": false}); this.rendering_engine = new openerp.web.FormRenderingEngine(this); }, start: function() { this._super(); return this.init_view(); }, init_view: function() { if (this.embedded_view) { var def = $.Deferred().then(this.on_loaded); var self = this; $.async_when().then(function() {def.resolve(self.embedded_view);}); 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); } }, destroy: function() { if (this.sidebar) { this.sidebar.attachments.destroy(); this.sidebar.destroy(); } _.each(this.get_widgets(), function(w) { w.destroy(); }); this._super(); }, reposition: function ($e) { this.$element = $e; this.on_loaded(); }, on_loaded: function(data) { var self = this; if (!data) { throw new Error("No data provided."); } if (this.arch) { throw "Form view does not support multiple calls to on_loaded"; } this.fields_order = []; this.fields_view = data; this.rendering_engine.set_fields_view(data); this.rendering_engine.render_to(this.$element.find('.oe_form_content')); this.$form_header = this.$element.find('.oe_form_header:first'); 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.on_button_save); this.$form_header.find('button.oe_form_button_cancel').click(this.on_button_cancel); 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.do_unfold(); this.sidebar.attachments = new openerp.web.form.SidebarAttachments(this.sidebar, this); this.sidebar.add_toolbar(this.fields_view.toolbar); this.set_common_sidebar_sections(this.sidebar); this.sidebar.add_section(_t('Customize'), 'customize'); this.sidebar.add_items('customize', [{ label: _t('Set Default'), form: this, callback: function (item) { item.form.open_defaults_dialog(); } }]); } this.has_been_loaded.resolve(); }, do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { if (!this.dataset.get_id_index(state.id)) { this.dataset.ids.push(state.id); } this.dataset.select_id(state.id); if (warm) { this.do_show(); } } }, do_show: function () { var self = this; this.$element.show().css('visibility', 'hidden'); this.$element.removeClass('oe_form_dirty'); return this.has_been_loaded.pipe(function() { var result; if (self.dataset.index === null) { // null index means we should start a new record result = self.on_button_new(); } else { result = self.dataset.read_index(_.keys(self.fields_view.fields), { context : { 'bin_size' : true } }).pipe(self.on_record_loaded); } result.pipe(function() { self.$element.css('visibility', 'visible'); }); if (self.sidebar) { self.sidebar.$element.show(); } return result; }); }, do_hide: function () { this._super(); if (this.sidebar) { this.sidebar.$element.hide(); } }, on_record_loaded: function(record) { var self = this, set_values = []; if (!record) { this.do_warn("Form", "The record could not be found in the database.", true); return $.Deferred().reject(); } this.datarecord = record; _(this.fields).each(function (field, f) { field.reset(); var result = field.set_value(self.datarecord[f] || false); set_values.push(result); $.when(result).then(function() { field.validate(); }); }); return $.when.apply(null, set_values).pipe(function() { if (!record.id) { // New record: Second pass in order to trigger the onchanges // respecting the fields order defined in the view _.each(self.fields_order, function(field_name) { if (record[field_name] !== undefined) { var field = self.fields[field_name]; field.dirty = true; self.do_onchange(field); } }); } self.on_form_changed(); self.is_initialized.resolve(); self.do_update_pager(record.id == null); if (self.sidebar) { self.sidebar.attachments.do_update(); } if (self.default_focus_field) { self.default_focus_field.focus(); } if (record.id) { self.do_push_state({id:record.id}); } self.$element.removeClass('oe_form_dirty'); }); }, on_form_changed: function() { _.each(this.get_widgets(), function(w) { w.process_modifiers(); if (w.field) { w.validate(); } w.update_dom(); }); }, do_notify_change: function() { this.$element.addClass('oe_form_dirty'); }, on_pager_action: function(action) { if (this.can_be_discarded()) { 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.$form_header.find('div.oe_form_pager'); var index = hide_index ? '-' : this.dataset.index + 1; $pager.find('button').prop('disabled', this.dataset.ids.length < 2); $pager.find('span.oe_pager_index').html(index); $pager.find('span.oe_pager_count').html(this.dataset.ids.length); }, parse_on_change: function (on_change, widget) { var self = this; var onchange = _.str.trim(on_change); var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/); if (!call) { return null; } var method = call[1]; if (!_.str.trim(call[2])) { return {method: method, args: [], 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 = new openerp.web.CompoundContext(self.dataset.get_context(), widget.build_context() ? widget.build_context() : {}); return ctx; } }; var parent_fields = null, context_index = null; var args = _.map(call[2].split(','), function (a, i) { var field = _.str.trim(a); // literal constant or context if (field in argument_replacement) { return argument_replacement[field](i); } // literal number if (/^-?\d+(\.\d+)?$/.test(field)) { return Number(field); } // form field if (self.fields[field]) { var value = self.fields[field].get_on_change_value(); return value == null ? false : value; } // parent field var splitted = field.split('.'); if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) { if (parent_fields === null) { parent_fields = self.dataset.parent_view.get_fields_values([self.dataset.child_name]); } var p_val = parent_fields[_.str.trim(splitted[1])]; if (p_val !== undefined) { return p_val == null ? false : p_val; } } // string literal var first_char = field[0], last_char = field[field.length-1]; if ((first_char === '"' && last_char === '"') || (first_char === "'" && last_char === "'")) { return field.slice(1, -1); } throw new Error("Could not get field with name '" + field + "' for onchange '" + onchange + "'"); }); return { method: method, args: args, context_index: context_index }; }, do_onchange: function(widget, processed) { var self = this; return this.on_change_mutex.exec(function() { try { var response = {}, can_process_onchange = $.Deferred(); processed = processed || []; processed.push(widget.name); var on_change = widget.node.attrs.on_change; if (on_change) { var change_spec = self.parse_on_change(on_change, widget); if (change_spec) { var ajax = { url: '/web/dataset/onchange', async: false }; can_process_onchange = self.rpc(ajax, { model: self.dataset.model, method: change_spec.method, args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args), context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1 }).then(function(r) { _.extend(response, r); }); } else { console.warn("Wrong on_change format", on_change); } } // fail if onchange failed if (can_process_onchange.isRejected()) { return can_process_onchange; } if (widget.field['change_default']) { var fieldname = widget.name, value; if (response.value && (fieldname in response.value)) { // Use value from onchange if onchange executed value = response.value[fieldname]; } else { // otherwise get form value for field value = self.fields[fieldname].get_on_change_value(); } var condition = fieldname + '=' + value; if (value) { can_process_onchange = self.rpc({ url: '/web/dataset/call', async: false }, { model: 'ir.values', method: 'get_defaults', args: [self.model, condition] }).then(function (results) { if (!results.length) { return; } if (!response.value) { response.value = {}; } for(var i=0; i"; } }); msg += ""; this.do_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 return $.Deferred().reject(); } else { return $.when(this.reload()).pipe(function () { return $.when(r).then(success); }, null); } }, /** * 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 return $.Deferred().reject(); } else { this.datarecord.id = r.result; if (!prepend_on_create) { this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id])); this.dataset.index = this.dataset.ids.length - 1; } else { this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids)); this.dataset.index = 0; } this.do_update_pager(); if (this.sidebar) { this.sidebar.attachments.do_update(); } //openerp.log("The record has been created with id #" + this.datarecord.id); this.reload(); return $.when(_.extend(r, {created: true})).then(success); } }, on_action: function (action) { console.debug('Executing action', action); }, reload: function() { var self = this; return this.reload_mutex.exec(function() { if (self.dataset.index == null || self.dataset.index < 0) { return $.when(self.on_button_new()); } else { return self.dataset.read_index(_.keys(self.fields_view.fields), { context : { 'bin_size' : true } }).pipe(self.on_record_loaded); } }); }, get_widgets: function() { return _.filter(this.getChildren(), function(obj) { return obj instanceof openerp.web.form.Widget; }); }, get_fields_values: function(blacklist) { blacklist = blacklist || []; var values = {}; var ids = this.get_selected_ids(); values["id"] = ids.length > 0 ? ids[0] : false; _.each(this.fields, function(value, key) { if (_.include(blacklist, key)) return; 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] : []; }, recursive_save: function() { var self = this; return $.when(this.do_save()).pipe(function(res) { if (self.dataset.parent_view) return self.dataset.parent_view.recursive_save(); }); }, is_dirty: function() { return _.any(this.fields, function (value) { return value.is_dirty(); }); }, is_interactible_record: function() { var id = this.datarecord.id; if (!id) { if (this.options.not_interactible_on_create) return false; } else if (typeof(id) === "string") { if(openerp.web.BufferedDataSet.virtual_id_regex.test(id)) return false; } return true; }, sidebar_context: function () { return this.do_save().pipe(_.bind(function() {return this.get_fields_values();}, this)); }, open_defaults_dialog: function () { var self = this; var fields = _.chain(this.fields) .map(function (field, name) { var value = field.get_value(); // ignore fields which are empty, invisible, readonly, o2m // or m2m if (!value || field.invisible || field.get("readonly") || field.field.type === 'one2many' || field.field.type === 'many2many') { return false; } var displayed; switch(field.field.type) { case 'selection': displayed = _(field.values).find(function (option) { return option[0] === value; })[1]; break; case 'many2one': displayed = field.value[1] || value; break; default: displayed = value; } return { name: name, string: field.node_atts.string, value: value, displayed: displayed, // convert undefined to false change_default: !!field.field.change_default } }) .compact() .sortBy(function (field) { return field.node_atts.string; }) .value(); var conditions = _.chain(fields) .filter(function (field) { return field.change_default; }) .value(); var d = new openerp.web.Dialog(this, { title: _t("Set Default"), args: { fields: fields, conditions: conditions }, buttons: [ {text: _t("Close"), click: function () { d.close(); }}, {text: _t("Save default"), click: function () { var $defaults = d.$element.find('#formview_default_fields'); var field_to_set = $defaults.val(); if (!field_to_set) { $defaults.parent().addClass('invalid'); return; } var condition = d.$element.find('#formview_default_conditions').val(), all_users = d.$element.find('#formview_default_all').is(':checked'); new openerp.web.DataSet(self, 'ir.values').call( 'set_default', [ self.dataset.model, field_to_set, self.fields[field_to_set].get_value(), all_users, false, condition || false ]).then(function () { d.close(); }); }} ] }); d.template = 'FormView.set_default'; d.open(); } }); /** * Default rendering engine for the form view. * * It is necessary to set the view using set_view() before usage. */ openerp.web.FormRenderingEngine = openerp.web.Class.extend({ init: function(view) { this.view = view; }, set_fields_view: function(fvg) { this.fvg = fvg; }, set_registry: function(registry) { this.registry = registry; }, render_to: function($element) { var self = this; this.$element = $element; this.fields_prefix = this.view.dataset ? this.view.dataset.model : ''; // TODO: I know this will save the world and all the kitten for a moment, // but one day, we will have to get rid of xml2json var xml = openerp.web.json_node_to_xml(this.fvg.arch); this.$form = $(xml); this.process(this.$form); this.$form.children().appendTo(this.$element); // OpenERP views spec : // - @width is obsolete ? // TODO: modifiers invisible. Add a special attribute, eg: data-invisible that should be used in order to create openerp.form.InvisibleWidgetG this.$element.find('field, button').each(function() { var $elem = $(this), key = $elem.attr('widget') || $elem[0].tagName.toLowerCase(); if (self.view.registry.contains(key)) { var obj = self.view.registry.get_object(key); var w = new (obj)(self.view, openerp.web.xml_to_json($elem[0])); self.alter_field(w); w.replace($elem); } }); $('').appendTo(this.$element).click(this.do_toggle_layout_debugging); }, alter_field: function(field) {}, do_toggle_layout_debugging: function() { if (!this.$element.has('.oe_layout_debug_cell:first').length) { this.$element.find('.oe_form_group_cell').each(function() { var $span = $('').text($(this).attr('width')); $span.prependTo($(this)); }); } this.$element.toggleClass('oe_layout_debugging'); }, process: function($tag) { var self = this; var tagname = $tag[0].nodeName.toLowerCase(); var fn = self['process_' + tagname]; if (this.registry && this.registry.contains(tagname)) { fn = this.registry.get_object(tagname); } if (fn) { var args = [].slice.call(arguments); args[0] = $tag; return fn.apply(self, args); } else { // generic tag handling, just process children $tag.children().each(function() { self.process($(this)); }); return $tag; } }, preprocess_field: function($field) { var name = $field.attr('name'), field_orm = this.fvg.fields[name], field_string = $field.attr('string') || field_orm.string || '', field_help = $field.attr('help') || field_orm.help || '', field_colspan = parseInt($field.attr('colspan'), 10); if ($field.attr('nolabel') !== '1') { $field.attr('nolabel', '1'); var $label = this.$form.find('label[for="' + name + '"]'); if (!$label.length) { field_string = $label.attr('string') || $label.text() || field_string; field_help = $label.attr('help') || field_help; $label = $('