odoo.define('web.FormView', function (require) { "use strict"; var common = require('web.form_common'); var core = require('web.core'); var crash_manager = require('web.crash_manager'); var data = require('web.data'); var Dialog = require('web.Dialog'); var FormRenderingEngine = require('web.FormRenderingEngine'); var Model = require('web.DataModel'); var Pager = require('web.Pager'); var Sidebar = require('web.Sidebar'); var utils = require('web.utils'); var View = require('web.View'); var _t = core._t; var _lt = core._lt; var QWeb = core.qweb; /** * Properties: * - actual_mode: always "view", "edit" or "create". Read-only property. Determines * the mode used by the view. */ var FormView = View.extend(common.FieldManagerMixin, { className: "o_form_view", defaults: _.extend({}, View.prototype.defaults, { not_interactible_on_create: false, initial_mode: "view", disable_autofocus: false, footer_to_buttons: false, }), display_name: _lt('Form'), icon: 'fa-edit', multi_record: false, // Indicates that this view is not searchable, and thus that no search view should be displayed. searchable: false, /** * Called each time the form view is attached into the DOM */ on_attach_callback: function() { this.trigger('attached'); this.autofocus(); }, /** * Called each time the form view is detached from the DOM */ on_detach_callback: function() { this.trigger('detached'); }, init: function() { var self = this; this._super.apply(this, arguments); this.fields = {}; this.fields_order = []; this.datarecord = {}; this._onchange_specs = {}; this.onchanges_mutex = new utils.Mutex(); this.default_focus_field = null; this.default_focus_button = null; this.fields_registry = core.form_widget_registry; this.tags_registry = core.form_tag_registry; this.widgets_registry = core.form_custom_registry; this.has_been_loaded = $.Deferred(); this.translatable_fields = []; this.is_initialized = $.Deferred(); this.record_loaded = $.Deferred(); this.mutating_mutex = new utils.Mutex(); this.save_list = []; this.render_value_defs = []; this.reload_mutex = new utils.Mutex(); this.__clicked_inside = false; this.__blur_timeout = null; this.rendering_engine = new FormRenderingEngine(this); this.set({actual_mode: this.options.initial_mode}); this.has_been_loaded.done(function() { self._build_onchange_specs(); self.on("change:actual_mode", self, self.toggle_buttons); self.on("change:actual_mode", self, self.toggle_sidebar); }); self.on("load_record", self, self.load_record); core.bus.on('clear_uncommitted_changes', this, function(chain_callbacks) { var self = this; chain_callbacks(function() { return self.can_be_discarded(); }); }); }, start: function() { if (this.$pager) { this.$pager.off(); } var self = this; this.rendering_engine.set_fields_registry(this.fields_registry); this.rendering_engine.set_tags_registry(this.tags_registry); this.rendering_engine.set_widgets_registry(this.widgets_registry); this.rendering_engine.set_fields_view(this.fields_view); this.rendering_engine.render_to(this.$el); this.$el.on('mousedown.formBlur', function () { self.__clicked_inside = true; }); this.has_been_loaded.resolve(); // Add bounce effect on button 'Edit' when click on readonly page view. this.$(".oe_title,.o_group").on('click', function (e) { if(self.get("actual_mode") === "view" && self.$buttons && !$(e.target).is('[data-toggle]')) { self.$buttons.find(".o_form_button_edit").openerpBounce(); core.bus.trigger('click', e); } }); return this._super(); }, destroy: function() { _.each(this.get_widgets(), function(w) { w.off('focused blurred'); w.destroy(); }); if (this.$el) { this.$el.off('.formBlur'); } this._super(); }, /** * Render the buttons according to the FormView.buttons template and add listeners on it. * Set this.$buttons with the produced jQuery element * @param {jQuery} [$node] a jQuery node where the rendered buttons should be inserted * $node may be undefined, in which case the FormView inserts them into this.options.$buttons * if it exists */ render_buttons: function($node) { this.$buttons = $('
'); var $footer = this.$('footer'); if (this.options.action_buttons !== false || this.options.footer_to_buttons && $footer.children().length === 0) { this.$buttons.append(QWeb.render("FormView.buttons", {'widget': this})); } if (this.options.footer_to_buttons) { $footer.appendTo(this.$buttons); } // Show or hide the buttons according to the view mode this.toggle_buttons(); this.$buttons.on('click', '.o_form_button_create', this.on_button_create); this.$buttons.on('click', '.o_form_button_edit', this.on_button_edit); this.$buttons.on('click', '.o_form_button_save', this.on_button_save); this.$buttons.on('click', '.o_form_button_cancel', this.on_button_cancel); this.$buttons.appendTo($node); }, /** * Instantiate and render the sidebar if a sidebar is requested * Sets this.sidebar * @param {jQuery} [$node] a jQuery node where the sidebar should be inserted **/ render_sidebar: function($node) { if (!this.sidebar && this.options.sidebar) { this.sidebar = new Sidebar(this, {editable: this.is_action_enabled('edit')}); if (this.fields_view.toolbar) { this.sidebar.add_toolbar(this.fields_view.toolbar); } this.sidebar.add_items('other', _.compact([ this.is_action_enabled('delete') && { label: _t('Delete'), callback: this.on_button_delete }, this.is_action_enabled('create') && { label: _t('Duplicate'), callback: this.on_button_duplicate } ])); this.sidebar.appendTo($node); // Show or hide the sidebar according to the view mode this.toggle_sidebar(); } }, /** * Instantiate and render the pager and add listeners on it. * Set this.pager * @param {jQuery} [$node] a jQuery node where the pager should be inserted * $node may be undefined, in which case the FormView inserts the pager into this.options.$pager */ render_pager: function($node) { if (this.options.pager) { var self = this; var options = { validate: _.bind(this.can_be_discarded, this), }; this.pager = new Pager(this, this.dataset.ids.length, this.dataset.index + 1, 1, options); this.pager.on('pager_changed', this, function (new_state) { this.pager.disable(); this.dataset.index = new_state.current_min - 1; this.trigger('pager_action_executed'); $.when(this.reload()).then(function () { self.pager.enable(); }); }); this.pager.appendTo($node = $node || this.options.$pager); // Hide the pager in create mode if (this.get("actual_mode") === "create") { this.pager.do_hide(); } } }, /** * Show or hide the buttons according to the actual_mode */ toggle_buttons: function() { var view_mode = this.get("actual_mode") === "view"; if (this.$buttons) { this.$buttons.find('.o_form_buttons_view').toggle(view_mode); this.$buttons.find('.o_form_buttons_edit').toggle(!view_mode); } }, /** * Show or hide the sidebar according to the actual_mode */ toggle_sidebar: function() { if (this.sidebar) { this.sidebar.do_toggle(this.get("actual_mode") === "view"); } }, update_pager: function() { if (this.pager) { // Hide the pager in create mode if (this.get("actual_mode") === "create") { this.pager.do_hide(); } else { this.pager.update_state({size: this.dataset.ids.length, current_min: this.dataset.index + 1}); } } }, widgetFocused: function() { // Clear click flag if used to focus a widget this.__clicked_inside = false; if (this.__blur_timeout) { clearTimeout(this.__blur_timeout); this.__blur_timeout = null; } }, widgetBlurred: function() { if (this.__clicked_inside) { // clicked in an other section of the form (than the currently // focused widget) => just ignore the blurring entirely? this.__clicked_inside = false; return; } var self = this; // clear timeout, if any this.widgetFocused(); this.__blur_timeout = setTimeout(function () { self.trigger('blurred'); }, 0); }, do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { if (this.dataset.get_id_index(state.id) === null) { this.dataset.ids.push(state.id); } this.dataset.select_id(state.id); this.do_show(); } }, /** * @param {Boolean} [options.mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view". * @param {Boolean} [options.reload=true] whether the form should reload its content on show, or use the currently loaded record * @return {$.Deferred} */ do_show: function (options) { var self = this; options = options || {}; this.$el.removeClass('oe_form_dirty'); var shown = this.has_been_loaded; if (options.reload !== false) { shown = shown.then(function() { if (self.dataset.index === null) { // null index means we should start a new record return self.on_button_new(); } var fields = _.keys(self.fields_view.fields); fields.push('display_name'); fields.push('__last_update'); return self.dataset.read_index(fields, { context: { 'bin_size': true } }).then(function(r) { self.trigger('load_record', r); }); }); } return $.when(shown, this._super()).then(function() { self._actualize_mode(options.mode || self.options.initial_mode); core.bus.trigger('form_view_shown', self); }); }, load_record: function(record) { var self = this, set_values = []; if (!record) { this.set({ 'title' : undefined }); this.do_warn(_t("Form"), _t("The record could not be found in the database."), true); return $.Deferred().reject(); } this.datarecord = record; this.record_loaded = $.Deferred(); _(this.fields).each(function (field, f) { field._dirty_flag = false; field._inhibit_on_change_flag = true; var result = field.set_value(self.datarecord[f] || false); field._inhibit_on_change_flag = false; set_values.push(result); }); this._actualize_mode(); // call after updating the fields as it may trigger a re-rendering this.set({ 'title' : record.id ? record.display_name : _t("New") }); this.update_pager(); // the mode must be actualized before updating the pager return $.when.apply(null, set_values).then(function() { if (!record.id) { // trigger onchanges self.do_onchange(null); } self.on_form_changed(); self.rendering_engine.init_fields().then(function() { self.is_initialized.resolve(); self.record_loaded.resolve(); if (self.sidebar) { self.sidebar.do_attachement_update(self.dataset, self.datarecord.id); } if (record.id) { self.do_push_state({id:record.id}); } else { self.do_push_state({}); } self.$el.removeClass('oe_form_dirty'); }); }); }, /** * Loads and sets up the default values for the model as the current * record * * @return {$.Deferred} */ load_defaults: function () { var self = this; var keys = _.keys(this.fields_view.fields); if (keys.length) { return this.dataset.default_get(keys).then(function(r) { self.trigger('load_record', _.clone(r)); }); } return $.when().then(this.trigger.bind(this, 'load_record', {})); }, on_form_changed: function() { this.trigger("view_content_has_changed"); }, do_notify_change: function() { this.$el.addClass('oe_form_dirty'); }, _build_onchange_specs: function() { var self = this; var find = function(field_name, root) { var fields = [root]; while (fields.length) { var node = fields.pop(); if (!node) { continue; } if (node.tag === 'field' && node.attrs.name === field_name) { return node.attrs.on_change || ""; } fields = _.union(fields, node.children); } return ""; }; self._onchange_fields = []; self._onchange_specs = {}; _.each(this.fields, function(field, name) { self._onchange_fields.push(name); self._onchange_specs[name] = find(name, field.node); _.each(field.field.views, function(view) { _.each(view.fields, function(_, subname) { self._onchange_specs[name + '.' + subname] = find(subname, view.arch); }); }); }); }, _get_onchange_values: function() { var field_values = this.get_fields_values(); if (field_values.id.toString().match(data.BufferedDataSet.virtual_id_regex)) { delete field_values.id; } if (this.dataset.parent_view) { // this belongs to a parent view: add parent field if possible var parent_view = this.dataset.parent_view; var child_name = this.dataset.child_name; var parent_name = parent_view.get_field_desc(child_name).relation_field; if (parent_name) { // consider all fields except the inverse of the parent field var parent_values = parent_view.get_fields_values(); delete parent_values[child_name]; field_values[parent_name] = parent_values; } } return field_values; }, do_onchange: function(widget) { var self = this; var onchange_specs = self._onchange_specs; try { var def = $.when({}); var change_spec = widget ? onchange_specs[widget.name] : null; if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) { var ids = [], trigger_field_name = widget ? widget.name : self._onchange_fields, values = self._get_onchange_values(), context = new data.CompoundContext(self.dataset.get_context()); if (widget && widget.build_context()) { context.add(widget.build_context()); } if (self.dataset.parent_view) { var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field; context.add({field_parent: parent_name}); } if (self.datarecord.id && !data.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) { // In case of a o2m virtual id, we should pass an empty ids list ids.push(self.datarecord.id); } def = self.alive(self.dataset.call( "onchange", [ids, values, trigger_field_name, onchange_specs, context])); } this.onchanges_mutex.exec(function(){ return def.then(function(response) { var fields = {}; if (widget){ fields[widget.name] = widget.field; } else{ fields = self.fields_view.fields; } var defs = []; _.each(fields, function(field, fieldname){ if (field && field.change_default) { var 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_value(); } var condition = fieldname + '=' + value_; if (value_) { defs.push(self.alive(new Model('ir.values').call( 'get_defaults', [self.model, condition] )).then(function (results) { if (!results.length) { return response; } if (!response.value) { response.value = {}; } for(var i=0; i