diff options
| author | Géry Debongnie <ged@odoo.com> | 2015-03-09 16:34:27 +0100 |
|---|---|---|
| committer | Géry Debongnie <ged@odoo.com> | 2015-03-18 09:01:49 +0100 |
| commit | e8a00bc50da435d3859118ca127d20a2eb9a0cab (patch) | |
| tree | 039f6ec40b37392e76c8830a8e518ac40f7e7fec /addons/web/static/src/js/view_form.js | |
| parent | 9036084f065c7aca3b2199903f4da67db3501ba7 (diff) | |
[REF] web: add module system to the web client
This commit introduces a new module system for the javascript code.
Instead of using global (namespaced) variables everywhere (like
openerp.web.whatever, ...), each file declare one (or more) module
with its dependencies. Each module will be loaded after its
dependencies, and will be given a reference to the return values.
This change is a massive departure from the way the client worked
previously. It will sadly break some/most history, but I am sure that
the future git ecosystem will allow you to easily browse the history
past this revision (one can dream).
This commit feature:
* main namespace is now odoo instead of openerp
* a way to define modules and dependencies (inspired by requirejs)
* many files have been splitted in simpler subfiles
* basic support for debugging in odoo.__DEBUG__
* no global variable (i.e. nothing in window.openerp)
* many bug fixes, many new bugs
Future vision: this modularization should simplify future code
development, simplify debugging and will allow a serious testing effort
Diffstat (limited to 'addons/web/static/src/js/view_form.js')
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 6443 |
1 files changed, 0 insertions, 6443 deletions
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js deleted file mode 100644 index bdd1ecb6225..00000000000 --- a/addons/web/static/src/js/view_form.js +++ /dev/null @@ -1,6443 +0,0 @@ -(function() { - -var instance = openerp; -var _t = instance.web._t, - _lt = instance.web._lt; -var QWeb = instance.web.qweb; - -/** @namespace */ -instance.web.form = {}; - -/** - * Interface implemented by the form view or any other object - * able to provide the features necessary for the fields to work. - * - * Properties: - * - display_invalid_fields : if true, all fields where is_valid() return true should - * be displayed as invalid. - * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create". - * Events: - * - view_content_has_changed : when the values of the fields have changed. When - * this event is triggered all fields should reprocess their modifiers. - * - field_changed:<field_name> : when the value of a field change, an event is triggered - * named "field_changed:<field_name>" with <field_name> replaced by the name of the field. - * This event is not related to the on_change mechanism of OpenERP and is always called - * when the value of a field is setted or changed. This event is only triggered when the - * value of the field is syntactically valid, but it can be triggered when the value - * is sematically invalid (ie, when a required field is false). It is possible that an event - * about a precise field is never triggered even if that field exists in the view, in that - * case the value of the field is assumed to be false. - */ -instance.web.form.FieldManagerMixin = { - /** - * Must return the asked field as in fields_get. - */ - get_field_desc: function(field_name) {}, - /** - * Returns the current value of a field present in the view. See the get_value() method - * method in FieldInterface for further information. - */ - get_field_value: function(field_name) {}, - /** - Gives new values for the fields contained in the view. The new values could not be setted - right after the call to this method. Setting new values can trigger on_changes. - - @param {Object} values A dictonary with key = field name and value = new value. - @return {$.Deferred} Is resolved after all the values are setted. - */ - set_values: function(values) {}, - /** - Computes an OpenERP domain. - - @param {Array} expression An OpenERP domain. - @return {boolean} The computed value of the domain. - */ - compute_domain: function(expression) {}, - /** - Builds an evaluation context for the resolution of the fields' contexts. Please note - the field are only supposed to use this context to evualuate their own, they should not - extend it. - - @return {CompoundContext} An OpenERP context. - */ - build_eval_context: function() {}, -}; - -instance.web.views.add('form', 'instance.web.FormView'); -/** - * Properties: - * - actual_mode: always "view", "edit" or "create". Read-only property. Determines - * the mode used by the view. - */ -instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, { - /** - * 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'), - view_type: "form", - /** - * @constructs instance.web.FormView - * @extends instance.web.View - * - * @param {instance.web.Session} session the current openerp session - * @param {instance.web.DataSet} dataset the dataset this view will work with - * @param {String} view_id the identifier of the OpenERP view object - * - * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance - */ - init: function(parent, dataset, view_id, options) { - var self = this; - this._super(parent); - this.ViewManager = 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._onchange_specs = {}; - this.onchanges_mutex = new $.Mutex(); - this.default_focus_field = null; - this.default_focus_button = null; - this.fields_registry = instance.web.form.widgets; - this.tags_registry = instance.web.form.tags; - this.widgets_registry = instance.web.form.custom_widgets; - this.has_been_loaded = $.Deferred(); - this.translatable_fields = []; - _.defaults(this.options, { - "not_interactible_on_create": false, - "initial_mode": "view", - "disable_autofocus": false, - "footer_to_buttons": false, - }); - this.is_initialized = $.Deferred(); - this.mutating_mutex = new $.Mutex(); - this.save_list = []; - this.render_value_defs = []; - this.reload_mutex = new $.Mutex(); - this.__clicked_inside = false; - this.__blur_timeout = null; - this.rendering_engine = new instance.web.form.FormRenderingEngine(this); - self.set({actual_mode: self.options.initial_mode}); - this.has_been_loaded.done(function() { - self._build_onchange_specs(); - self.on("change:actual_mode", self, self.check_actual_mode); - self.check_actual_mode(); - self.on("change:actual_mode", self, self.init_pager); - self.init_pager(); - }); - self.on("load_record", self, self.load_record); - instance.web.bus.on('clear_uncommitted_changes', this, function(e) { - if (!this.can_be_discarded()) { - e.preventDefault(); - } - }); - }, - view_loading: function(r) { - return this.load_form(r); - }, - destroy: function() { - _.each(this.get_widgets(), function(w) { - w.off('focused blurred'); - w.destroy(); - }); - if (this.$el) { - this.$el.off('.formBlur'); - } - this._super(); - }, - load_form: function(data) { - var self = this; - if (!data) { - throw new Error(_t("No data provided.")); - } - if (this.arch) { - throw "Form view does not support multiple calls to load_form"; - } - this.fields_order = []; - this.fields_view = data; - - 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(data); - var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container'); - this.rendering_engine.render_to($dest); - - this.$el.on('mousedown.formBlur', function () { - self.__clicked_inside = true; - }); - - this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self})); - if (this.options.$buttons) { - this.$buttons.appendTo(this.options.$buttons); - } else { - this.$el.find('.oe_form_buttons').replaceWith(this.$buttons); - } - this.$buttons.on('click', '.oe_form_button_create', - this.guard_active(this.on_button_create)); - this.$buttons.on('click', '.oe_form_button_edit', - this.guard_active(this.on_button_edit)); - this.$buttons.on('click', '.oe_form_button_save', - this.guard_active(this.on_button_save)); - this.$buttons.on('click', '.oe_form_button_cancel', - this.guard_active(this.on_button_cancel)); - if (this.options.footer_to_buttons) { - this.$el.find('footer').appendTo(this.$buttons); - } - - this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar'); - if (!this.sidebar && this.options.$sidebar) { - this.sidebar = new instance.web.Sidebar(this); - this.sidebar.appendTo(this.$sidebar); - if (this.fields_view.toolbar) { - this.sidebar.add_toolbar(this.fields_view.toolbar); - } - this.sidebar.add_items('other', _.compact([ - self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete }, - self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate } - ])); - } - - this.has_been_loaded.resolve(); - - // Add bounce effect on button 'Edit' when click on readonly page view. - this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) { - if(self.get("actual_mode") == "view") { - var $button = self.options.$buttons.find(".oe_form_button_edit"); - $button.openerpBounce(); - e.stopPropagation(); - instance.web.bus.trigger('click', e); - } - }); - //bounce effect on red button when click on statusbar. - this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) { - if((self.get("actual_mode") == "view")) { - var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'}); - $button.openerpBounce(); - e.stopPropagation(); - } - }); - this.trigger('form_view_loaded', data); - return $.when(); - }, - 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 {Object} [options] - * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view". - * @param {Boolean} [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 || {}; - if (this.sidebar) { - this.sidebar.$el.show(); - } - if (this.$buttons) { - this.$buttons.show(); - } - this.$el.show().css({ - opacity: '0', - filter: 'alpha(opacity = 0)' - }); - this.$el.add(this.$buttons).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'); - return self.dataset.read_index(fields, { - context: { 'bin_size': true, 'future_display_name' : 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); - self.$el.css({ - opacity: '1', - filter: 'alpha(opacity = 100)' - }); - instance.web.bus.trigger('form_view_shown', self); - }); - }, - do_hide: function () { - if (this.sidebar) { - this.sidebar.$el.hide(); - } - if (this.$pager) { - this.$pager.hide(); - } - this._super(); - }, - 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._actualize_mode(); - this.set({ 'title' : record.id ? record.display_name : _t("New") }); - - _(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); - }); - 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(); - self.is_initialized.resolve(); - self.do_update_pager(record.id === null || record.id === undefined); - 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.add(self.$buttons).removeClass('oe_form_dirty'); - self.autofocus(); - }); - }, - /** - * 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', r); - }); - } - return self.trigger('load_record', {}); - }, - on_form_changed: function() { - this.trigger("view_content_has_changed"); - }, - do_notify_change: function() { - this.$el.add(this.$buttons).addClass('oe_form_dirty'); - }, - execute_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; - } - var def = this.reload(); - this.trigger('pager_action_executed'); - return def; - } - return $.when(); - }, - init_pager: function() { - var self = this; - if (this.$pager) - this.$pager.remove(); - if (this.get("actual_mode") === "create") - return; - this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); - if (this.options.$pager) { - this.$pager.appendTo(this.options.$pager); - } else { - this.$el.find('.oe_form_pager').replaceWith(this.$pager); - } - this.$pager.on('click','a[data-pager-action]',function() { - var $el = $(this); - if ($el.attr("disabled")) - return; - var action = $el.data('pager-action'); - var def = $.when(self.execute_pager_action(action)); - $el.attr("disabled"); - def.always(function() { - $el.removeAttr("disabled"); - }); - }); - this.do_update_pager(); - }, - do_update_pager: function(hide_index) { - this.$pager.toggle(this.dataset.ids.length > 1); - if (hide_index) { - $(".oe_form_pager_state", this.$pager).html(""); - } else { - $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length)); - } - }, - - _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(instance.web.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 instance.web.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 && !instance.web.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(new instance.web.Model(self.dataset.model).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 instance.web.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<results.length; ++i) { - // [whatever, key, value] - var triplet = results[i]; - response.value[triplet[1]] = triplet[2]; - } - return response; - })); - } - } - }); - return _.isEmpty(defs) ? response : $.when.apply(null, defs); - }).then(function(response) { - return self.on_processed_onchange(response); - }); - }); - return this.onchanges_mutex.def; - } catch(e) { - console.error(e); - instance.webclient.crashmanager.show_message(e); - return $.Deferred().reject(); - } - }, - on_processed_onchange: function(result) { - try { - var fields = this.fields; - _(result.domain).each(function (domain, fieldname) { - var field = fields[fieldname]; - if (!field) { return; } - field.node.attrs.domain = domain; - }); - - if (!_.isEmpty(result.value)) { - this._internal_set_values(result.value); - } - // FIXME XXX a list of warnings? - if (!_.isEmpty(result.warning)) { - new instance.web.Dialog(this, { - size: 'medium', - title:result.warning.title, - buttons: [ - {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }} - ] - }, QWeb.render("CrashManager.warning", result.warning)).open(); - } - - return $.Deferred().resolve(); - } catch(e) { - console.error(e); - instance.webclient.crashmanager.show_message(e); - return $.Deferred().reject(); - } - }, - _process_operations: function() { - var self = this; - return this.mutating_mutex.exec(function() { - function iterate() { - - var mutex = new $.Mutex(); - _.each(self.fields, function(field) { - self.onchanges_mutex.def.then(function(){ - mutex.exec(function(){ - return field.commit_value(); - }); - }); - }); - - var args = _.toArray(arguments); - return $.when.apply(null, [mutex.def, self.onchanges_mutex.def]).then(function() { - var save_obj = self.save_list.pop(); - if (save_obj) { - return self._process_save(save_obj).then(function() { - save_obj.ret = _.toArray(arguments); - return iterate(); - }, function() { - save_obj.error = true; - }); - } - return $.when(); - }).fail(function() { - self.save_list.pop(); - return $.when(); - }); - } - return iterate(); - }); - }, - _internal_set_values: function(values) { - for (var f in values) { - if (!values.hasOwnProperty(f)) { continue; } - var field = this.fields[f]; - // If field is not defined in the view, just ignore it - if (field) { - var value_ = values[f]; - if (field.get_value() != value_) { - field._inhibit_on_change_flag = true; - field.set_value(value_); - field._inhibit_on_change_flag = false; - field._dirty_flag = true; - } - } - } - this.on_form_changed(); - }, - set_values: function(values) { - var self = this; - return this.mutating_mutex.exec(function() { - self._internal_set_values(values); - }); - }, - /** - * Ask the view to switch to view mode if possible. The view may not do it - * if the current record is not yet saved. It will then stay in create mode. - */ - to_view_mode: function() { - this._actualize_mode("view"); - }, - /** - * Ask the view to switch to edit mode if possible. The view may not do it - * if the current record is not yet saved. It will then stay in create mode. - */ - to_edit_mode: function() { - this.onchanges_mutex = new $.Mutex(); - this._actualize_mode("edit"); - }, - /** - * Ask the view to switch to a precise mode if possible. The view is free to - * not respect this command if the state of the dataset is not compatible with - * the new mode. For example, it is not possible to switch to edit mode if - * the current record is not yet saved in database. - * - * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If - * undefined the view will test the actual mode to check if it is still consistent - * with the dataset state. - */ - _actualize_mode: function(switch_to) { - var mode = switch_to || this.get("actual_mode"); - if (! this.datarecord.id) { - mode = "create"; - } else if (mode === "create") { - mode = "edit"; - } - this.render_value_defs = []; - this.set({actual_mode: mode}); - }, - check_actual_mode: function(source, options) { - var self = this; - if(this.get("actual_mode") === "view") { - self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly'); - self.$buttons.find('.oe_form_buttons_edit').hide(); - self.$buttons.find('.oe_form_buttons_view').show(); - self.$sidebar.show(); - } else { - self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable'); - self.$buttons.find('.oe_form_buttons_edit').show(); - self.$buttons.find('.oe_form_buttons_view').hide(); - self.$sidebar.hide(); - this.autofocus(); - } - }, - autofocus: function() { - if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) { - var fields_order = this.fields_order.slice(0); - if (this.default_focus_field) { - fields_order.unshift(this.default_focus_field.name); - } - for (var i = 0; i < fields_order.length; i += 1) { - var field = this.fields[fields_order[i]]; - if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) { - if (field.focus() !== false) { - break; - } - } - } - } - }, - on_button_save: function(e) { - var self = this; - $(e.target).attr("disabled", true); - return this.save().done(function(result) { - self.trigger("save", result); - self.reload().then(function() { - self.to_view_mode(); - var menu = instance.webclient.menu; - if (menu) { - menu.do_reload_needaction(); - } - instance.web.bus.trigger('form_view_saved', self); - }); - }).always(function(){ - $(e.target).attr("disabled", false); - }); - }, - on_button_cancel: function(event) { - var self = this; - if (this.can_be_discarded()) { - if (this.get('actual_mode') === 'create') { - this.trigger('history_back'); - } else { - this.to_view_mode(); - $.when.apply(null, this.render_value_defs).then(function(){ - self.trigger('load_record', self.datarecord); - }); - } - } - this.trigger('on_button_cancel'); - return false; - }, - on_button_new: function() { - var self = this; - this.to_edit_mode(); - return $.when(this.has_been_loaded).then(function() { - if (self.can_be_discarded()) { - return self.load_defaults(); - } - }); - }, - on_button_edit: function() { - return this.to_edit_mode(); - }, - on_button_create: function() { - this.dataset.index = null; - this.do_show(); - }, - on_button_duplicate: function() { - var self = this; - return this.has_been_loaded.then(function() { - return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { - self.record_created(new_id); - self.to_edit_mode(); - }); - }); - }, - on_button_delete: function() { - var self = this; - var def = $.Deferred(); - this.has_been_loaded.done(function() { - if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) { - self.dataset.unlink([self.datarecord.id]).done(function() { - if (self.dataset.size()) { - self.execute_pager_action('next'); - } else { - self.do_action('history_back'); - } - def.resolve(); - }); - } else { - $.async_when().done(function () { - def.reject(); - }); - } - }); - return def.promise(); - }, - can_be_discarded: function() { - if (this.$el.is('.oe_form_dirty')) { - if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) { - return false; - } - this.$el.removeClass('oe_form_dirty'); - } - return true; - }, - /** - * 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 {Boolean} [prepend_on_create=false] if ``save`` creates a new - * record, should that record be inserted at the start of the dataset (by - * default, records are added at the end) - */ - save: function(prepend_on_create) { - var self = this; - var save_obj = {prepend_on_create: prepend_on_create, ret: null}; - this.save_list.push(save_obj); - return self._process_operations().then(function() { - if (save_obj.error) - return $.Deferred().reject(); - return $.when.apply($, save_obj.ret); - }).done(function(result) { - self.$el.removeClass('oe_form_dirty'); - }); - }, - _process_save: function(save_obj) { - var self = this; - var prepend_on_create = save_obj.prepend_on_create; - try { - var form_invalid = false, - values = {}, - first_invalid_field = null, - readonly_values = {}; - for (var f in self.fields) { - if (!self.fields.hasOwnProperty(f)) { continue; } - f = self.fields[f]; - if (!f.is_valid()) { - form_invalid = true; - if (!first_invalid_field) { - first_invalid_field = f; - } - } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) { - // Special case 'id' field, do not save this field - // on 'create' : save all non readonly fields - // on 'edit' : save non readonly modified fields - if (!f.get("readonly")) { - values[f.name] = f.get_value(true); - } else { - readonly_values[f.name] = f.get_value(true); - } - } - } - if (form_invalid) { - self.set({'display_invalid_fields': true}); - first_invalid_field.focus(); - self.on_invalid(); - return $.Deferred().reject(); - } else { - self.set({'display_invalid_fields': false}); - var save_deferral; - if (!self.datarecord.id) { - // Creation save - save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) { - return self.record_created(r, prepend_on_create); - }, null); - } else if (_.isEmpty(values)) { - // Not dirty, noop save - save_deferral = $.Deferred().resolve({}).promise(); - } else { - // Write save - save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) { - return self.record_saved(r); - }, null); - } - return save_deferral; - } - } catch (e) { - console.error(e); - return $.Deferred().reject(); - } - }, - on_invalid: function() { - var warnings = _(this.fields).chain() - .filter(function (f) { return !f.is_valid(); }) - .map(function (f) { - return _.str.sprintf('<li>%s</li>', - _.escape(f.string)); - }).value(); - warnings.unshift('<ul>'); - warnings.push('</ul>'); - this.do_warn(_t("The following fields are invalid:"), warnings.join('')); - }, - /** - * Reload the form after saving - * - * @param {Object} r result of the write function. - */ - record_saved: function(r) { - this.trigger('record_saved', r); - if (!r) { - // should not happen in the server, but may happen for internal purpose - return $.Deferred().reject(); - } - return r; - }, - /** - * 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 {Boolean} [prepend_on_create=false] adds the newly created record - * at the beginning of the dataset instead of the end - */ - record_created: function(r, prepend_on_create) { - var self = this; - if (!r) { - // should not happen in the server, but may happen for internal purpose - this.trigger('record_created', r); - return $.Deferred().reject(); - } else { - this.datarecord.id = r; - 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.do_attachement_update(this.dataset, this.datarecord.id); - } - //openerp.log("The record has been created with id #" + this.datarecord.id); - return $.when(this.reload()).then(function () { - self.trigger('record_created', r); - return _.extend(r, {created: true}); - }); - } - }, - 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 === undefined) { - self.trigger("previous_view"); - return $.Deferred().reject().promise(); - } - if (self.dataset.index < 0) { - return $.when(self.on_button_new()); - } else { - var fields = _.keys(self.fields_view.fields); - fields.push('display_name'); - return self.dataset.read_index(fields, - { - context: { - 'bin_size': true, - 'future_display_name': true - }, - check_access_rule: true - }).then(function(r) { - self.trigger('load_record', r); - }).fail(function (){ - self.do_action('history_back'); - }); - } - }); - }, - get_widgets: function() { - return _.filter(this.getChildren(), function(obj) { - return obj instanceof instance.web.form.FormWidget; - }); - }, - get_fields_values: function() { - var values = {}; - var ids = this.get_selected_ids(); - values["id"] = ids.length > 0 ? ids[0] : false; - _.each(this.fields, function(value_, key) { - values[key] = value_.get_value(); - }); - 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.save()).then(function(res) { - if (self.dataset.parent_view) - return self.dataset.parent_view.recursive_save(); - }); - }, - recursive_reload: function() { - var self = this; - var pre = $.when(); - if (self.dataset.parent_view) - pre = self.dataset.parent_view.recursive_reload(); - return pre.then(function() { - return self.reload(); - }); - }, - is_dirty: function() { - return _.any(this.fields, function (value_) { - return value_._dirty_flag; - }); - }, - 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(instance.web.BufferedDataSet.virtual_id_regex.test(id)) - return false; - } - return true; - }, - sidebar_eval_context: function () { - return $.when(this.build_eval_context()); - }, - open_defaults_dialog: function () { - var self = this; - var display = function (field, value) { - if (!value) { return value; } - if (field instanceof instance.web.form.FieldSelection) { - return _(field.get('values')).find(function (option) { - return option[0] === value; - })[1]; - } else if (field instanceof instance.web.form.FieldMany2One) { - return field.get_displayed(); - } - return value; - }; - var fields = _.chain(this.fields) - .map(function (field) { - var value = field.get_value(); - // ignore fields which are empty, invisible, readonly, o2m - // or m2m - if (!value - || field.get('invisible') - || field.get("readonly") - || field.field.type === 'one2many' - || field.field.type === 'many2many' - || field.field.type === 'binary' - || field.password) { - return false; - } - - return { - name: field.name, - string: field.string, - value: value, - displayed: display(field, value), - }; - }) - .compact() - .sortBy(function (field) { return field.string; }) - .value(); - var conditions = _.chain(self.fields) - .filter(function (field) { return field.field.change_default; }) - .map(function (field) { - var value = field.get_value(); - return { - name: field.name, - string: field.string, - value: value, - displayed: display(field, value), - }; - }) - .value(); - var d = new instance.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.$el.find('#formview_default_fields'); - var field_to_set = $defaults.val(); - if (!field_to_set) { - $defaults.parent().addClass('oe_form_invalid'); - return; - } - var condition = d.$el.find('#formview_default_conditions').val(), - all_users = d.$el.find('#formview_default_all').is(':checked'); - new instance.web.DataSet(self, 'ir.values').call( - 'set_default', [ - self.dataset.model, - field_to_set, - self.fields[field_to_set].get_value(), - all_users, - true, - condition || false - ]).done(function () { d.close(); }); - }} - ] - }); - d.template = 'FormView.set_default'; - d.open(); - }, - register_field: function(field, name) { - this.fields[name] = field; - this.fields_order.push(name); - if (JSON.parse(field.node.attrs.default_focus || "0")) { - this.default_focus_field = field; - } - - field.on('focused', null, this.proxy('widgetFocused')) - .on('blurred', null, this.proxy('widgetBlurred')); - if (this.get_field_desc(name).translate) { - this.translatable_fields.push(field); - } - field.on('changed_value', this, function() { - if (field.is_syntax_valid()) { - this.trigger('field_changed:' + name); - } - if (field._inhibit_on_change_flag) { - return; - } - field._dirty_flag = true; - if (field.is_syntax_valid()) { - this.do_onchange(field); - this.on_form_changed(true); - this.do_notify_change(); - } - }); - }, - get_field_desc: function(field_name) { - return this.fields_view.fields[field_name]; - }, - get_field_value: function(field_name) { - return this.fields[field_name].get_value(); - }, - compute_domain: function(expression) { - return instance.web.form.compute_domain(expression, this.fields); - }, - _build_view_fields_values: function() { - var a_dataset = this.dataset; - var fields_values = this.get_fields_values(); - var active_id = a_dataset.ids[a_dataset.index]; - _.extend(fields_values, { - active_id: active_id || false, - active_ids: active_id ? [active_id] : [], - active_model: a_dataset.model, - parent: {} - }); - if (a_dataset.parent_view) { - fields_values.parent = a_dataset.parent_view.get_fields_values(); - } - return fields_values; - }, - build_eval_context: function() { - var a_dataset = this.dataset; - return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values()); - }, -}); - -/** - * Interface to be implemented by rendering engines for the form view. - */ -instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({ - set_fields_view: function(fields_view) {}, - set_fields_registry: function(fields_registry) {}, - render_to: function($el) {}, -}); - -/** - * Default rendering engine for the form view. - * - * It is necessary to set the view using set_view() before usage. - */ -instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({ - init: function(view) { - this.view = view; - }, - set_fields_view: function(fvg) { - this.fvg = fvg; - this.version = parseFloat(this.fvg.arch.attrs.version); - if (isNaN(this.version)) { - this.version = 7.0; - } - }, - set_tags_registry: function(tags_registry) { - this.tags_registry = tags_registry; - }, - set_fields_registry: function(fields_registry) { - this.fields_registry = fields_registry; - }, - set_widgets_registry: function(widgets_registry) { - this.widgets_registry = widgets_registry; - }, - // Backward compatibility tools, current default version: v7 - process_version: function() { - if (this.version < 7.0) { - this.$form.find('form:first').wrapInner('<group col="4"/>'); - this.$form.find('page').each(function() { - if (!$(this).parents('field').length) { - $(this).wrapInner('<group col="4"/>'); - } - }); - } - }, - get_arch_fragment: function() { - var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement; - // IE won't allow custom button@type and will revert it to spec default : 'submit' - $('button', doc).each(function() { - $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button'); - }); - // IE's html parser is also a css parser. How convenient... - $('board', doc).each(function() { - $(this).attr('layout', $(this).attr('style')); - }); - return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc)); - }, - render_to: function($target) { - var self = this; - this.$target = $target; - - this.$form = this.get_arch_fragment(); - - this.process_version(); - - this.fields_to_init = []; - this.tags_to_init = []; - this.widgets_to_init = []; - this.labels = {}; - this.process(this.$form); - - this.$form.appendTo(this.$target); - - this.to_replace = []; - - _.each(this.fields_to_init, function($elem) { - var name = $elem.attr("name"); - if (!self.fvg.fields[name]) { - throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name)); - } - var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]); - if (!obj) { - throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget'))); - } - var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); - var $label = self.labels[$elem.attr("name")]; - if ($label) { - w.set_input_id($label.attr("for")); - } - self.alter_field(w); - self.view.register_field(w, $elem.attr("name")); - self.to_replace.push([w, $elem]); - }); - _.each(this.tags_to_init, function($elem) { - var tag_name = $elem[0].tagName.toLowerCase(); - var obj = self.tags_registry.get_object(tag_name); - var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); - self.to_replace.push([w, $elem]); - }); - _.each(this.widgets_to_init, function($elem) { - var widget_type = $elem.attr("type"); - var obj = self.widgets_registry.get_object(widget_type); - var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); - self.to_replace.push([w, $elem]); - }); - }, - init_fields: function() { - var defs = []; - _.each(this.to_replace, function(el) { - defs.push(el[0].replace(el[1])); - if (el[1].children().length) { - el[0].$el.append(el[1].children()); - } - }); - this.to_replace = []; - return $.when.apply($, defs); - }, - render_element: function(template /* dictionaries */) { - var dicts = [].slice.call(arguments).slice(1); - var dict = _.extend.apply(_, dicts); - dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb - return $(QWeb.render(template, dict)); - }, - alter_field: function(field) { - }, - toggle_layout_debugging: function() { - if (!this.$target.has('.oe_layout_debug_cell:first').length) { - this.$target.find('[title]').removeAttr('title'); - this.$target.find('.oe_form_group_cell').each(function() { - var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan'); - $(this).attr('title', text); - }); - } - this.$target.toggleClass('oe_layout_debugging'); - }, - process: function($tag) { - var self = this; - var tagname = $tag[0].nodeName.toLowerCase(); - if (this.tags_registry.contains(tagname)) { - this.tags_to_init.push($tag); - return (tagname === 'button') ? this.process_button($tag) : $tag; - } - var fn = self['process_' + 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)); - }); - self.handle_common_properties($tag, $tag); - $tag.removeAttr("modifiers"); - return $tag; - } - }, - process_button: function ($button) { - var self = this; - $button.children().each(function() { - self.process($(this)); - }); - return $button; - }, - process_widget: function($widget) { - this.widgets_to_init.push($widget); - return $widget; - }, - process_sheet: function($sheet) { - var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes()); - this.handle_common_properties($new_sheet, $sheet); - var $dst = $new_sheet.find('.oe_form_sheet'); - $sheet.contents().appendTo($dst); - $sheet.before($new_sheet).remove(); - this.process($new_sheet); - }, - process_form: function($form) { - if ($form.find('> sheet').length === 0) { - $form.addClass('oe_form_nosheet'); - } - var $new_form = this.render_element('FormRenderingForm', $form.getAttributes()); - this.handle_common_properties($new_form, $form); - $form.contents().appendTo($new_form); - if ($form[0] === this.$form[0]) { - // If root element, replace it - this.$form = $new_form; - } else { - $form.before($new_form).remove(); - } - this.process($new_form); - }, - /* - * Used by direct <field> children of a <group> tag only - * This method will add the implicit <label...> for every field - * in the <group> - */ - preprocess_field: function($field) { - var self = this; - var name = $field.attr('name'), - field_colspan = parseInt($field.attr('colspan'), 10), - field_modifiers = JSON.parse($field.attr('modifiers') || '{}'); - - if ($field.attr('nolabel') === '1') - return; - $field.attr('nolabel', '1'); - var found = false; - this.$form.find('label[for="' + name + '"]').each(function(i ,el) { - $(el).parents().each(function(unused, tag) { - var name = tag.tagName.toLowerCase(); - if (name === "field" || name in self.tags_registry.map) - found = true; - }); - }); - if (found) - return; - - var $label = $('<label/>').attr({ - 'for' : name, - "modifiers": JSON.stringify({invisible: field_modifiers.invisible}), - "string": $field.attr('string'), - "help": $field.attr('help'), - "class": $field.attr('class'), - }); - $label.insertBefore($field); - if (field_colspan > 1) { - $field.attr('colspan', field_colspan - 1); - } - return $label; - }, - process_field: function($field) { - if ($field.parent().is('group')) { - // No implicit labels for normal fields, only for <group> direct children - var $label = this.preprocess_field($field); - if ($label) { - this.process($label); - } - } - this.fields_to_init.push($field); - return $field; - }, - process_group: function($group) { - var self = this; - $group.children('field').each(function() { - self.preprocess_field($(this)); - }); - var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes()); - var $table; - if ($new_group.first().is('table.oe_form_group')) { - $table = $new_group; - } else if ($new_group.filter('table.oe_form_group').length) { - $table = $new_group.filter('table.oe_form_group').first(); - } else { - $table = $new_group.find('table.oe_form_group').first(); - } - - var $tr, $td, - cols = parseInt($group.attr('col') || 2, 10), - row_cols = cols; - - var children = []; - $group.children().each(function(a,b,c) { - var $child = $(this); - var colspan = parseInt($child.attr('colspan') || 1, 10); - var tagName = $child[0].tagName.toLowerCase(); - var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan); - var newline = tagName === 'newline'; - - // Note FME: those classes are used in layout debug mode - if ($tr && row_cols > 0 && (newline || row_cols < colspan)) { - $tr.addClass('oe_form_group_row_incomplete'); - if (newline) { - $tr.addClass('oe_form_group_row_newline'); - } - } - if (newline) { - $tr = null; - return; - } - if (!$tr || row_cols < colspan) { - $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table); - row_cols = cols; - } else if (tagName==='group') { - // When <group> <group/><group/> </group>, we need a spacing between the two groups - $td.addClass('oe_group_right'); - } - row_cols -= colspan; - - // invisibility transfer - var field_modifiers = JSON.parse($child.attr('modifiers') || '{}'); - var invisible = field_modifiers.invisible; - self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible}))); - - $tr.append($td.append($child)); - children.push($child[0]); - }); - if (row_cols && $td) { - $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols); - } - $group.before($new_group).remove(); - - $table.find('> tbody > tr').each(function() { - var to_compute = [], - row_cols = cols, - total = 100; - $(this).children().each(function() { - var $td = $(this), - $child = $td.children(':first'); - if ($child.attr('cell-class')) { - $td.addClass($child.attr('cell-class')); - } - switch ($child[0].tagName.toLowerCase()) { - case 'separator': - break; - case 'label': - if ($child.attr('for')) { - $td.attr('width', '1%').addClass('oe_form_group_cell_label'); - row_cols-= $td.attr('colspan') || 1; - total--; - } - break; - default: - var width = _.str.trim($child.attr('width') || ''), - iwidth = parseInt(width, 10); - if (iwidth) { - if (width.substr(-1) === '%') { - total -= iwidth; - width = iwidth + '%'; - } else { - // Absolute width - $td.css('min-width', width + 'px'); - } - $td.attr('width', width); - $child.removeAttr('width'); - row_cols-= $td.attr('colspan') || 1; - } else { - to_compute.push($td); - } - - } - }); - if (row_cols) { - var unit = Math.floor(total / row_cols); - if (!$(this).is('.oe_form_group_row_incomplete')) { - _.each(to_compute, function($td, i) { - var width = parseInt($td.attr('colspan'), 10) * unit; - $td.attr('width', width + '%'); - total -= width; - }); - } - } - }); - _.each(children, function(el) { - self.process($(el)); - }); - this.handle_common_properties($new_group, $group); - return $new_group; - }, - process_notebook: function($notebook) { - var self = this; - - // Extract useful info from the notebook xml declaration - var pages = []; - $notebook.find('> page').each(function() { - var $page = $(this); - var page_attrs = $page.getAttributes(); - page_attrs.id = _.uniqueId('notebook_page_'); - page_attrs.contents = $page.html(); - page_attrs.ref = $page; // reference to the current page node in the xml declaration - pages.push(page_attrs); - }); - - // Render the notebook and replace $notebook with it - var $new_notebook = self.render_element('FormRenderingNotebook', {'pages': pages}); - $notebook.before($new_notebook).remove(); - - // Bind the invisibility changers and find the page to display - var pageid_to_display; - _.each(pages, function(page) { - var $tab = $new_notebook.find('a[href=#' + page.id + ']').parent(); - var $content = $new_notebook.find('#' + page.id); - - // Case: <page autofocus="autofocus">; - // We attach the autofocus attribute to the node because it can be useful during the - // page execution - self.attach_node_attr($tab, page.ref, 'autofocus'); - self.attach_node_attr($content, page.ref, 'autofocus'); - if (!pageid_to_display && page.autofocus) { - // If multiple autofocus, keep the first one - pageid_to_display = page.id; - } - - // Case: <page attrs="{'invisible': domain}">; - self.handle_common_properties($tab, page.ref, instance.web.form.NotebookInvisibilityChanger); - self.handle_common_properties($content, page.ref, instance.web.form.NotebookInvisibilityChanger); - }); - if (!pageid_to_display) { - pageid_to_display = $new_notebook.find('div[role="tabpanel"]:not(.oe_form_invisible):first').attr('id'); - } - - // Display page. Note: we can't use bootstrap's show function because it is looking for - // document attached DOM, and the form view is only attached when everything is processed - $new_notebook.find('a[href=#' + pageid_to_display + ']').parent().addClass('active'); - $new_notebook.find('#' + pageid_to_display).addClass('active'); - - self.process($new_notebook.children()); - self.handle_common_properties($new_notebook, $notebook); - - return $new_notebook; - }, - process_separator: function($separator) { - var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes()); - $separator.before($new_separator).remove(); - this.handle_common_properties($new_separator, $separator); - return $new_separator; - }, - process_label: function($label) { - var name = $label.attr("for"), - field_orm = this.fvg.fields[name]; - var dict = { - string: $label.attr('string') || (field_orm || {}).string || '', - help: $label.attr('help') || (field_orm || {}).help || '', - _for: name ? _.uniqueId('oe-field-input-') : undefined, - }; - var align = parseFloat(dict.align); - if (isNaN(align) || align === 1) { - align = 'right'; - } else if (align === 0) { - align = 'left'; - } else { - align = 'center'; - } - dict.align = align; - var $new_label = this.render_element('FormRenderingLabel', dict); - $label.before($new_label).remove(); - this.handle_common_properties($new_label, $label); - if (name) { - this.labels[name] = $new_label; - } - return $new_label; - }, - handle_common_properties: function($new_element, $node, InvisibilityChanger) { - var str_modifiers = $node.attr("modifiers") || "{}"; - var modifiers = JSON.parse(str_modifiers); - var ic = null; - if (modifiers.invisible !== undefined) { - var InvisibilityChangerCls = InvisibilityChanger || instance.web.form.InvisibilityChanger; - ic = new InvisibilityChangerCls(this.view, this.view, modifiers.invisible, $new_element); - } - $new_element.addClass($node.attr("class") || ""); - $new_element.attr('style', $node.attr('style')); - return {invisibility_changer: ic,}; - }, - /** - * Attach a node attribute to an html element to be able to use it after - * the processing of the page. - */ - attach_node_attr: function($new_element, $node, attr) { - $new_element.data(attr, $node.attr(attr)); - } -}); - -/** - Welcome. - - If you read this documentation, it probably means that you were asked to use a form view widget outside of - a form view. Before going further, you must understand that those fields were never really created for - that usage. Don't think that this class will hold the answer to all your problems, at best it will allow - you to hack the system with more style. -*/ -instance.web.form.DefaultFieldManager = instance.web.Widget.extend({ - init: function(parent, eval_context) { - this._super(parent); - this.field_descs = {}; - this.eval_context = eval_context || {}; - this.set({ - display_invalid_fields: false, - actual_mode: 'create', - }); - }, - get_field_desc: function(field_name) { - if (this.field_descs[field_name] === undefined) { - this.field_descs[field_name] = { - string: field_name, - }; - } - return this.field_descs[field_name]; - }, - extend_field_desc: function(fields) { - var self = this; - _.each(fields, function(v, k) { - _.extend(self.get_field_desc(k), v); - }); - }, - get_field_value: function(field_name) { - return false; - }, - set_values: function(values) { - // nothing - }, - compute_domain: function(expression) { - return instance.web.form.compute_domain(expression, {}); - }, - build_eval_context: function() { - return new instance.web.CompoundContext(this.eval_context); - }, -}); - -instance.web.form.compute_domain = function(expr, fields) { - if (! (expr instanceof Array)) - return !! expr; - 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(_.str.sprintf( - _t("Unknown operator %s in domain %s"), - ex, JSON.stringify(expr))); - } - } - - var field = fields[ex[0]]; - if (!field) { - throw new Error(_.str.sprintf( - _t("Unknown field %s in domain %s"), - ex[0], JSON.stringify(expr))); - } - var field_value = field.get_value ? field.get_value() : field.value; - var op = ex[1]; - var val = ex[2]; - - switch (op.toLowerCase()) { - case '=': - case '==': - stack.push(_.isEqual(field_value, val)); - break; - case '!=': - case '<>': - stack.push(!_.isEqual(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': - if (!_.isArray(val)) val = [val]; - stack.push(_(val).contains(field_value)); - break; - case 'not in': - if (!_.isArray(val)) val = [val]; - stack.push(!_(val).contains(field_value)); - break; - default: - console.warn( - _t("Unsupported operator %s in domain %s"), - op, JSON.stringify(expr)); - } - } - return _.all(stack, _.identity); -}; - -instance.web.form.is_bin_size = function(v) { - return (/^\d+(\.\d*)? \w+$/).test(v); -}; - -/** - * Must be applied over an class already possessing the PropertiesMixin. - * - * Apply the result of the "invisible" domain to this.$el. - */ -instance.web.form.InvisibilityChangerMixin = { - init: function(field_manager, invisible_domain) { - var self = this; - this._ic_field_manager = field_manager; - this._ic_invisible_modifier = invisible_domain; - this._ic_field_manager.on("view_content_has_changed", this, function() { - var result = self._ic_invisible_modifier === undefined ? false : - self._ic_field_manager.compute_domain(self._ic_invisible_modifier); - self.set({"invisible": result}); - }); - this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false}); - var check = function() { - if (self.get("invisible") || self.get('force_invisible')) { - self.set({"effective_invisible": true}); - } else { - self.set({"effective_invisible": false}); - } - }; - this.on('change:invisible', this, check); - this.on('change:force_invisible', this, check); - check.call(this); - }, - start: function() { - this.on("change:effective_invisible", this, this._check_visibility); - this._check_visibility(); - }, - _check_visibility: function() { - this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible")); - }, -}; - -instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, { - init: function(parent, field_manager, invisible_domain, $el) { - this.setParent(parent); - instance.web.PropertiesMixin.init.call(this); - instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain); - this.$el = $el; - this.start(); - }, -}); - -// Specialization of InvisibilityChanger for the `notebook` form element to handle more -// elegantly its special cases (i.e. activate the closest visible sibling) -instance.web.form.NotebookInvisibilityChanger = instance.web.form.InvisibilityChanger.extend({ - // Override start so that it does not call _check_visibility since it will be - // called again when view content will be loaded (event view_content_has_changed) - start: function() { - this.on("change:effective_invisible", this, this._check_visibility); - }, - _check_visibility: function() { - this._super(); - if (this.get("effective_invisible") === true) { - // Switch to invisible - // Remove this element as active and set a visible sibling active (if there is one) - if (this.$el.hasClass('active')) { - this.$el.removeClass('active'); - var visible_siblings = this.$el.siblings(':not(.oe_form_invisible)'); - if (visible_siblings.length) { - $(visible_siblings[0]).addClass('active'); - } - } - } else { - // Switch to visible - // If there is no visible active sibling, set this element as active, - // otherwise if that sibling hasn't autofocus and if we are in edit mode, - // remove that sibling as active and set this element as active - var visible_active_sibling = this.$el.siblings(':not(.oe_form_invisible).active'); - if (!(visible_active_sibling.length)) { - this.$el.addClass('active'); - } else if (!$(visible_active_sibling[0]).data('autofocus') && this._ic_field_manager.get('actual_mode') === "edit") { - this.$el.addClass('active'); - $(visible_active_sibling[0]).removeClass('active'); - } - } - }, -}); - - -/** - Base class for all fields, custom widgets and buttons to be displayed in the form view. - - Properties: - - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending - the values of the "readonly" property and the "mode" property on the field manager. -*/ -instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, { - /** - * @constructs instance.web.form.FormWidget - * @extends instance.web.Widget - * - * @param field_manager - * @param node - */ - init: function(field_manager, node) { - this._super(field_manager); - this.field_manager = field_manager; - if (this.field_manager instanceof instance.web.FormView) - this.view = this.field_manager; - this.node = node; - this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}'); - instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible); - - this.field_manager.on("view_content_has_changed", this, this.process_modifiers); - - this.set({ - required: false, - readonly: false, - }); - // some events to make the property "effective_readonly" sync automatically with "readonly" and - // "mode" on field_manager - var self = this; - var test_effective_readonly = function() { - self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"}); - }; - this.on("change:readonly", this, test_effective_readonly); - this.field_manager.on("change:actual_mode", this, test_effective_readonly); - test_effective_readonly.call(this); - }, - renderElement: function() { - this.process_modifiers(); - this._super(); - this.$el.addClass(this.node.attrs["class"] || ""); - }, - destroy: function() { - $.fn.tooltip('destroy'); - this._super.apply(this, arguments); - }, - /** - * Sets up blur/focus forwarding from DOM elements to a widget (`this`). - * - * This method is an utility method that is meant to be called by child classes. - * - * @param {jQuery} $e jQuery object of elements to bind focus/blur on - */ - setupFocus: function ($e) { - var self = this; - $e.on({ - focus: function () { self.trigger('focused'); }, - blur: function () { self.trigger('blurred'); } - }); - }, - process_modifiers: function() { - var to_set = {}; - for (var a in this.modifiers) { - if (!this.modifiers.hasOwnProperty(a)) { continue; } - if (!_.include(["invisible"], a)) { - var val = this.field_manager.compute_domain(this.modifiers[a]); - to_set[a] = val; - } - } - this.set(to_set); - }, - do_attach_tooltip: function(widget, trigger, options) { - widget = widget || this; - trigger = trigger || this.$el; - options = _.extend({ - delay: { show: 500, hide: 0 }, - title: function() { - var template = widget.template + '.tooltip'; - if (!QWeb.has_template(template)) { - template = 'WidgetLabel.tooltip'; - } - return QWeb.render(template, { - debug: instance.session.debug, - widget: widget - }); - }, - }, options || {}); - //only show tooltip if we are in debug or if we have a help to show, otherwise it will display - //as empty - if (instance.session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){ - $(trigger).tooltip(options); - } - }, - /** - * Builds a new context usable for operations related to fields by merging - * the fields'context with the action's context. - */ - build_context: function() { - // only use the model's context if there is not context on the node - var v_context = this.node.attrs.context; - if (! v_context) { - v_context = (this.field || {}).context || {}; - } - - if (v_context.__ref || true) { //TODO: remove true - var fields_values = this.field_manager.build_eval_context(); - v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values); - } - return v_context; - }, - build_domain: function() { - var f_domain = this.field.domain || []; - var n_domain = this.node.attrs.domain || null; - // if there is a domain on the node, overrides the model's domain - var final_domain = n_domain !== null ? n_domain : f_domain; - if (!(final_domain instanceof Array) || true) { //TODO: remove true - var fields_values = this.field_manager.build_eval_context(); - final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values); - } - return final_domain; - } -}); - -instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ - template: 'WidgetButton', - init: function(field_manager, node) { - node.attrs.type = node.attrs['data-button-type']; - this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']); - this.icon_class = node.attrs.icon && "stat_button_icon fa " + node.attrs.icon + " fa-fw"; - this._super(field_manager, node); - this.force_disabled = false; - this.string = (this.node.attrs.string || '').replace(/_/g, ''); - if (JSON.parse(this.node.attrs.default_focus || "0")) { - // TODO fme: provide enter key binding to widgets - this.view.default_focus_button = this; - } - if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) { - this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png'; - } - }, - start: function() { - this._super.apply(this, arguments); - this.view.on('view_content_has_changed', this, this.check_disable); - this.check_disable(); - this.$el.click(this.on_click); - if (this.node.attrs.help || instance.session.debug) { - this.do_attach_tooltip(); - } - this.setupFocus(this.$el); - }, - on_click: function() { - var self = this; - this.force_disabled = true; - this.check_disable(); - this.execute_action().always(function() { - self.force_disabled = false; - self.check_disable(); - }); - }, - execute_action: function() { - var self = this; - var exec_action = function() { - if (self.node.attrs.confirm) { - var def = $.Deferred(); - var dialog = new instance.web.Dialog(this, { - title: _t('Confirm'), - buttons: [ - {text: _t("Cancel"), click: function() { - this.parents('.modal').modal('hide'); - } - }, - {text: _t("Ok"), click: function() { - var self2 = this; - self.on_confirmed().always(function() { - self2.parents('.modal').modal('hide'); - }); - } - } - ], - }, $('<div/>').text(self.node.attrs.confirm)).open(); - dialog.on("closing", null, function() {def.resolve();}); - return def.promise(); - } else { - return self.on_confirmed(); - } - }; - if (!this.node.attrs.special) { - return this.view.recursive_save().then(exec_action); - } else { - return exec_action(); - } - }, - on_confirmed: function() { - var self = this; - - var context = this.build_context(); - return this.view.do_execute_action( - _.extend({}, this.node.attrs, {context: context}), - this.view.dataset, this.view.datarecord.id, function (reason) { - if (!_.isObject(reason)) { - self.view.recursive_reload(); - } - }); - }, - check_disable: function() { - var disabled = (this.force_disabled || !this.view.is_interactible_record()); - this.$el.prop('disabled', disabled); - this.$el.css('color', disabled ? 'grey' : ''); - } -}); - -/** - * Interface to be implemented by fields. - * - * Events: - * - changed_value: triggered when the value of the field has changed. This can be due - * to a user interaction or a call to set_value(). - * - */ -instance.web.form.FieldInterface = { - /** - * Constructor takes 2 arguments: - * - field_manager: Implements FieldManagerMixin - * - node: the "<field>" node in json form - */ - init: function(field_manager, node) {}, - /** - * Called by the form view to indicate the value of the field. - * - * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation, - * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur - * before the widget is inserted into the DOM. - * - * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the - * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must - * also be able to handle any other format commonly used in the _defaults key on the models in the addons - * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as - * no information is ever given to know which format is used. - */ - set_value: function(value_) {}, - /** - * Get the current value of the widget. - * - * Must always return a syntactically correct value to be passed to the "write" method of the osv class in - * the OpenERP server, although it is not assumed to respect the constraints applied to the field. - * For example if the field is marked as "required", a call to get_value() can return false. - * - * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to - * return a default value according to the type of field. - * - * This method is always assumed to perform synchronously, it can not return a promise. - * - * If there was no user interaction to modify the value of the field, it is always assumed that - * get_value() return the same semantic value than the one passed in the last call to set_value(), - * although the syntax can be different. This can be the case for type of fields that have a different - * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0). - */ - get_value: function() {}, - /** - * Inform the current object of the id it should use to match a html <label> that exists somewhere in the - * view. - */ - set_input_id: function(id) {}, - /** - * Returns true if is_syntax_valid() returns true and the value is semantically - * valid too according to the semantic restrictions applied to the field. - */ - is_valid: function() {}, - /** - * Returns true if the field holds a value which is syntactically correct, ignoring - * the potential semantic restrictions applied to the field. - */ - is_syntax_valid: function() {}, - /** - * Must set the focus on the field. Return false if field is not focusable. - */ - focus: function() {}, - /** - * Called when the translate button is clicked. - */ - on_translate: function() {}, - /** - This method is called by the form view before reading on_change values and before saving. It tells - the field to save its value before reading it using get_value(). Must return a promise. - */ - commit_value: function() {}, -}; - -/** - * Abstract class for classes implementing FieldInterface. - * - * Properties: - * - value: useful property to hold the value of the field. By default, set_value() and get_value() - * set and retrieve the value property. Changing the value property also triggers automatically - * a 'changed_value' event that inform the view to trigger on_changes. - * - */ -instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, { - /** - * @constructs instance.web.form.AbstractField - * @extends instance.web.form.FormWidget - * - * @param field_manager - * @param node - */ - init: function(field_manager, node) { - var self = this; - this._super(field_manager, node); - this.name = this.node.attrs.name; - this.field = this.field_manager.get_field_desc(this.name); - this.widget = this.node.attrs.widget; - this.string = this.node.attrs.string || this.field.string || this.name; - this.options = instance.web.py_eval(this.node.attrs.options || '{}'); - this.set({'value': false}); - - this.on("change:value", this, function() { - this.trigger('changed_value'); - this._check_css_flags(); - }); - }, - renderElement: function() { - var self = this; - this._super(); - if (this.field.translate && this.view) { - this.$el.addClass('oe_form_field_translatable'); - this.$el.find('.oe_field_translate').click(this.on_translate); - } - this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $(); - this.do_attach_tooltip(this, this.$label[0] || this.$el); - if (instance.session.debug) { - this.$label.off('dblclick').on('dblclick', function() { - console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view); - window.w = self; - console.log("window.w =", window.w); - }); - } - if (!this.disable_utility_classes) { - this.off("change:required", this, this._set_required); - this.on("change:required", this, this._set_required); - this._set_required(); - } - this._check_visibility(); - this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags); - this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags); - this._check_css_flags(); - }, - start: function() { - var tmp = this._super(); - this.on("change:value", this, function() { - if (! this.no_rerender) - this.render_value(); - }); - this.render_value(); - }, - /** - * Private. Do not use. - */ - _set_required: function() { - this.$el.toggleClass('oe_form_required', this.get("required")); - }, - set_value: function(value_) { - this.set({'value': value_}); - }, - get_value: function() { - return this.get('value'); - }, - /** - Utility method that all implementations should use to change the - value without triggering a re-rendering. - */ - internal_set_value: function(value_) { - var tmp = this.no_rerender; - this.no_rerender = true; - this.set({'value': value_}); - this.no_rerender = tmp; - }, - /** - This method is called each time the value is modified. - */ - render_value: function() {}, - is_valid: function() { - return this.is_syntax_valid() && !(this.get('required') && this.is_false()); - }, - is_syntax_valid: function() { - return true; - }, - /** - * Method useful to implement to ease validity testing. Must return true if the current - * value is similar to false in OpenERP. - */ - is_false: function() { - return this.get('value') === false; - }, - _check_css_flags: function() { - if (this.field.translate) { - this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create"); - } - if (!this.disable_utility_classes) { - if (this.field_manager.get('display_invalid_fields')) { - this.$el.toggleClass('oe_form_invalid', !this.is_valid()); - } - } - }, - focus: function() { - return false; - }, - set_input_id: function(id) { - this.id_for_label = id; - }, - on_translate: function() { - var self = this; - var trans = new instance.web.DataSet(this, 'ir.translation'); - return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) { - self.do_action(r); - }); - }, - - set_dimensions: function (height, width) { - this.$el.css({ - width: width, - minHeight: height - }); - }, - commit_value: function() { - return $.when(); - }, -}); - -/** - * A mixin to apply on any FormWidget that has to completely re-render when its readonly state - * switch. - */ -instance.web.form.ReinitializeWidgetMixin = { - /** - * Default implementation of, you should not override it, use initialize_field() instead. - */ - start: function() { - this.initialize_field(); - this._super(); - }, - initialize_field: function() { - this.on("change:effective_readonly", this, this.reinitialize); - this.initialize_content(); - }, - reinitialize: function() { - this.destroy_content(); - this.renderElement(); - this.initialize_content(); - }, - /** - * Called to destroy anything that could have been created previously, called before a - * re-initialization. - */ - destroy_content: function() {}, - /** - * Called to initialize the content. - */ - initialize_content: function() {}, -}; - -/** - * A mixin to apply on any field that has to completely re-render when its readonly state - * switch. - */ -instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, { - reinitialize: function() { - instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this); - var res = this.render_value(); - if (this.view && this.view.render_value_defs){ - this.view.render_value_defs.push(res); - } - }, -}); - -/** - Some hack to make placeholders work in ie9. -*/ -if (!('placeholder' in document.createElement('input'))) { - document.addEventListener("DOMNodeInserted",function(event){ - var nodename = event.target.nodeName.toLowerCase(); - if ( nodename === "input" || nodename == "textarea" ) { - $(event.target).placeholder(); - } - }); -} - -instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - template: 'FieldChar', - widget_class: 'oe_form_field_char', - events: { - 'change input': 'store_dom_value', - }, - init: function (field_manager, node) { - this._super(field_manager, node); - this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1'; - }, - initialize_content: function() { - this.setupFocus(this.$('input')); - }, - store_dom_value: function () { - if (!this.get('effective_readonly') - && this.$('input').length - && this.is_syntax_valid()) { - this.internal_set_value( - this.parse_value( - this.$('input').val())); - } - }, - commit_value: function () { - this.store_dom_value(); - return this._super(); - }, - render_value: function() { - var show_value = this.format_value(this.get('value'), ''); - if (!this.get("effective_readonly")) { - this.$el.find('input').val(show_value); - } else { - if (this.password) { - show_value = new Array(show_value.length + 1).join('*'); - } - this.$(".oe_form_char_content").text(show_value); - } - }, - is_syntax_valid: function() { - if (!this.get("effective_readonly") && this.$("input").size() > 0) { - try { - this.parse_value(this.$('input').val(), ''); - return true; - } catch(e) { - return false; - } - } - return true; - }, - parse_value: function(val, def) { - return instance.web.parse_value(val, this, def); - }, - format_value: function(val, def) { - return instance.web.format_value(val, this, def); - }, - is_false: function() { - return this.get('value') === '' || this._super(); - }, - focus: function() { - var input = this.$('input:first')[0]; - return input ? input.focus() : false; - }, - set_dimensions: function (height, width) { - this._super(height, width); - this.$('input').css({ - height: height, - width: width - }); - } -}); - -instance.web.form.KanbanSelection = instance.web.form.FieldChar.extend({ - init: function (field_manager, node) { - this._super(field_manager, node); - }, - start: function () { - var self = this; - this.states = []; - this._super.apply(this, arguments); - // hook on form view content changed: recompute the states, because it may be related to the current stage - this.getParent().on('view_content_has_changed', self, function () { - self.render_value(); - }); - }, - prepare_dropdown_selection: function() { - var self = this; - var data = []; - var selection = this.field.selection || []; - var stage_id = _.isArray(this.view.datarecord.stage_id) ? this.view.datarecord.stage_id[0] : this.view.datarecord.stage_id; - var legend_field = this.options && this.options.states_legend_field || false; - var fields_to_read = _.map( - this.options && this.options.states_legend || {}, - function (value, key, list) { return value; }); - if (legend_field && fields_to_read && stage_id) { - var fetch_stage = new openerp.web.DataSet( - this, - self.view.fields[legend_field].field.relation).read_ids([stage_id], - fields_to_read); - } - else { var fetch_stage = $.Deferred().resolve(false); } - return $.when(fetch_stage).then(function (stage_data) { - _.map(selection, function(res) { - var value = { - 'name': res[0], - 'tooltip': res[1], - 'state_name': res[1], - } - if (stage_data && stage_data[0][self.options.states_legend[res[0]]]) { - value['state_name'] = stage_data[0][self.options.states_legend[res[0]]]; - value['tooltip'] = stage_data[0][self.options.states_legend[res[0]]]; - } - if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; } - else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; } - else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; } - data.push(value); - }); - return data; - }); - }, - render_value: function() { - var self = this; - var dd_fetched = this.prepare_dropdown_selection();; - return $.when(dd_fetched).then(function (states) { - self.states = states; - self.$el.html(QWeb.render("KanbanSelection", {'widget': self})); - self.$el.find('li').on('click', self.set_kanban_selection.bind(self)); - }) - }, - /* setting the value: in view mode, perform an asynchronous call and reload - the form view; in edit mode, use set_value to save the new value that will - be written when saving the record. */ - set_kanban_selection: function (ev) { - var self = this; - var li = $(ev.target).closest('li'); - if (li.length) { - var value = String(li.data('value')); - if (this.view.get('actual_mode') == 'view') { - var write_values = {} - write_values[self.name] = value; - return this.view.dataset._model.call( - 'write', [ - [this.view.datarecord.id], - write_values, - self.view.dataset.get_context() - ]).done(self.reload_record.bind(self)); - } - else { - return this.set_value(value); - } - } - }, - reload_record: function() { - this.view.reload(); - }, -}); - -instance.web.form.Priority = instance.web.form.FieldChar.extend({ - init: function (field_manager, node) { - this._super(field_manager, node); - }, - prepare_priority: function() { - var self = this; - var selection = this.field.selection || []; - var init_value = selection && selection[0][0] || 0; - var data = _.map(selection.slice(1), function(element, index) { - var value = { - 'value': element[0], - 'name': element[1], - 'click_value': element[0], - } - if (index == 0 && self.get('value') == element[0]) { - value['click_value'] = init_value; - } - return value; - }); - return data; - }, - render_value: function() { - var self = this; - this.priorities = this.prepare_priority(); - this.$el.html(QWeb.render("Priority", {'widget': this})); - if (!this.get('readonly')){ - this.$el.find('li').on('click', this.set_priority.bind(this)); - } - }, - /* setting the value: in view mode, perform an asynchronous call and reload - the form view; in edit mode, use set_value to save the new value that will - be written when saving the record. */ - set_priority: function (ev) { - var self = this; - var li = $(ev.target).closest('li'); - if (li.length) { - var value = String(li.data('value')); - if (this.view.get('actual_mode') == 'view') { - var write_values = {} - write_values[self.name] = value; - return this.view.dataset._model.call( - 'write', [ - [this.view.datarecord.id], - write_values, - self.view.dataset.get_context() - ]).done(self.reload_record.bind(self)); - } - else { - return this.set_value(value); - } - } - - }, - reload_record: function() { - this.view.reload(); - }, -}); - -instance.web.form.FieldID = instance.web.form.FieldChar.extend({ - process_modifiers: function () { - this._super(); - this.set({ readonly: true }); - }, -}); - -instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({ - template: 'FieldEmail', - initialize_content: function() { - this._super(); - var $button = this.$el.find('button'); - $button.click(this.on_button_clicked); - this.setupFocus($button); - }, - render_value: function() { - if (!this.get("effective_readonly")) { - this._super(); - } else { - this.$el.find('a') - .attr('href', 'mailto:' + this.get('value')) - .text(this.get('value') || ''); - } - }, - on_button_clicked: function() { - if (!this.get('value') || !this.is_syntax_valid()) { - this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address")); - } else { - location.href = 'mailto:' + this.get('value'); - } - } -}); - -instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ - template: 'FieldUrl', - initialize_content: function() { - this._super(); - var $button = this.$el.find('button'); - $button.click(this.on_button_clicked); - this.setupFocus($button); - }, - render_value: function() { - if (!this.get("effective_readonly")) { - this._super(); - } else { - var tmp = this.get('value'); - var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp); - if (!s) { - tmp = "http://" + this.get('value'); - } - var text = this.get('value') ? this.node.attrs.text || tmp : ''; - this.$el.find('a').attr('href', tmp).text(text); - } - }, - on_button_clicked: function() { - if (!this.get('value')) { - this.do_warn(_t("Resource Error"), _t("This resource is empty")); - } else { - var url = $.trim(this.get('value')); - if(/^www\./i.test(url)) - url = 'http://'+url; - window.open(url); - } - } -}); - -instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ - is_field_number: true, - widget_class: 'oe_form_field_float', - init: function (field_manager, node) { - this._super(field_manager, node); - this.internal_set_value(0); - if (this.node.attrs.digits) { - this.digits = this.node.attrs.digits; - } else { - this.digits = this.field.digits; - } - }, - set_value: function(value_) { - if (value_ === false || value_ === undefined) { - // As in GTK client, floats default to 0 - value_ = 0; - } - if (this.digits !== undefined && this.digits.length === 2) { - value_ = instance.web.round_decimals(value_, this.digits[1]); - } - this._super.apply(this, [value_]); - }, - focus: function () { - var $input = this.$('input:first'); - return $input.length ? $input.select() : false; - } -}); - - -instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - render_value: function() { - var self = this; - this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this})); - if (this.get('value')) { - var model = this.options.model || this.field_manager.get_field_value(this.options.model_field); - var domain = instance.web.pyeval.eval('domain', this.get('value')); - var ds = new instance.web.DataSetStatic(self, model, self.build_context()); - ds.call('search_count', [domain]).then(function (results) { - $('.oe_domain_count', self.$el).text(results + ' records selected'); - if (self.get('effective_readonly')) { - $('button span', self.$el).text(' See selection'); - } - else { - $('button span', self.$el).text(' Change selection'); - } - }); - } else { - $('.oe_domain_count', this.$el).text('0 record selected'); - $('button span', this.$el).text(' Select records'); - }; - this.$('.select_records').on('click', self.on_click); - }, - on_click: function(event) { - event.preventDefault(); - var self = this; - var model = this.options.model || this.field_manager.get_field_value(this.options.model_field); - - var options = { - title: this.get('effective_readonly') ? 'Selected records' : 'Select records...', - readonly: this.get('effective_readonly'), - disable_multiple_selection: this.get('effective_readonly'), - no_create: this.get('effective_readonly'), - }; - var domain = this.get('value'); - popup = new instance.web.form.DomainEditorPopup(this); - popup.select_element(model, options, domain, {}); - popup.on("elements_selected", self, function(selected_ids) { - if (!self.get('effective_readonly')) { - self.set_value(popup.get_domain(selected_ids)); - } - }); - }, -}); - - -instance.web.DateTimeWidget = instance.web.Widget.extend({ - template: "web.datepicker", - type_of_date: "datetime", - events: { - 'dp.change .oe_datepicker_main': 'change_datetime', - 'dp.show .oe_datepicker_main': 'set_datetime_default', - 'keypress .oe_datepicker_master': 'change_datetime', - }, - init: function(parent) { - this._super(parent); - this.name = parent.name; - }, - start: function() { - var self = this; - var l10n = _t.database.parameters; - var options = { - pickTime: true, - useSeconds: true, - startDate: moment({ y: 1900 }), - endDate: moment().add(200, "y"), - calendarWeeks: true, - icons : { - time: 'fa fa-clock-o', - date: 'fa fa-calendar', - up: 'fa fa-chevron-up', - down: 'fa fa-chevron-down' - }, - language : moment.locale(), - format : instance.web.normalize_format(l10n.date_format +' '+ l10n.time_format), - }; - this.$input = this.$el.find('input.oe_datepicker_master'); - if (this.type_of_date === 'date') { - options['pickTime'] = false; - options['useSeconds'] = false; - options['format'] = instance.web.normalize_format(l10n.date_format); - } - this.picker = this.$('.oe_datepicker_main').datetimepicker(options); - this.set_readonly(false); - this.set({'value': false}); - }, - set_value: function(value_) { - this.set({'value': value_}); - this.$input.val(value_ ? this.format_client(value_) : ''); - }, - get_value: function() { - return this.get('value'); - }, - set_value_from_ui_: function() { - var value_ = this.$input.val() || false; - this.set_value(this.parse_client(value_)); - }, - set_readonly: function(readonly) { - this.readonly = readonly; - this.$input.prop('readonly', this.readonly); - }, - is_valid_: function() { - var value_ = this.$input.val(); - if (value_ === "") { - return true; - } else { - try { - this.parse_client(value_); - return true; - } catch(e) { - return false; - } - } - }, - parse_client: function(v) { - return instance.web.parse_value(v, {"widget": this.type_of_date}); - }, - format_client: function(v) { - return instance.web.format_value(v, {"widget": this.type_of_date}); - }, - set_datetime_default: function(){ - //when opening datetimepicker the date and time by default should be the one from - //the input field if any or the current day otherwise - if (this.type_of_date === 'datetime') { - value = moment().second(0); - if (this.$input.val().length !== 0 && this.is_valid_()){ - var value = this.$input.val(); - } - this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value); - } - }, - change_datetime: function(e) { - if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) { - this.set_value_from_ui_(); - this.trigger("datetime_changed"); - } - }, - commit_value: function () { - this.change_datetime(); - }, -}); - -instance.web.DateWidget = instance.web.DateTimeWidget.extend({ - type_of_date: "date" -}); - -instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - template: "FieldDatetime", - build_widget: function() { - return new instance.web.DateTimeWidget(this); - }, - destroy_content: function() { - if (this.datewidget) { - this.datewidget.destroy(); - this.datewidget = undefined; - } - }, - initialize_content: function() { - if (!this.get("effective_readonly")) { - this.datewidget = this.build_widget(); - this.datewidget.on('datetime_changed', this, _.bind(function() { - this.internal_set_value(this.datewidget.get_value()); - }, this)); - this.datewidget.appendTo(this.$el); - this.setupFocus(this.datewidget.$input); - } - }, - render_value: function() { - if (!this.get("effective_readonly")) { - this.datewidget.set_value(this.get('value')); - } else { - this.$el.text(instance.web.format_value(this.get('value'), this, '')); - } - }, - is_syntax_valid: function() { - if (!this.get("effective_readonly") && this.datewidget) { - return this.datewidget.is_valid_(); - } - return true; - }, - is_false: function() { - return this.get('value') === '' || this._super(); - }, - focus: function() { - var input = this.datewidget && this.datewidget.$input[0]; - return input ? input.focus() : false; - }, - set_dimensions: function (height, width) { - this._super(height, width); - if (!this.get("effective_readonly")) { - this.datewidget.$input.css('height', height); - } - } -}); - -instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({ - template: "FieldDate", - build_widget: function() { - return new instance.web.DateWidget(this); - } -}); - -instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - template: 'FieldText', - events: { - 'keyup': function (e) { - if (e.which === $.ui.keyCode.ENTER) { - e.stopPropagation(); - } - }, - 'keypress': function (e) { - if (e.which === $.ui.keyCode.ENTER) { - e.stopPropagation(); - } - }, - 'change textarea': 'store_dom_value', - }, - initialize_content: function() { - var self = this; - if (! this.get("effective_readonly")) { - this.$textarea = this.$el.find('textarea'); - this.auto_sized = false; - this.default_height = this.$textarea.css('height'); - if (this.default_height === '0px') this.default_height = '90px'; - if (this.get("effective_readonly")) { - this.$textarea.attr('disabled', 'disabled'); - } - this.setupFocus(this.$textarea); - } else { - this.$textarea = undefined; - } - }, - commit_value: function () { - if (! this.get("effective_readonly") && this.$textarea) { - this.store_dom_value(); - } - return this._super(); - }, - store_dom_value: function () { - this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this)); - }, - render_value: function() { - if (! this.get("effective_readonly")) { - var show_value = instance.web.format_value(this.get('value'), this, ''); - if (show_value === '') { - this.$textarea.css('height', parseInt(this.default_height, 10)+"px"); - } - this.$textarea.val(show_value); - if (! this.auto_sized) { - this.auto_sized = true; - this.$textarea.autosize(); - } else { - this.$textarea.trigger("autosize"); - } - } else { - var txt = this.get("value") || ''; - this.$(".oe_form_text_content").text(txt); - } - }, - is_syntax_valid: function() { - if (!this.get("effective_readonly") && this.$textarea) { - try { - instance.web.parse_value(this.$textarea.val(), this, ''); - return true; - } catch(e) { - return false; - } - } - return true; - }, - is_false: function() { - return this.get('value') === '' || this._super(); - }, - focus: function($el) { - var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0]; - return input ? input.focus() : false; - }, - set_dimensions: function (height, width) { - this._super(height, width); - if (!this.get("effective_readonly") && this.$textarea) { - this.$textarea.css({ - width: width, - minHeight: height - }); - } - }, -}); - -/** - * FieldTextHtml Widget - * Intended for FieldText widgets meant to display HTML content. This - * widget will instantiate the CLEditor (see cleditor in static/src/lib) - * To find more information about CLEditor configutation: go to - * http://premiumsoftware.net/cleditor/docs/GettingStarted.html - */ -instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - template: 'FieldTextHtml', - init: function() { - this._super.apply(this, arguments); - }, - initialize_content: function() { - var self = this; - if (! this.get("effective_readonly")) { - self._updating_editor = false; - this.$textarea = this.$el.find('textarea'); - var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)'); - var height = ((this.node.attrs || {}).editor_height || 250); - this.$textarea.cleditor({ - width: width, // width not including margins, borders or padding - height: height, // height not including margins, borders or padding - controls: // controls to add to the toolbar - "bold italic underline strikethrough " + - "| removeformat | bullets numbering | outdent " + - "indent | link unlink | source", - bodyStyle: // style to assign to document body contained within the editor - "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text" - }); - this.$cleditor = this.$textarea.cleditor()[0]; - this.$cleditor.change(function() { - if (! self._updating_editor) { - self.$cleditor.updateTextArea(); - self.internal_set_value(self.$textarea.val()); - } - }); - if (this.field.translate) { - var $img = $('<img class="oe_field_translate oe_input_icon" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>') - .click(this.on_translate); - this.$cleditor.$toolbar.append($img); - } - } - }, - render_value: function() { - if (! this.get("effective_readonly")) { - this.$textarea.val(this.get('value') || ''); - this._updating_editor = true; - this.$cleditor.updateFrame(); - this._updating_editor = false; - } else { - this.$el.html(this.get('value')); - } - }, -}); - -instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ - template: 'FieldBoolean', - start: function() { - var self = this; - this.$checkbox = $("input", this.$el); - this.setupFocus(this.$checkbox); - this.$el.click(_.bind(function() { - this.internal_set_value(this.$checkbox.is(':checked')); - }, this)); - var check_readonly = function() { - self.$checkbox.prop('disabled', self.get("effective_readonly")); - self.click_disabled_boolean(); - }; - this.on("change:effective_readonly", this, check_readonly); - check_readonly.call(this); - this._super.apply(this, arguments); - }, - render_value: function() { - this.$checkbox[0].checked = this.get('value'); - }, - focus: function() { - var input = this.$checkbox && this.$checkbox[0]; - return input ? input.focus() : false; - }, - click_disabled_boolean: function(){ - var $disabled = this.$el.find('input[type=checkbox]:disabled'); - $disabled.each(function (){ - $(this).next('div').remove(); - $(this).closest("span").append($('<div class="boolean"></div>')); - }); - } -}); - -/** - The progressbar field expect a float from 0 to 100. -*/ -instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({ - template: 'FieldProgressBar', - render_value: function() { - this.$el.progressbar({ - value: this.get('value') || 0, - disabled: this.get("effective_readonly") - }); - var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' }); - this.$('span').html(formatted_value + '%'); - } -}); - -/** - The PercentPie field expect a float from 0 to 100. -*/ -instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({ - template: 'FieldPercentPie', - - render_value: function() { - var value = this.get('value'), - formatted_value = Math.round(value || 0) + '%', - svg = this.$('svg')[0]; - - svg.innerHTML = ""; - nv.addGraph(function() { - var width = 42, height = 42; - var chart = nv.models.pieChart() - .width(width) - .height(height) - .margin({top: 0, right: 0, bottom: 0, left: 0}) - .donut(true) - .showLegend(false) - .showLabels(false) - .tooltips(false) - .color(['#7C7BAD','#DDD']) - .donutRatio(0.62); - - d3.select(svg) - .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}]) - .transition() - .call(chart) - .attr('style', 'width: ' + width + 'px; height:' + height + 'px;'); - - d3.select(svg) - .append("text") - .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'}) - .style({"font-size": "10px", "font-weight": "bold"}) - .text(formatted_value); - - return chart; - }); - - } -}); - -/** - The FieldBarChart expectsa list of values (indeed) -*/ -instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({ - template: 'FieldBarChart', - - render_value: function() { - var value = JSON.parse(this.get('value')); - var svg = this.$('svg')[0]; - svg.innerHTML = ""; - nv.addGraph(function() { - var width = 34, height = 34; - var chart = nv.models.discreteBarChart() - .x(function (d) { return d.tooltip }) - .y(function (d) { return d.value }) - .width(width) - .height(height) - .margin({top: 0, right: 0, bottom: 0, left: 0}) - .tooltips(false) - .showValues(false) - .transitionDuration(350) - .showXAxis(false) - .showYAxis(false); - - d3.select(svg) - .datum([{key: 'values', values: value}]) - .transition() - .call(chart) - .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;'); - - nv.utils.windowResize(chart.update); - - return chart; - }); - - } -}); - - -instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - template: 'FieldSelection', - events: { - 'change select': 'store_dom_value', - }, - init: function(field_manager, node) { - var self = this; - this._super(field_manager, node); - this.set("value", false); - this.set("values", []); - this.records_orderer = new instance.web.DropMisordered(); - this.field_manager.on("view_content_has_changed", this, function() { - var domain = new openerp.web.CompoundDomain(this.build_domain()).eval(); - if (! _.isEqual(domain, this.get("domain"))) { - this.set("domain", domain); - } - }); - }, - initialize_field: function() { - instance.web.form.ReinitializeFieldMixin.initialize_field.call(this); - this.on("change:domain", this, this.query_values); - this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval()); - this.on("change:values", this, this.render_value); - }, - query_values: function() { - var self = this; - var def; - if (this.field.type === "many2one") { - var model = new openerp.Model(openerp.session, this.field.relation); - def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()}); - } else { - var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; }); - def = $.when(values); - } - this.records_orderer.add(def).then(function(values) { - if (! _.isEqual(values, self.get("values"))) { - self.set("values", values); - } - }); - }, - initialize_content: 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; - var $select = this.$el.find('select') - .change(function () { ischanging = true; }) - .click(function () { ischanging = false; }) - .keyup(function (e) { - if (e.which !== 13 || !ischanging) { return; } - e.stopPropagation(); - ischanging = false; - }); - this.setupFocus($select); - }, - commit_value: function () { - this.store_dom_value(); - return this._super(); - }, - store_dom_value: function () { - if (!this.get('effective_readonly') && this.$('select').length) { - var val = JSON.parse(this.$('select').val()); - this.internal_set_value(val); - } - }, - set_value: function(value_) { - value_ = value_ === null ? false : value_; - value_ = value_ instanceof Array ? value_[0] : value_; - this._super(value_); - }, - render_value: function() { - var values = this.get("values"); - values = [[false, this.node.attrs.placeholder || '']].concat(values); - var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this); - if (! found) { - found = [this.get("value"), _t('Unknown')]; - values = [found].concat(values); - } - if (! this.get("effective_readonly")) { - this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values})); - this.$("select").val(JSON.stringify(found[0])); - } else { - this.$el.text(found[1]); - } - }, - focus: function() { - var input = this.$('select:first')[0]; - return input ? input.focus() : false; - }, - set_dimensions: function (height, width) { - this._super(height, width); - this.$('select').css({ - height: height, - width: width - }); - } -}); - -instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - template: 'FieldRadio', - events: { - 'click input': 'click_change_value' - }, - init: function(field_manager, node) { - /* Radio button widget: Attributes options: - * - "horizontal" to display in column - * - "no_radiolabel" don't display text values - */ - this._super(field_manager, node); - this.selection = _.clone(this.field.selection) || []; - this.domain = false; - this.uniqueId = _.uniqueId("radio"); - }, - initialize_content: function () { - this.on("change:effective_readonly", this, this.render_value); - this.field_manager.on("view_content_has_changed", this, this.get_selection); - this.get_selection(); - }, - click_change_value: function (event) { - var val = $(event.target).val(); - val = this.field.type == "selection" ? val : +val; - if (val !== this.get_value()) { - this.set_value(val); - } - }, - /** Get the selection and render it - * selection: [[identifier, value_to_display], ...] - * For selection fields: this is directly given by this.field.selection - * For many2one fields: perform a search on the relation of the many2one field - */ - get_selection: function() { - var self = this; - var selection = []; - var def = $.Deferred(); - if (self.field.type == "many2one") { - var domain = instance.web.pyeval.eval('domain', this.build_domain()) || []; - if (! _.isEqual(self.domain, domain)) { - self.domain = domain; - var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context()); - ds.call('search', [self.domain]) - .then(function (records) { - ds.name_get(records).then(function (records) { - selection = records; - def.resolve(); - }); - }); - } else { - selection = self.selection; - def.resolve(); - } - } - else if (self.field.type == "selection") { - selection = self.field.selection || []; - def.resolve(); - } - return def.then(function () { - if (! _.isEqual(selection, self.selection)) { - self.selection = _.clone(selection); - self.renderElement(); - self.render_value(); - } - }); - }, - set_value: function (value_) { - if (this.field.type == "selection") { - value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;}); - } - else if (!this.selection.length) { - this.selection = [value_]; - } - - this._super(value_); - }, - get_value: function () { - var value = this.get('value'); - return value instanceof Array ? value[0] : value; - }, - render_value: function () { - var self = this; - this.$el.toggleClass("oe_readonly", this.get('effective_readonly')); - this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true); - this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : ""); - } -}); - -// jquery autocomplete tweak to allow html and classnames -(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 ) - .addClass(item.classname); - } - }); -})(); - -/** - A mixin containing some useful methods to handle completion inputs. - - The widget containing this option can have these arguments in its widget options: - - no_quick_create: if true, it will disable the quick create -*/ -instance.web.form.CompletionFieldMixin = { - init: function() { - this.limit = 7; - this.orderer = new instance.web.DropMisordered(); - this.can_create = this.node.attrs.can_create || true; - this.can_write = this.node.attrs.can_write || true; - }, - /** - * Call this method to search using a string. - */ - get_search_result: function(search_val) { - var self = this; - - var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context()); - this.last_query = search_val; - var exclusion_domain = [], ids_blacklist = this.get_search_blacklist(); - if (!_(ids_blacklist).isEmpty()) { - exclusion_domain.push(['id', 'not in', ids_blacklist]); - } - - return this.orderer.add(dataset.name_search( - search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain), - 'ilike', this.limit + 1, self.build_context())).then(function(data) { - self.last_search = data; - // possible selections for the m2o - var values = _.map(data, function(x) { - x[1] = x[1].split("\n")[0]; - return { - label: _.str.escapeHTML(x[1]), - value: x[1], - 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("Search More..."), - action: function() { - dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) { - self._search_create_popup("search", data); - }); - }, - classname: 'oe_m2o_dropdown_option' - }); - } - // quick create - var raw_result = _(data.result).map(function(x) {return x[1];}); - if (search_val.length > 0 && !_.include(raw_result, search_val) && - ! (self.options && (self.options.no_create || self.options.no_quick_create))) { - self.can_create && values.push({ - label: _.str.sprintf(_t('Create "<strong>%s</strong>"'), - $('<span />').text(search_val).html()), - action: function() { - self._quick_create(search_val); - }, - classname: 'oe_m2o_dropdown_option' - }); - } - // create... - if (!(self.options && (self.options.no_create || self.options.no_create_edit)) && self.can_create){ - values.push({ - label: _t("Create and Edit..."), - action: function() { - self._search_create_popup("form", undefined, self._create_context(search_val)); - }, - classname: 'oe_m2o_dropdown_option' - }); - } - else if (values.length == 0) - values.push({ - label: _t("No results to show..."), - action: function() {}, - classname: 'oe_m2o_dropdown_option' - }); - - return values; - }); - }, - get_search_blacklist: function() { - return []; - }, - _quick_create: function(name) { - var self = this; - var slow_create = function () { - self._search_create_popup("form", undefined, self._create_context(name)); - }; - if (self.options.quick_create === undefined || self.options.quick_create) { - new instance.web.DataSet(this, this.field.relation, self.build_context()) - .name_create(name).done(function(data) { - if (!self.get('effective_readonly')) - self.add_id(data[0]); - }).fail(function(error, event) { - event.preventDefault(); - slow_create(); - }); - } else - slow_create(); - }, - // all search/create popup handling - _search_create_popup: function(view, ids, context) { - var self = this; - var pop = new instance.web.form.SelectCreatePopup(this); - pop.select_element( - self.field.relation, - { - title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string, - initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined, - initial_view: view, - disable_multiple_selection: true - }, - self.build_domain(), - new instance.web.CompoundContext(self.build_context(), context || {}) - ); - pop.on("elements_selected", self, function(element_ids) { - self.add_id(element_ids[0]); - self.focus(); - }); - }, - /** - * To implement. - */ - add_id: function(id) {}, - _create_context: function(name) { - var tmp = {}; - var field = (this.options || {}).create_name_field; - if (field === undefined) - field = "name"; - if (field !== false && name && (this.options || {}).quick_create !== false) - tmp["default_" + field] = name; - return tmp; - }, -}; - -instance.web.form.M2ODialog = instance.web.Dialog.extend({ - template: "M2ODialog", - init: function(parent) { - this.name = parent.string; - this._super(parent, { - title: _.str.sprintf(_t("Create a %s"), parent.string), - size: 'medium', - }); - }, - start: function() { - var self = this; - var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name); - this.$("p").text( text ); - this.$buttons.html(QWeb.render("M2ODialog.buttons")); - this.$("input").val(this.getParent().$input.val()); - this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){ - if (self.$("input").val() != ''){ - self.getParent()._quick_create(self.$("input").val()); - self.destroy(); - } else{ - e.preventDefault(); - self.$("input").focus(); - } - }); - this.$buttons.find(".oe_form_m2o_sc_button").click(function(){ - self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val())); - self.destroy(); - }); - this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){ - self.destroy(); - }); - }, -}); - -instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, { - template: "FieldMany2One", - events: { - 'keydown input': function (e) { - switch (e.which) { - case $.ui.keyCode.UP: - case $.ui.keyCode.DOWN: - e.stopPropagation(); - } - }, - }, - init: function(field_manager, node) { - this._super(field_manager, node); - instance.web.form.CompletionFieldMixin.init.call(this); - this.set({'value': false}); - this.display_value = {}; - this.display_value_backup = {}; - this.last_search = []; - this.floating = false; - this.current_display = null; - this.is_started = false; - this.ignore_focusout = false; - }, - reinit_value: function(val) { - this.internal_set_value(val); - this.floating = false; - if (this.is_started && !this.no_rerender) - this.render_value(); - }, - initialize_field: function() { - this.is_started = true; - instance.web.bus.on('click', this, function() { - if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) { - this.$input.autocomplete("close"); - } - }); - instance.web.form.ReinitializeFieldMixin.initialize_field.call(this); - }, - initialize_content: function() { - if (!this.get("effective_readonly")) - this.render_editable(); - }, - destroy_content: function () { - if (this.$drop_down) { - this.$drop_down.off('click'); - delete this.$drop_down; - } - if (this.$input) { - this.$input.closest(".modal .modal-content").off('scroll'); - this.$input.off('keyup blur autocompleteclose autocompleteopen ' + - 'focus focusout change keydown'); - delete this.$input; - } - if (this.$follow_button) { - this.$follow_button.off('blur focus click'); - delete this.$follow_button; - } - }, - destroy: function () { - this.destroy_content(); - return this._super(); - }, - init_error_displayer: function() { - // nothing - }, - hide_error_displayer: function() { - // doesn't work - }, - show_error_displayer: function() { - new instance.web.form.M2ODialog(this).open(); - }, - render_editable: function() { - var self = this; - this.$input = this.$el.find("input"); - - this.init_error_displayer(); - - self.$input.on('focus', function() { - self.hide_error_displayer(); - }); - - this.$drop_down = this.$el.find(".oe_m2o_drop_down_button"); - this.$follow_button = $(".oe_m2o_cm_button", this.$el); - - this.$follow_button.click(function(ev) { - ev.preventDefault(); - if (!self.get('value')) { - self.focus(); - return; - } - var pop = new instance.web.form.FormOpenPopup(self); - var context = self.build_context().eval(); - var model_obj = new instance.web.Model(self.field.relation); - model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){ - pop.show_element( - self.field.relation, - self.get("value"), - self.build_context(), - { - title: _t("Open: ") + self.string, - view_id: view_id, - readonly: !self.can_write - } - ); - pop.on('write_completed', self, function(){ - self.display_value = {}; - self.display_value_backup = {}; - self.render_value(); - self.focus(); - self.trigger('changed_value'); - }); - }); - }); - - // some behavior for input - var input_changed = function() { - if (self.current_display !== self.$input.val()) { - self.current_display = self.$input.val(); - if (self.$input.val() === "") { - self.internal_set_value(false); - self.floating = false; - } else { - self.floating = true; - } - } - }; - this.$input.keydown(input_changed); - this.$input.change(input_changed); - this.$drop_down.click(function() { - self.$input.focus(); - if (self.$input.autocomplete("widget").is(":visible")) { - self.$input.autocomplete("close"); - } else { - if (self.get("value") && ! self.floating) { - self.$input.autocomplete("search", ""); - } else { - self.$input.autocomplete("search"); - } - } - }); - - // Autocomplete close on dialog content scroll - var close_autocomplete = _.debounce(function() { - if (self.$input.autocomplete("widget").is(":visible")) { - self.$input.autocomplete("close"); - } - }, 50); - this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete); - - self.ed_def = $.Deferred(); - self.uned_def = $.Deferred(); - var ed_delay = 200; - var ed_duration = 15000; - var anyoneLoosesFocus = function (e) { - if (self.ignore_focusout) { return; } - var used = false; - if (self.floating) { - if (self.last_search.length > 0) { - if (self.last_search[0][0] != self.get("value")) { - self.display_value = {}; - self.display_value_backup = {}; - self.display_value["" + self.last_search[0][0]] = self.last_search[0][1]; - self.reinit_value(self.last_search[0][0]); - self.last_search = [] - } else { - used = true; - self.render_value(); - } - } else { - used = true; - } - self.floating = false; - } - if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) { - self.ed_def.reject(); - self.uned_def.reject(); - self.ed_def = $.Deferred(); - self.ed_def.done(function() { - self.can_create && self.show_error_displayer(); - ignore_blur = false; - self.trigger('focused'); - }); - ignore_blur = true; - setTimeout(function() { - self.ed_def.resolve(); - self.uned_def.reject(); - self.uned_def = $.Deferred(); - self.uned_def.done(function() { - self.hide_error_displayer(); - }); - setTimeout(function() {self.uned_def.resolve();}, ed_duration); - }, ed_delay); - } else { - self.no_ed = false; - self.ed_def.reject(); - } - }; - var ignore_blur = false; - this.$input.on({ - focusout: anyoneLoosesFocus, - focus: function () { self.trigger('focused'); }, - autocompleteopen: function () { ignore_blur = true; }, - autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); }, - blur: function () { - // autocomplete open - if (ignore_blur) { $(this).focus(); return; } - if (_(self.getChildren()).any(function (child) { - return child instanceof instance.web.form.AbstractFormPopup; - })) { return; } - self.trigger('blurred'); - } - }); - - var isSelecting = false; - // autocomplete - this.$input.autocomplete({ - source: function(req, resp) { - self.get_search_result(req.term).done(function(result) { - resp(result); - }); - }, - select: function(event, ui) { - isSelecting = true; - var item = ui.item; - if (item.id) { - self.display_value = {}; - self.display_value_backup = {}; - self.display_value["" + item.id] = item.name; - self.reinit_value(item.id); - } else if (item.action) { - item.action(); - // Cancel widget blurring, to avoid form blur event - self.trigger('focused'); - return false; - } - }, - focus: function(e, ui) { - e.preventDefault(); - }, - html: true, - // disabled to solve a bug, but may cause others - //close: anyoneLoosesFocus, - minLength: 0, - delay: 200, - }); - var appendTo = this.$input.parents('.oe-view-manager-content, .modal-dialog').last(); - if (appendTo.length === 0) { - appendTo = '.oe_application > *'; - } - this.$input.autocomplete({ - appendTo: appendTo - }); - // set position for list of suggestions box - this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } ); - this.$input.autocomplete("widget").openerpClass(); - // 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) { // ENTER - if (isSelecting) - e.stopPropagation(); - } - isSelecting = false; - }); - this.setupFocus(this.$follow_button); - }, - render_value: function(no_recurse) { - var self = this; - if (! this.get("value")) { - this.display_string(""); - return; - } - var display = this.display_value["" + this.get("value")]; - if (display) { - this.display_string(display); - return; - } - if (! no_recurse) { - var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); - var def = this.alive(dataset.name_get([self.get("value")])).done(function(data) { - if (!data[0]) { - self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value"))); - return; - } - self.display_value["" + self.get("value")] = data[0][1]; - self.render_value(true); - }).fail( function (data, event) { - // avoid displaying crash errors as many2One should be name_get compliant - event.preventDefault(); - self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")]; - self.render_value(true); - }); - if (this.view && this.view.render_value_defs){ - this.view.render_value_defs.push(def); - } - } - }, - display_string: function(str) { - var self = this; - if (!this.get("effective_readonly")) { - this.$input.val(str.split("\n")[0]); - this.current_display = this.$input.val(); - if (this.is_false()) { - this.$('.oe_m2o_cm_button').css({'display':'none'}); - } else { - this.$('.oe_m2o_cm_button').css({'display':'inline'}); - } - } else { - var lines = _.escape(str).split("\n"); - var link = ""; - var follow = ""; - link = lines[0]; - follow = _.rest(lines).join("<br />"); - if (follow) - link += "<br />"; - var $link = this.$el.find('.oe_form_uri') - .unbind('click') - .html(link); - if (! this.options.no_open) - $link.click(function () { - var context = self.build_context().eval(); - var model_obj = new instance.web.Model(self.field.relation); - model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){ - self.do_action(action); - }); - return false; - }); - $(".oe_form_m2o_follow", this.$el).html(follow); - } - }, - set_value: function(value_) { - var self = this; - if (value_ instanceof Array) { - this.display_value = {}; - this.display_value_backup = {}; - if (! this.options.always_reload) { - this.display_value["" + value_[0]] = value_[1]; - } - else { - this.display_value_backup["" + value_[0]] = value_[1]; - } - value_ = value_[0]; - } - value_ = value_ || false; - this.reinit_value(value_); - }, - get_displayed: function() { - return this.display_value["" + this.get("value")]; - }, - add_id: function(id) { - this.display_value = {}; - this.display_value_backup = {}; - this.reinit_value(id); - }, - is_false: function() { - return ! this.get("value"); - }, - focus: function () { - var input = !this.get('effective_readonly') && this.$input && this.$input[0]; - return input ? input.focus() : false; - }, - _quick_create: function() { - this.no_ed = true; - this.ed_def.reject(); - return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments); - }, - _search_create_popup: function() { - this.no_ed = true; - this.ed_def.reject(); - this.ignore_focusout = true; - this.reinit_value(false); - var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments); - this.ignore_focusout = false; - this.no_ed = false; - return res; - }, - set_dimensions: function (height, width) { - this._super(height, width); - if (!this.get("effective_readonly") && this.$input) - this.$input.css('height', height); - } -}); - -instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({ - template: 'Many2OneButton', - init: function(field_manager, node) { - this._super.apply(this, arguments); - }, - start: function() { - this._super.apply(this, arguments); - this.set_button(); - }, - set_button: function() { - var self = this; - if (this.$button) { - this.$button.remove(); - } - this.string = ''; - this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png'; - this.$button = $(QWeb.render('WidgetButton', {'widget': this})); - this.$button.addClass('oe_link').css({'padding':'4px'}); - this.$el.append(this.$button); - this.$button.on('click', self.on_click); - }, - on_click: function(ev) { - var self = this; - this.popup = new instance.web.form.FormOpenPopup(this); - this.popup.show_element( - this.field.relation, - this.get('value'), - this.build_context(), - {title: this.string} - ); - this.popup.on('create_completed', self, function(r) { - self.set_value(r); - }); - }, - set_value: function(value_) { - var self = this; - if (value_ instanceof Array) { - value_ = value_[0]; - } - value_ = value_ || false; - this.set('value', value_); - this.set_button(); - }, -}); - -/** - * Abstract-ish ListView.List subclass adding an "Add an item" row to replace - * the big ugly button in the header. - * - * Requires the implementation of a ``is_readonly`` method (usually a proxy to - * the corresponding field's readonly or effective_readonly property) to - * decide whether the special row should or should not be inserted. - * - * Optionally an ``_add_row_class`` attribute can be set for the class(es) to - * set on the insertion row. - */ -instance.web.form.AddAnItemList = instance.web.ListView.List.extend({ - pad_table_to: function (count) { - if (!this.view.is_action_enabled('create') || this.is_readonly()) { - this._super(count); - return; - } - - this._super(count > 0 ? count - 1 : 0); - - var self = this; - var columns = _(this.columns).filter(function (column) { - return column.invisible !== '1'; - }).length; - if (this.options.selectable) { columns++; } - if (this.options.deletable) { columns++; } - - var $cell = $('<td>', { - colspan: columns, - 'class': this._add_row_class || '' - }).append( - $('<a>', {href: '#'}).text(_t("Add an item")) - .mousedown(function () { - // FIXME: needs to be an official API somehow - if (self.view.editor.is_editing()) { - self.view.__ignore_blur = true; - } - }) - .click(function (e) { - e.preventDefault(); - e.stopPropagation(); - // FIXME: there should also be an API for that one - if (self.view.editor.form.__blur_timeout) { - clearTimeout(self.view.editor.form.__blur_timeout); - self.view.editor.form.__blur_timeout = false; - } - self.view.ensure_saved().done(function () { - self.view.do_add_record(); - }); - })); - - var $padding = this.$current.find('tr:not([data-id]):first'); - var $newrow = $('<tr>').append($cell); - if ($padding.length) { - $padding.before($newrow); - } else { - this.$current.append($newrow) - } - } -}); - -/* -# 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]; - } -}; -instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ - multi_selection: false, - disable_utility_classes: true, - init: function(field_manager, node) { - this._super(field_manager, node); - this.is_loaded = $.Deferred(); - this.initial_is_loaded = this.is_loaded; - this.form_last_update = $.Deferred(); - this.init_form_last_update = this.form_last_update; - this.is_started = false; - this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation); - this.dataset.o2m = this; - this.dataset.parent_view = this.view; - this.dataset.child_name = this.name; - var self = this; - this.dataset.on('dataset_changed', this, function() { - self.trigger_on_change(); - }); - this.set_value([]); - }, - start: function() { - this._super.apply(this, arguments); - this.$el.addClass('oe_form_field oe_form_field_one2many'); - - var self = this; - - self.load_views(); - var destroy = function() { - self.is_loaded = self.is_loaded.then(function() { - self.viewmanager.destroy(); - return $.when(self.load_views()).done(function() { - self.reload_current_view(); - }); - }); - }; - this.is_loaded.done(function() { - self.on("change:effective_readonly", self, destroy); - }); - this.view.on("on_button_cancel", self, destroy); - this.is_started = true; - this.reload_current_view(); - }, - trigger_on_change: function() { - this.trigger('changed_value'); - }, - load_views: function() { - var self = this; - - var modes = this.node.attrs.mode; - modes = !!modes ? modes.split(",") : ["tree"]; - var views = []; - _.each(modes, function(mode) { - if (! _.include(["list", "tree", "graph", "kanban"], mode)) { - throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode)); - } - var view = { - view_id: false, - view_type: mode == "tree" ? "list" : mode, - options: {} - }; - if (self.field.views && self.field.views[mode]) { - view.embedded_view = self.field.views[mode]; - } - if(view.view_type === "list") { - _.extend(view.options, { - addable: null, - selectable: self.multi_selection, - sortable: true, - import_enabled: false, - deletable: true - }); - if (self.get("effective_readonly")) { - _.extend(view.options, { - deletable: null, - reorderable: false, - }); - } - } else if (view.view_type === "form") { - if (self.get("effective_readonly")) { - view.view_type = 'form'; - } - _.extend(view.options, { - not_interactible_on_create: true, - }); - } else if (view.view_type === "kanban") { - _.extend(view.options, { - confirm_on_delete: false, - }); - if (self.get("effective_readonly")) { - _.extend(view.options, { - action_buttons: false, - quick_creatable: false, - creatable: false, - read_only_mode: true, - }); - } - } - views.push(view); - }); - this.views = views; - - this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {}); - this.viewmanager.o2m = self; - var once = $.Deferred().done(function() { - self.init_form_last_update.resolve(); - }); - var def = $.Deferred().done(function() { - self.initial_is_loaded.resolve(); - }); - this.viewmanager.on("controller_inited", self, function(view_type, controller) { - controller.o2m = self; - if (view_type == "list") { - if (self.get("effective_readonly")) { - controller.on('edit:before', self, function (e) { - e.cancel = true; - }); - _(controller.columns).find(function (column) { - if (!(column instanceof instance.web.list.Handle)) { - return false; - } - column.modifiers.invisible = true; - return true; - }); - } - } else if (view_type === "form") { - if (self.get("effective_readonly")) { - $(".oe_form_buttons", controller.$el).children().remove(); - } - controller.on("load_record", self, function(){ - once.resolve(); - }); - controller.on('pager_action_executed',self,self.save_any_view); - } else if (view_type == "graph") { - self.reload_current_view(); - } - def.resolve(); - }); - this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) { - $.when(self.save_any_view()).done(function() { - if (n_mode === "list") { - $.async_when().done(function() { - self.reload_current_view(); - }); - } - }); - }); - $.async_when().done(function () { - self.viewmanager.appendTo(self.$el); - }); - return def; - }, - reload_current_view: function() { - var self = this; - self.is_loaded = self.is_loaded.then(function() { - var view = self.get_active_view(); - if (view.type === "list") { - return view.controller.reload_content(); - } else if (view.type === "form") { - if (self.dataset.index === null && self.dataset.ids.length >= 1) { - self.dataset.index = 0; - } - var act = function() { - return view.controller.do_show(); - }; - self.form_last_update = self.form_last_update.then(act, act); - return self.form_last_update; - } else if (view.controller.do_search) { - return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []); - } - }, undefined); - return self.is_loaded; - }, - get_active_view: function () { - /** - * Returns the current active view if any. - */ - return (this.viewmanager && this.viewmanager.active_view); - }, - set_value: function(value_) { - value_ = value_ || []; - var self = this; - var view = this.get_active_view(); - this.dataset.reset_ids([]); - var ids; - if(value_.length >= 1 && value_[0] instanceof Array) { - ids = []; - _.each(value_, function(command) { - var obj = {values: command[2]}; - switch (command[0]) { - case commands.CREATE: - obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix); - obj.defaults = {}; - self.dataset.to_create.push(obj); - self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])})); - ids.push(obj.id); - return; - case commands.UPDATE: - obj['id'] = command[1]; - self.dataset.to_write.push(obj); - self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])})); - 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") { - ids = []; - this.dataset.delete_all = true; - _.each(value_, function(command) { - var obj = {values: command}; - obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix); - obj.defaults = {}; - 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; - } - this.trigger_on_change(); - if (this.is_started && !this.no_rerender) { - return self.reload_current_view(); - } else { - return $.when(); - } - }, - 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);})); - }, - commit_value: function() { - return this.save_any_view(); - }, - save_any_view: function() { - var view = this.get_active_view(); - if (view) { - if (this.viewmanager.active_view.type === "form") { - if (view.controller.is_initialized.state() !== 'resolved') { - return $.when(false); - } - return $.when(view.controller.save()); - } else if (this.viewmanager.active_view.type === "list") { - return $.when(view.controller.ensure_saved()); - } - } - return $.when(false); - }, - is_syntax_valid: function() { - var view = this.get_active_view(); - if (!view){ - return true; - } - switch (this.viewmanager.active_view.type) { - case 'form': - return _(view.controller.fields).chain() - .invoke('is_valid') - .all(_.identity) - .value(); - case 'list': - return view.controller.is_valid(); - } - return true; - }, -}); - -instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ - template: 'One2Many.viewmanager', - init: function(parent, dataset, views, flags) { - this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false})); - this.registry = instance.web.views.extend({ - list: 'instance.web.form.One2ManyListView', - form: 'instance.web.form.One2ManyFormView', - }); - this.__ignore_blur = false; - }, - switch_mode: function(mode, unused) { - if (mode !== 'form') { - return this._super(mode, unused); - } - var self = this; - var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null; - var pop = new instance.web.form.FormOpenPopup(this); - pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { - title: _t("Open: ") + self.o2m.string, - create_function: function(data, options) { - return self.o2m.dataset.create(data, options).done(function(r) { - self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); - self.o2m.dataset.trigger("dataset_changed", r); - }); - }, - write_function: function(id, data, options) { - return self.o2m.dataset.write(id, data, {}).done(function() { - self.o2m.reload_current_view(); - }); - }, - alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, - parent_view: self.o2m.view, - child_name: self.o2m.name, - read_function: function() { - return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); - }, - form_view_options: {'not_interactible_on_create':true}, - readonly: self.o2m.get("effective_readonly") - }); - pop.on("elements_selected", self, function() { - self.o2m.reload_current_view(); - }); - }, -}); - -instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({ - get_context: function() { - this.context = this.o2m.build_context(); - return this.context; - } -}); - -instance.web.form.One2ManyListView = instance.web.ListView.extend({ - _template: 'One2Many.listview', - init: function (parent, dataset, view_id, options) { - this._super(parent, dataset, view_id, _.extend(options || {}, { - GroupsType: instance.web.form.One2ManyGroups, - ListType: instance.web.form.One2ManyList - })); - this.on('edit:after', this, this.proxy('_after_edit')); - this.on('save:before cancel:before', this, this.proxy('_before_unedit')); - - this.records - .bind('add', this.proxy("changed_records")) - .bind('remove', this.proxy("changed_records")); - this.on('save:after', this, this.proxy("changed_records")); - }, - start: function () { - var ret = this._super(); - this.$el - .off('mousedown.handleButtons') - .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down')); - return ret; - }, - changed_records: function () { - this.o2m.trigger_on_change(); - }, - is_valid: function () { - var self = this; - if (!this.fields_view || !this.editable()){ - return true; - } - if (_.isEmpty(this.records.records)){ - return true; - } - current_values = {}; - _.each(this.editor.form.fields, function(field){ - field._inhibit_on_change_flag = true; - field.no_rerender = true; - current_values[field.name] = field.get('value'); - }); - var valid = _.every(this.records.records, function(record){ - _.each(self.editor.form.fields, function(field){ - field.set_value(record.attributes[field.name]); - }); - return _.every(self.editor.form.fields, function(field){ - field.process_modifiers(); - field._check_css_flags(); - return field.is_valid(); - }); - }); - _.each(this.editor.form.fields, function(field){ - field.set('value', current_values[field.name]); - field._inhibit_on_change_flag = false; - field.no_rerender = false; - }); - return valid; - }, - do_add_record: function () { - if (this.editable()) { - this._super.apply(this, arguments); - } else { - var self = this; - var pop = new instance.web.form.SelectCreatePopup(this); - pop.select_element( - self.o2m.field.relation, - { - title: _t("Create: ") + self.o2m.string, - initial_view: "form", - alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, - create_function: function(data, options) { - return self.o2m.dataset.create(data, options).done(function(r) { - self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); - self.o2m.dataset.trigger("dataset_changed", r); - }); - }, - read_function: function() { - return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); - }, - parent_view: self.o2m.view, - child_name: self.o2m.name, - form_view_options: {'not_interactible_on_create':true} - }, - self.o2m.build_domain(), - self.o2m.build_context() - ); - pop.on("elements_selected", self, function() { - self.o2m.reload_current_view(); - }); - } - }, - do_activate_record: function(index, id) { - var self = this; - var pop = new instance.web.form.FormOpenPopup(self); - pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { - title: _t("Open: ") + self.o2m.string, - write_function: function(id, data) { - return self.o2m.dataset.write(id, data, {}).done(function() { - self.o2m.reload_current_view(); - }); - }, - alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, - parent_view: self.o2m.view, - child_name: self.o2m.name, - read_function: function() { - return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); - }, - form_view_options: {'not_interactible_on_create':true}, - readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly") - }); - }, - do_button_action: function (name, id, callback) { - if (!_.isNumber(id)) { - instance.webclient.notification.warn( - _t("Action Button"), - _t("The o2m record must be saved before an action can be used")); - return; - } - var parent_form = this.o2m.view; - var self = this; - this.ensure_saved().then(function () { - if (parent_form) - return parent_form.save(); - else - return $.when(); - }).done(function () { - var ds = self.o2m.dataset; - var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) { - return value.length; - }); - if (!self.o2m.options.reload_on_button && !cached_records) { - self.handle_button(name, id, callback); - }else { - self.handle_button(name, id, function(){ - self.o2m.view.reload(); - }); - } - }); - }, - - _after_edit: function () { - this.__ignore_blur = false; - this.editor.form.on('blurred', this, this._on_form_blur); - - // The form's blur thing may be jiggered during the edition setup, - // potentially leading to the o2m instasaving the row. Cancel any - // blurring triggered the edition startup here - this.editor.form.widgetFocused(); - }, - _before_unedit: function () { - this.editor.form.off('blurred', this, this._on_form_blur); - }, - _button_down: function () { - // If a button is clicked (usually some sort of action button), it's - // the button's responsibility to ensure the editable list is in the - // correct state -> ignore form blurring - this.__ignore_blur = true; - }, - /** - * Handles blurring of the nested form (saves the currently edited row), - * unless the flag to ignore the event is set to ``true`` - * - * Makes the internal form go away - */ - _on_form_blur: function () { - if (this.__ignore_blur) { - this.__ignore_blur = false; - return; - } - // FIXME: why isn't there an API for this? - if (this.editor.form.$el.hasClass('oe_form_dirty')) { - this.ensure_saved(); - return; - } - this.cancel_edition(); - }, - keypress_ENTER: function () { - // blurring caused by hitting the [Return] key, should skip the - // autosave-on-blur and let the handler for [Return] do its thing (save - // the current row *anyway*, then create a new one/edit the next one) - this.__ignore_blur = true; - this._super.apply(this, arguments); - }, - do_delete: function (ids) { - var confirm = window.confirm; - window.confirm = function () { return true; }; - try { - return this._super(ids); - } finally { - window.confirm = confirm; - } - }, - reload_record: function (record, options) { - if (!options || !options['do_not_evict']) { - // Evict record.id from cache to ensure it will be reloaded correctly - this.dataset.evict_record(record.get('id')); - } - - return this._super(record); - } -}); -instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({ - setup_resequence_rows: function () { - if (!this.view.o2m.get('effective_readonly')) { - this._super.apply(this, arguments); - } - } -}); -instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({ - _add_row_class: 'oe_form_field_one2many_list_row_add', - is_readonly: function () { - return this.view.o2m.get('effective_readonly'); - }, -}); - -instance.web.form.One2ManyFormView = instance.web.FormView.extend({ - form_template: 'One2Many.formview', - load_form: function(data) { - this._super(data); - var self = this; - this.$buttons.find('button.oe_form_button_create').click(function() { - self.save().done(self.on_button_new); - }); - }, - do_notify_change: function() { - if (this.dataset.parent_view) { - this.dataset.parent_view.do_notify_change(); - } else { - this._super.apply(this, arguments); - } - } -}); - -instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, { - template: "FieldMany2ManyTags", - tag_template: "FieldMany2ManyTag", - init: function() { - this._super.apply(this, arguments); - instance.web.form.CompletionFieldMixin.init.call(this); - this.set({"value": []}); - this._display_orderer = new instance.web.DropMisordered(); - this._drop_shown = false; - }, - initialize_texttext: function(){ - var self = this; - return { - plugins : 'tags arrow autocomplete', - autocomplete: { - render: function(suggestion) { - return $('<span class="text-label"/>'). - data('index', suggestion['index']).html(suggestion['label']); - } - }, - ext: { - autocomplete: { - selectFromDropdown: function() { - this.trigger('hideDropdown'); - var index = Number(this.selectedSuggestionElement().children().children().data('index')); - var data = self.search_result[index]; - if (data.id) { - self.add_id(data.id); - } else { - self.ignore_blur = true; - data.action(); - } - this.trigger('setSuggestions', {result : []}); - }, - }, - tags: { - isTagAllowed: function(tag) { - return !!tag.name; - - }, - removeTag: function(tag) { - var id = tag.data("id"); - self.set({"value": _.without(self.get("value"), id)}); - }, - renderTag: function(stuff) { - return $.fn.textext.TextExtTags.prototype.renderTag. - call(this, stuff).data("id", stuff.id); - }, - }, - itemManager: { - itemToString: function(item) { - return item.name; - }, - }, - core: { - onSetInputData: function(e, data) { - if (data === '') { - this._plugins.autocomplete._suggestions = null; - } - this.input().val(data); - }, - }, - }, - } - }, - initialize_content: function() { - if (this.get("effective_readonly")) - return; - var self = this; - self.ignore_blur = false; - self.$text = this.$("textarea"); - self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) { - var _this = this; - var str = !!data ? data.query || '' : ''; - self.get_search_result(str).done(function(result) { - self.search_result = result; - $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) { - return _.extend(el, {index:i}); - })}); - }); - }).bind('hideDropdown', function() { - self._drop_shown = false; - }).bind('showDropdown', function() { - self._drop_shown = true; - }); - self.tags = self.$text.textext()[0].tags(); - self.$text - .focusin(function () { - self.trigger('focused'); - self.ignore_blur = false; - }) - .focusout(function() { - self.$text.trigger("setInputData", ""); - if (!self.ignore_blur) { - self.trigger('blurred'); - } - }).keydown(function(e) { - if (e.which === $.ui.keyCode.TAB && self._drop_shown) { - self.$text.textext()[0].autocomplete().selectFromDropdown(); - } - }); - }, - // WARNING: duplicated in 4 other M2M widgets - set_value: function(value_) { - value_ = value_ || []; - if (value_.length >= 1 && value_[0] instanceof Array) { - // value_ is a list of m2m commands. We only process - // LINK_TO and REPLACE_WITH in this context - var val = []; - _.each(value_, function (command) { - if (command[0] === commands.LINK_TO) { - val.push(command[1]); // (4, id[, _]) - } else if (command[0] === commands.REPLACE_WITH) { - val = command[2]; // (6, _, ids) - } - }); - value_ = val; - } - this._super(value_); - }, - is_false: function() { - return _(this.get("value")).isEmpty(); - }, - get_value: function() { - var tmp = [commands.replace_with(this.get("value"))]; - return tmp; - }, - get_search_blacklist: function() { - return this.get("value"); - }, - map_tag: function(data){ - return _.map(data, function(el) {return {name: el[1], id:el[0]};}) - }, - get_render_data: function(ids){ - var self = this; - var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); - return dataset.name_get(ids); - }, - render_tag: function(data) { - var self = this; - if (! self.get("effective_readonly")) { - self.tags.containerElement().children().remove(); - self.$('textarea').css("padding-left", "3px"); - self.tags.addTags(self.map_tag(data)); - } else { - self.$el.html(QWeb.render(self.tag_template, {elements: data})); - } - }, - render_value: function() { - var self = this; - var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); - var values = self.get("value"); - var handle_names = function(data) { - if (self.isDestroyed()) - return; - var indexed = {}; - _.each(data, function(el) { - indexed[el[0]] = el; - }); - data = _.map(values, function(el) { return indexed[el]; }); - self.render_tag(data); - } - if (! values || values.length > 0) { - return this._display_orderer.add(self.get_render_data(values)).done(handle_names); - } else { - handle_names([]); - } - }, - add_id: function(id) { - this.set({'value': _.uniq(this.get('value').concat([id]))}); - }, - focus: function () { - var input = this.$text && this.$text[0]; - return input ? input.focus() : false; - }, - set_dimensions: function (height, width) { - this._super(height, width); - this.$("textarea").css({ - width: width, - minHeight: height - }); - }, - _search_create_popup: function() { - self.ignore_blur = true; - return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments); - }, -}); - -/** - widget options: - - reload_on_button: Reload the whole form view if click on a button in a list view. - If you see this options, do not use it, it's basically a dirty hack to make one - precise o2m to behave the way we want. -*/ -instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - multi_selection: false, - disable_utility_classes: true, - init: function(field_manager, node) { - this._super(field_manager, node); - this.is_loaded = $.Deferred(); - this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); - this.dataset.m2m = this; - var self = this; - this.dataset.on('unlink', self, function(ids) { - self.dataset_changed(); - }); - this.set_value([]); - this.list_dm = new instance.web.DropMisordered(); - this.render_value_dm = new instance.web.DropMisordered(); - }, - initialize_content: function() { - var self = this; - - this.$el.addClass('oe_form_field oe_form_field_many2many'); - - this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, { - 'addable': null, - 'deletable': this.get("effective_readonly") ? false : true, - 'selectable': this.multi_selection, - 'sortable': false, - 'reorderable': false, - 'import_enabled': false, - }); - var embedded = (this.field.views || {}).tree; - if (embedded) { - this.list_view.set_embedded_view(embedded); - } - this.list_view.m2m_field = this; - var loaded = $.Deferred(); - this.list_view.on("list_view_loaded", this, function() { - loaded.resolve(); - }); - this.list_view.appendTo(this.$el); - - var old_def = self.is_loaded; - self.is_loaded = $.Deferred().done(function() { - old_def.resolve(); - }); - this.list_dm.add(loaded).then(function() { - self.is_loaded.resolve(); - }); - }, - destroy_content: function() { - this.list_view.destroy(); - this.list_view = undefined; - }, - // WARNING: duplicated in 4 other M2M widgets - set_value: function(value_) { - value_ = value_ || []; - if (value_.length >= 1 && value_[0] instanceof Array) { - // value_ is a list of m2m commands. We only process - // LINK_TO and REPLACE_WITH in this context - var val = []; - _.each(value_, function (command) { - if (command[0] === commands.LINK_TO) { - val.push(command[1]); // (4, id[, _]) - } else if (command[0] === commands.REPLACE_WITH) { - val = command[2]; // (6, _, ids) - } - }); - value_ = val; - } - this._super(value_); - }, - get_value: function() { - return [commands.replace_with(this.get('value'))]; - }, - is_false: function () { - return _(this.get("value")).isEmpty(); - }, - render_value: function() { - var self = this; - this.dataset.set_ids(this.get("value")); - this.render_value_dm.add(this.is_loaded).then(function() { - return self.list_view.reload_content(); - }); - }, - dataset_changed: function() { - this.internal_set_value(this.dataset.ids); - }, -}); - -instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({ - get_context: function() { - this.context = this.m2m.build_context(); - return this.context; - } -}); - -/** - * @class - * @extends instance.web.ListView - */ -instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{ - init: function (parent, dataset, view_id, options) { - this._super(parent, dataset, view_id, _.extend(options || {}, { - ListType: instance.web.form.Many2ManyList, - })); - }, - do_add_record: function () { - var pop = new instance.web.form.SelectCreatePopup(this); - pop.select_element( - this.model, - { - title: _t("Add: ") + this.m2m_field.string, - alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views["form"] : undefined, - no_create: this.m2m_field.options.no_create, - }, - new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]), - this.m2m_field.build_context() - ); - var self = this; - pop.on("elements_selected", self, function(element_ids) { - var reload = false; - _(element_ids).each(function (id) { - if(! _.detect(self.dataset.ids, function(x) {return x == id;})) { - self.dataset.set_ids(self.dataset.ids.concat([id])); - self.m2m_field.dataset_changed(); - reload = true; - } - }); - if (reload) { - self.reload_content(); - } - }); - }, - do_activate_record: function(index, id) { - var self = this; - var pop = new instance.web.form.FormOpenPopup(this); - pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), { - title: _t("Open: ") + this.m2m_field.string, - alternative_form_view: this.m2m_field.field.views ? this.m2m_field.field.views["form"] : undefined, - readonly: this.getParent().get("effective_readonly") - }); - pop.on('write_completed', self, self.reload_content); - }, - do_button_action: function(name, id, callback) { - var self = this; - var _sup = _.bind(this._super, this); - if (! this.m2m_field.options.reload_on_button) { - return _sup(name, id, callback); - } else { - return this.m2m_field.view.save().then(function() { - return _sup(name, id, function() { - self.m2m_field.view.reload(); - }); - }); - } - }, - is_action_enabled: function () { return true; }, -}); -instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({ - _add_row_class: 'oe_form_field_many2many_list_row_add', - is_readonly: function () { - return this.view.m2m_field.get('effective_readonly'); - } -}); - -instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, { - disable_utility_classes: true, - init: function(field_manager, node) { - this._super(field_manager, node); - instance.web.form.CompletionFieldMixin.init.call(this); - m2m_kanban_lazy_init(); - this.is_loaded = $.Deferred(); - this.initial_is_loaded = this.is_loaded; - - var self = this; - this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); - this.dataset.m2m = this; - this.dataset.on('unlink', self, function(ids) { - self.dataset_changed(); - }); - }, - start: function() { - this._super.apply(this, arguments); - - var self = this; - - self.load_view(); - self.on("change:effective_readonly", self, function() { - self.is_loaded = self.is_loaded.then(function() { - self.kanban_view.destroy(); - return $.when(self.load_view()).done(function() { - self.render_value(); - }); - }); - }); - }, - // WARNING: duplicated in 4 other M2M widgets - set_value: function(value_) { - value_ = value_ || []; - if (value_.length >= 1 && value_[0] instanceof Array) { - // value_ is a list of m2m commands. We only process - // LINK_TO and REPLACE_WITH in this context - var val = []; - _.each(value_, function (command) { - if (command[0] === commands.LINK_TO) { - val.push(command[1]); // (4, id[, _]) - } else if (command[0] === commands.REPLACE_WITH) { - val = command[2]; // (6, _, ids) - } - }); - value_ = val; - } - this._super(value_); - }, - get_value: function() { - return [commands.replace_with(this.get('value'))]; - }, - load_view: function() { - var self = this; - this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, { - 'create_text': _t("Add"), - 'creatable': self.get("effective_readonly") ? false : true, - 'quick_creatable': self.get("effective_readonly") ? false : true, - 'read_only_mode': self.get("effective_readonly") ? true : false, - 'confirm_on_delete': false, - }); - var embedded = (this.field.views || {}).kanban; - if (embedded) { - this.kanban_view.set_embedded_view(embedded); - } - this.kanban_view.m2m = this; - var loaded = $.Deferred(); - this.kanban_view.on("kanban_view_loaded",self,function() { - self.initial_is_loaded.resolve(); - loaded.resolve(); - }); - this.kanban_view.on('switch_mode', this, this.open_popup); - $.async_when().done(function () { - self.kanban_view.appendTo(self.$el); - }); - return loaded; - }, - render_value: function() { - var self = this; - this.dataset.set_ids(this.get("value")); - this.is_loaded = this.is_loaded.then(function() { - return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []); - }); - }, - dataset_changed: function() { - this.set({'value': this.dataset.ids}); - }, - open_popup: function(type, unused) { - if (type !== "form") - return; - var self = this; - var pop; - if (this.dataset.index === null) { - pop = new instance.web.form.SelectCreatePopup(this); - pop.select_element( - this.field.relation, - { - title: _t("Add: ") + this.string - }, - new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]), - this.build_context() - ); - pop.on("elements_selected", self, function(element_ids) { - _.each(element_ids, function(one_id) { - if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) { - self.dataset.set_ids([].concat(self.dataset.ids, [one_id])); - self.dataset_changed(); - self.render_value(); - } - }); - }); - } else { - var id = self.dataset.ids[self.dataset.index]; - pop = new instance.web.form.FormOpenPopup(this); - pop.show_element(self.field.relation, id, self.build_context(), { - title: _t("Open: ") + self.string, - write_function: function(id, data, options) { - return self.dataset.write(id, data, {}).done(function() { - self.render_value(); - }); - }, - alternative_form_view: self.field.views ? self.field.views["form"] : undefined, - parent_view: self.view, - child_name: self.name, - readonly: self.get("effective_readonly") - }); - } - }, - add_id: function(id) { - this.quick_create.add_id(id); - }, -}); - -function m2m_kanban_lazy_init() { -if (instance.web.form.Many2ManyKanbanView) - return; -instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({ - quick_create_class: 'instance.web.form.Many2ManyQuickCreate', - _is_quick_create_enabled: function() { - return this._super() && ! this.group_by; - }, -}); -instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({ - template: 'Many2ManyKanban.quick_create', - - /** - * close_btn: If true, the widget will display a "Close" button able to trigger - * a "close" event. - */ - init: function(parent, dataset, context, buttons) { - this._super(parent); - this.m2m = this.getParent().view.m2m; - this.m2m.quick_create = this; - this._dataset = dataset; - this._buttons = buttons || false; - this._context = context || {}; - }, - start: function () { - var self = this; - self.$text = this.$el.find('input').css("width", "200px"); - self.$text.textext({ - plugins : 'arrow autocomplete', - autocomplete: { - render: function(suggestion) { - return $('<span class="text-label"/>'). - data('index', suggestion['index']).html(suggestion['label']); - } - }, - ext: { - autocomplete: { - selectFromDropdown: function() { - $(this).trigger('hideDropdown'); - var index = Number(this.selectedSuggestionElement().children().children().data('index')); - var data = self.search_result[index]; - if (data.id) { - self.add_id(data.id); - } else { - data.action(); - } - }, - }, - itemManager: { - itemToString: function(item) { - return item.name; - }, - }, - }, - }).bind('getSuggestions', function(e, data) { - var _this = this; - var str = !!data ? data.query || '' : ''; - self.m2m.get_search_result(str).done(function(result) { - self.search_result = result; - $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) { - return _.extend(el, {index:i}); - })}); - }); - }); - self.$text.focusout(function() { - self.$text.val(""); - }); - }, - focus: function() { - this.$text[0].focus(); - }, - add_id: function(id) { - var self = this; - self.$text.val(""); - self.trigger('added', id); - this.m2m.dataset_changed(); - }, -}); -} - -/** - * Class with everything which is common between FormOpenPopup and SelectCreatePopup. - */ -instance.web.form.AbstractFormPopup = instance.web.Widget.extend({ - template: "AbstractFormPopup.render", - /** - * options: - * -readonly: only applicable when not in creation mode, default to false - * - alternative_form_view - * - view_id - * - write_function - * - read_function - * - create_function - * - parent_view - * - child_name - * - form_view_options - */ - init_popup: function(model, row_id, domain, context, options) { - this.row_id = row_id; - this.model = model; - this.domain = domain || []; - this.context = context || {}; - this.options = options; - _.defaults(this.options, {}); - }, - init_dataset: function() { - var self = this; - this.created_elements = []; - this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context); - this.dataset.read_function = this.options.read_function; - this.dataset.create_function = function(data, options, sup) { - var fct = self.options.create_function || sup; - return fct.call(this, data, options).done(function(r) { - self.trigger('create_completed saved', r); - self.created_elements.push(r); - }); - }; - this.dataset.write_function = function(id, data, options, sup) { - var fct = self.options.write_function || sup; - return fct.call(this, id, data, options).done(function(r) { - self.trigger('write_completed saved', r); - }); - }; - this.dataset.parent_view = this.options.parent_view; - this.dataset.child_name = this.options.child_name; - }, - display_popup: function() { - var self = this; - this.renderElement(); - var dialog = new instance.web.Dialog(this, { - dialogClass: 'oe_act_window', - title: this.options.title || "", - }, this.$el).open(); - dialog.on('closing', this, function (e){ - self.check_exit(true); - }); - this.$buttonpane = dialog.$buttons; - this.start(); - }, - setup_form_view: function() { - var self = this; - if (this.row_id) { - this.dataset.ids = [this.row_id]; - this.dataset.index = 0; - } else { - this.dataset.index = null; - } - var options = _.clone(self.options.form_view_options) || {}; - if (this.row_id !== null) { - options.initial_mode = this.options.readonly ? "view" : "edit"; - } - _.extend(options, { - $buttons: this.$buttonpane, - }); - this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options); - if (this.options.alternative_form_view) { - this.view_form.set_embedded_view(this.options.alternative_form_view); - } - this.view_form.appendTo(this.$(".oe_popup_form").show()); - this.view_form.on("form_view_loaded", self, function() { - var multi_select = self.row_id === null && ! self.options.disable_multiple_selection; - self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", { - multi_select: multi_select, - readonly: self.row_id !== null && self.options.readonly, - })); - var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new"); - $snbutton.click(function() { - $.when(self.view_form.save()).done(function() { - self.view_form.reload_mutex.exec(function() { - self.view_form.on_button_new(); - }); - }); - }); - var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save"); - $sbutton.click(function() { - $.when(self.view_form.save()).done(function() { - self.view_form.reload_mutex.exec(function() { - self.check_exit(); - }); - }); - }); - var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close"); - $cbutton.click(function() { - self.view_form.trigger('on_button_cancel'); - self.check_exit(); - }); - self.view_form.do_show(); - }); - }, - select_elements: function(element_ids) { - this.trigger("elements_selected", element_ids); - }, - check_exit: function(no_destroy) { - if (this.created_elements.length > 0) { - this.select_elements(this.created_elements); - this.created_elements = []; - } - this.trigger('closed'); - this.destroy(); - }, - destroy: function () { - this.trigger('closed'); - if (this.$el.is(":data(bs.modal)")) { - this.$el.parents('.modal').modal('hide'); - } - this._super(); - }, -}); - -/** - * Class to display a popup containing a form view. - */ -instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({ - show_element: function(model, row_id, context, options) { - this.init_popup(model, row_id, [], context, options); - _.defaults(this.options, { - }); - this.display_popup(); - }, - start: function() { - this._super(); - this.init_dataset(); - this.setup_form_view(); - }, -}); - -/** - * Class to display a popup to display a list to search a row. It also allows - * to switch to a form view to create a new row. - */ -instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({ - /** - * options: - * - initial_ids - * - initial_view: form or search (default search) - * - disable_multiple_selection - * - list_view_options - */ - select_element: function(model, options, domain, context) { - this.init_popup(model, null, domain, context, options); - var self = this; - _.defaults(this.options, { - initial_view: "search", - }); - this.initial_ids = this.options.initial_ids; - this.display_popup(); - }, - start: function() { - this.init_dataset(); - if (this.options.initial_view == "search") { - var context = instance.web.pyeval.sync_eval_domains_and_contexts({ - domains: [], - contexts: [this.context] - }).context; - var search_defaults = {}; - _.each(context, function (value_, key) { - var match = /^search_default_(.*)$/.exec(key); - if (match) { - search_defaults[match[1]] = value_; - } - }); - this.setup_search_view(search_defaults); - } else { // "form" - this.new_object(); - } - }, - setup_search_view: function(search_defaults) { - var self = this; - if (this.searchview) { - this.searchview.destroy(); - } - var $buttons = this.$('.o-search-options'); - this.searchview = new instance.web.SearchView(this, - this.dataset, false, search_defaults, {$buttons: $buttons}); - this.searchview.on('search_data', self, function(domains, contexts, groupbys) { - if (self.initial_ids) { - self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]), - contexts.concat(self.context), groupbys); - self.initial_ids = undefined; - } else { - self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys); - } - }); - this.searchview.appendTo(this.$(".o-popup-search")).done(function() { - self.searchview.toggle_visibility(true); - self.view_list = new instance.web.form.SelectCreateListView(self, - self.dataset, false, - _.extend({'deletable': false, - 'selectable': !self.options.disable_multiple_selection, - 'import_enabled': false, - '$buttons': self.$buttonpane, - 'disable_editable_mode': true, - '$pager': self.$('.oe_popup_list_pager'), - }, self.options.list_view_options || {})); - self.view_list.on('edit:before', self, function (e) { - e.cancel = true; - }); - self.view_list.popup = self; - self.view_list.appendTo(self.$(".oe_popup_list").show()).then(function() { - self.view_list.do_show(); - }).then(function() { - if (self.options.initial_facet) { - self.searchview.query.reset([self.options.initial_facet], { - preventSearch: true, - }); - } - self.searchview.do_search(); - }); - self.view_list.on("list_view_loaded", self, function() { - self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self})); - var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close"); - $cbutton.click(function() { - self.destroy(); - }); - var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select"); - $sbutton.click(function() { - self.select_elements(self.selected_ids); - self.destroy(); - }); - $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create"); - $cbutton.click(function() { - self.new_object(); - }); - }); - }); - }, - do_search: function(domains, contexts, groupbys) { - var results = instance.web.pyeval.sync_eval_domains_and_contexts({ - domains: domains || [], - contexts: contexts || [], - group_by_seq: groupbys || [] - }); - this.view_list.do_search(results.domain, results.context, results.group_by); - }, - on_click_element: function(ids) { - var self = this; - this.selected_ids = ids || []; - if(this.selected_ids.length > 0) { - self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled'); - } else { - self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled"); - } - }, - new_object: function() { - if (this.searchview) { - this.searchview.do_hide(); - } - if (this.view_list) { - this.view_list.do_hide(); - this.view_list.$el.parent().hide(); - } - this.setup_form_view(); - }, -}); - -instance.web.form.DomainEditorPopup = instance.web.form.SelectCreatePopup.extend({ - select_element: function (model, options, domain, context) { - if (options.readonly) { - return this._super(model, options, domain, context); - } - options.initial_facet = { - category: _t("Custom Filter"), - icon: 'fa-star', - field: { - get_context: function () { return context; }, - get_groupby: function () { return []; }, - get_domain: function () { return domain; }, - }, - values: [{label: _t("Selected domain"), value: null}], - }; - this._super(model, options, [], context); - }, - get_domain: function (selected_ids) { - var group_domain = [], - domain; - if (this.$('input.oe_list_record_selector').prop('checked')) { - if (this.view_list.grouped) { - var group_domain = _.chain(_.values(this.view_list.groups.children)) - .filter(function (child) { return child.records.length; }) - .map(function (c) { return c.datagroup.domain;}) - .value(); - group_domain = _.flatten(group_domain, true); - group_domain = _.times(group_domain.length - 1, _.constant('|')).concat(group_domain); - } - var search_data = this.searchview.build_search_data(); - domain = instance.web.pyeval.sync_eval_domains_and_contexts({ - domains: search_data.domains, - contexts: search_data.contexts, - group_by_seq: search_data.groupbys || [] - }).domain; - } - else { - domain = [["id", "in", selected_ids]]; - } - return this.dataset.domain.concat(group_domain).concat(domain || []); - }, -}); - -instance.web.form.SelectCreateListView = instance.web.ListView.extend({ - do_add_record: function () { - this.popup.new_object(); - }, - select_record: function(index) { - this.popup.select_elements([this.dataset.ids[index]]); - this.popup.destroy(); - }, - do_select: function(ids, records) { - this._super(ids, records); - this.popup.on_click_element(ids); - } -}); - -instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - template: 'FieldReference', - init: function(field_manager, node) { - this._super(field_manager, node); - this.reference_ready = true; - }, - destroy_content: function() { - if (this.fm) { - this.fm.destroy(); - this.fm = undefined; - } - }, - initialize_content: function() { - var self = this; - var fm = new instance.web.form.DefaultFieldManager(this); - this.fm = fm; - fm.extend_field_desc({ - "selection": { - selection: this.field_manager.get_field_desc(this.name).selection, - type: "selection", - }, - "m2o": { - relation: null, - type: "many2one", - }, - }); - this.selection = new instance.web.form.FieldSelection(fm, { attrs: { - name: 'selection', - modifiers: JSON.stringify({readonly: this.get('effective_readonly')}), - }}); - this.selection.on("change:value", this, this.on_selection_changed); - this.selection.appendTo(this.$(".oe_form_view_reference_selection")); - this.selection - .on('focused', null, function () {self.trigger('focused');}) - .on('blurred', null, function () {self.trigger('blurred');}); - - this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: { - name: 'Referenced Document', - modifiers: JSON.stringify({readonly: this.get('effective_readonly')}), - }}); - this.m2o.on("change:value", this, this.data_changed); - this.m2o.appendTo(this.$(".oe_form_view_reference_m2o")); - this.m2o - .on('focused', null, function () {self.trigger('focused');}) - .on('blurred', null, function () {self.trigger('blurred');}); - }, - on_selection_changed: function() { - if (this.reference_ready) { - this.internal_set_value([this.selection.get_value(), false]); - this.render_value(); - } - }, - data_changed: function() { - if (this.reference_ready) { - this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]); - } - }, - set_value: function(val) { - if (val) { - val = val.split(','); - val[0] = val[0] || false; - val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false; - } - this._super(val || [false, false]); - }, - get_value: function() { - return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false; - }, - render_value: function() { - this.reference_ready = false; - if (!this.get("effective_readonly")) { - this.selection.set_value(this.get('value')[0]); - } - this.m2o.field.relation = this.get('value')[0]; - this.m2o.set_value(this.get('value')[1]); - this.m2o.$el.toggle(!!this.get('value')[0]); - this.reference_ready = true; - }, -}); - -instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - init: function(field_manager, node) { - var self = this; - this._super(field_manager, node); - this.binary_value = false; - this.useFileAPI = !!window.FileReader; - this.max_upload_size = 25 * 1024 * 1024; // 25Mo - if (!this.useFileAPI) { - this.fileupload_id = _.uniqueId('oe_fileupload'); - $(window).on(this.fileupload_id, function() { - var args = [].slice.call(arguments).slice(1); - self.on_file_uploaded.apply(self, args); - }); - } - }, - stop: function() { - if (!this.useFileAPI) { - $(window).off(this.fileupload_id); - } - this._super.apply(this, arguments); - }, - initialize_content: function() { - var self= this; - this.$el.find('input.oe_form_binary_file').change(this.on_file_change); - this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as); - this.$el.find('.oe_form_binary_file_clear').click(this.on_clear); - this.$el.find('.oe_form_binary_file_edit').click(function(event){ - self.$el.find('input.oe_form_binary_file').click(); - }); - }, - on_file_change: function(e) { - var self = this; - var file_node = e.target; - if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) { - if (this.useFileAPI) { - var file = file_node.files[0]; - if (file.size > this.max_upload_size) { - var msg = _t("The selected file exceed the maximum file size of %s."); - instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size))); - return false; - } - var filereader = new FileReader(); - filereader.readAsDataURL(file); - filereader.onloadend = function(upload) { - var data = upload.target.result; - data = data.split(',')[1]; - self.on_file_uploaded(file.size, file.name, file.type, data); - }; - } else { - this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id); - this.$el.find('form.oe_form_binary_form').submit(); - } - this.$el.find('.oe_form_binary_progress').show(); - this.$el.find('.oe_form_binary').hide(); - } - }, - on_file_uploaded: function(size, name, content_type, file_base64) { - if (size === false) { - this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file")); - // TODO: use openerp web crashmanager - console.warn("Error while uploading file : ", name); - } else { - this.filename = name; - this.on_file_uploaded_and_valid.apply(this, arguments); - } - this.$el.find('.oe_form_binary_progress').hide(); - this.$el.find('.oe_form_binary').show(); - }, - on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { - }, - on_save_as: function(ev) { - var value = this.get('value'); - if (!value) { - this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !")); - ev.stopPropagation(); - } else { - instance.web.blockUI(); - var c = instance.webclient.crashmanager; - var filename_fieldname = this.node.attrs.filename; - var filename_field = this.view.fields && this.view.fields[filename_fieldname]; - this.session.get_file({ - url: '/web/binary/saveas_ajax', - data: {data: JSON.stringify({ - model: this.view.dataset.model, - id: (this.view.datarecord.id || ''), - field: this.name, - filename_field: (filename_fieldname || ''), - data: instance.web.form.is_bin_size(value) ? null : value, - filename: filename_field ? filename_field.get('value') : null, - context: this.view.dataset.get_context() - })}, - complete: instance.web.unblockUI, - error: c.rpc_error.bind(c) - }); - ev.stopPropagation(); - return false; - } - }, - set_filename: function(value) { - var filename = this.node.attrs.filename; - if (filename) { - var tmp = {}; - tmp[filename] = value; - this.field_manager.set_values(tmp); - } - }, - on_clear: function() { - if (this.get('value') !== false) { - this.binary_value = false; - this.internal_set_value(false); - } - return false; - } -}); - -instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ - template: 'FieldBinaryFile', - initialize_content: function() { - this._super(); - if (this.get("effective_readonly")) { - var self = this; - this.$el.find('a').click(function(ev) { - if (self.get('value')) { - self.on_save_as(ev); - } - return false; - }); - } - }, - render_value: function() { - var show_value; - if (!this.get("effective_readonly")) { - if (this.node.attrs.filename) { - show_value = this.view.datarecord[this.node.attrs.filename] || ''; - } else { - show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : ''; - } - this.$el.find('input').eq(0).val(show_value); - } else { - this.$el.find('a').toggle(!!this.get('value')); - if (this.get('value')) { - show_value = _t("Download"); - if (this.view) - show_value += " " + (this.view.datarecord[this.node.attrs.filename] || ''); - this.$el.find('a').text(show_value); - } - } - }, - on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { - this.binary_value = true; - this.internal_set_value(file_base64); - var show_value = name + " (" + instance.web.human_size(size) + ")"; - this.$el.find('input').eq(0).val(show_value); - this.set_filename(name); - }, - on_clear: function() { - this._super.apply(this, arguments); - this.$el.find('input').eq(0).val(''); - this.set_filename(''); - } -}); - -instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ - template: 'FieldBinaryImage', - placeholder: "/web/static/src/img/placeholder.png", - render_value: function() { - var self = this; - var url; - if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) { - url = 'data:image/png;base64,' + this.get('value'); - } else if (this.get('value')) { - var id = JSON.stringify(this.view.datarecord.id || null); - var field = this.name; - if (this.options.preview_image) - field = this.options.preview_image; - url = this.session.url('/web/binary/image', { - model: this.view.dataset.model, - id: id, - field: field, - t: (new Date().getTime()), - }); - } else { - url = this.placeholder; - } - var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url })); - $($img).click(function(e) { - if(self.view.get("actual_mode") == "view") { - var $button = $(".oe_form_button_edit"); - $button.openerpBounce(); - e.stopPropagation(); - } - }); - this.$el.find('> img').remove(); - this.$el.prepend($img); - $img.load(function() { - if (! self.options.size) - return; - $img.css("max-width", "" + self.options.size[0] + "px"); - $img.css("max-height", "" + self.options.size[1] + "px"); - }); - $img.on('error', function() { - $img.attr('src', self.placeholder); - instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image.")); - }); - }, - on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { - this.internal_set_value(file_base64); - this.binary_value = true; - this.render_value(); - this.set_filename(name); - }, - on_clear: function() { - this._super.apply(this, arguments); - this.render_value(); - this.set_filename(''); - }, - set_value: function(value_){ - var changed = value_ !== this.get_value(); - this._super.apply(this, arguments); - // By default, on binary images read, the server returns the binary size - // This is possible that two images have the exact same size - // Therefore we trigger the change in case the image value hasn't changed - // So the image is re-rendered correctly - if (!changed){ - this.trigger("change:value", this, { - oldValue: value_, - newValue: value_ - }); - } - } -}); - -/** - * Widget for (many2many field) to upload one or more file in same time and display in list. - * The user can delete his files. - * Options on attribute ; "blockui" {Boolean} block the UI or not - * during the file is uploading - */ -instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - template: "FieldBinaryFileUploader", - init: function(field_manager, node) { - this._super(field_manager, node); - this.field_manager = field_manager; - this.node = node; - if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') { - throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string); - } - this.data = {}; - this.set_value([]); - this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment'); - this.fileupload_id = _.uniqueId('oe_fileupload_temp'); - $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this)); - }, - initialize_content: function() { - this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change ); - }, - // WARNING: duplicated in 4 other M2M widgets - set_value: function(value_) { - value_ = value_ || []; - if (value_.length >= 1 && value_[0] instanceof Array) { - // value_ is a list of m2m commands. We only process - // LINK_TO and REPLACE_WITH in this context - var val = []; - _.each(value_, function (command) { - if (command[0] === commands.LINK_TO) { - val.push(command[1]); // (4, id[, _]) - } else if (command[0] === commands.REPLACE_WITH) { - val = command[2]; // (6, _, ids) - } - }); - value_ = val; - } - this._super(value_); - }, - get_value: function() { - var tmp = [commands.replace_with(this.get("value"))]; - return tmp; - }, - get_file_url: function (attachment) { - return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']}); - }, - read_name_values : function () { - var self = this; - // don't reset know values - var ids = this.get('value'); - var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } ); - // send request for get_name - if (_value.length) { - return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) { - _.each(datas, function (data) { - data.no_unlink = true; - data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id}); - self.data[data.id] = data; - }); - return ids; - }); - } else { - return $.when(ids); - } - }, - render_value: function () { - var self = this; - this.read_name_values().then(function (ids) { - var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids})); - render.on('click', '.oe_delete', _.bind(self.on_file_delete, self)); - self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render ); - - // reinit input type file - var $input = self.$('input.oe_form_binary_file'); - $input.after($input.clone(true)).remove(); - self.$(".oe_fileupload").show(); - - }); - }, - on_file_change: function (event) { - event.stopPropagation(); - var self = this; - var $target = $(event.target); - if ($target.val() !== '') { - var filename = $target.val().replace(/.*[\\\/]/,''); - // don't uplode more of one file in same time - if (self.data[0] && self.data[0].upload ) { - return false; - } - for (var id in this.get('value')) { - // if the files exits, delete the file before upload (if it's a new file) - if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) { - self.ds_file.unlink([id]); - } - } - - // block UI or not - if(this.node.attrs.blockui>0) { - instance.web.blockUI(); - } - - // TODO : unactivate send on wizard and form - - // submit file - this.$('form.oe_form_binary_form').submit(); - this.$(".oe_fileupload").hide(); - // add file on data result - this.data[0] = { - 'id': 0, - 'name': filename, - 'filename': filename, - 'url': '', - 'upload': true - }; - } - }, - on_file_loaded: function (event, result) { - var files = this.get('value'); - - // unblock UI - if(this.node.attrs.blockui>0) { - instance.web.unblockUI(); - } - - if (result.error || !result.id ) { - this.do_warn( _t('Uploading Error'), result.error); - delete this.data[0]; - } else { - if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) { - delete this.data[0]; - this.data[result.id] = { - 'id': result.id, - 'name': result.name, - 'filename': result.filename, - 'url': this.get_file_url(result) - }; - } else { - this.data[result.id] = { - 'id': result.id, - 'name': result.name, - 'filename': result.filename, - 'url': this.get_file_url(result) - }; - } - var values = _.clone(this.get('value')); - values.push(result.id); - this.set({'value': values}); - } - this.render_value(); - }, - on_file_delete: function (event) { - event.stopPropagation(); - var file_id=$(event.target).data("id"); - if (file_id) { - var files = _.filter(this.get('value'), function (id) {return id != file_id;}); - if(!this.data[file_id].no_unlink) { - this.ds_file.unlink([file_id]); - } - this.set({'value': files}); - } - }, -}); - -instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ - template: "FieldStatus", - init: function(field_manager, node) { - this._super(field_manager, node); - this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false; - this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false; - this.set({value: false}); - this.selection = {'unfolded': [], 'folded': []}; - this.set("selection", {'unfolded': [], 'folded': []}); - this.selection_dm = new instance.web.DropMisordered(); - this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context()); - }, - start: function() { - this.field_manager.on("view_content_has_changed", this, this.calc_domain); - this.calc_domain(); - this.on("change:value", this, this.get_selection); - this.on("change:evaluated_selection_domain", this, this.get_selection); - this.on("change:selection", this, function() { - this.selection = this.get("selection"); - this.render_value(); - }); - this.get_selection(); - if (this.options.clickable) { - this.$el.on('click','li[data-id]',this.on_click_stage); - } - if (this.$el.parent().is('header')) { - this.$el.after('<div class="oe_clear"/>'); - } - this._super(); - }, - set_value: function(value_) { - if (value_ instanceof Array) { - value_ = value_[0]; - } - this._super(value_); - }, - render_value: function() { - var self = this; - var content = QWeb.render("FieldStatus.content", { - 'widget': self, - 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');}) - }); - self.$el.html(content); - }, - calc_domain: function() { - var d = instance.web.pyeval.eval('domain', this.build_domain()); - var domain = []; //if there is no domain defined, fetch all the records - - if (d.length) { - domain = ['|',['id', '=', this.get('value')]].concat(d); - } - - if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) { - this.set("evaluated_selection_domain", domain); - } - }, - /** Get the selection and render it - * selection: [[identifier, value_to_display], ...] - * For selection fields: this is directly given by this.field.selection - * For many2one fields: perform a search on the relation of the many2one field - */ - get_selection: function() { - var self = this; - var selection_unfolded = []; - var selection_folded = []; - var fold_field = this.options.fold_field; - - var calculation = _.bind(function() { - if (this.field.type == "many2one") { - return self.get_distant_fields().then(function (fields) { - return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain")) - .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) { - var ids = _.pluck(records, 'id'); - return self.dataset.name_get(ids).then(function (records_name) { - _.each(records, function (record) { - var name = _.find(records_name, function (val) {return val[0] == record.id;})[1]; - if (fold_field && record[fold_field] && record.id != self.get('value')) { - selection_folded.push([record.id, name]); - } else { - selection_unfolded.push([record.id, name]); - } - }); - }); - }); - }); - } else { - // For field type selection filter values according to - // statusbar_visible attribute of the field. For example: - // statusbar_visible="draft,open". - var select = this.field.selection; - for(var i=0; i < select.length; i++) { - var key = select[i][0]; - if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) { - selection_unfolded.push(select[i]); - } - } - return $.when(); - } - }, this); - this.selection_dm.add(calculation()).then(function () { - var selection = {'unfolded': selection_unfolded, 'folded': selection_folded}; - if (! _.isEqual(selection, self.get("selection"))) { - self.set("selection", selection); - } - }); - }, - /* - * :deprecated: this feature will probably be removed with OpenERP v8 - */ - get_distant_fields: function() { - var self = this; - if (! this.options.fold_field) { - this.distant_fields = {} - } - if (this.distant_fields) { - return $.when(this.distant_fields); - } - return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) { - self.distant_fields = fields; - return fields; - }); - }, - on_click_stage: function (ev) { - var self = this; - var $li = $(ev.currentTarget); - var val; - if (this.field.type == "many2one") { - val = parseInt($li.data("id"), 10); - } - else { - val = $li.data("id"); - } - if (val != self.get('value')) { - this.view.recursive_save().done(function() { - var change = {}; - change[self.name] = val; - self.view.dataset.write(self.view.datarecord.id, change).done(function() { - self.view.reload(); - }); - }); - } - }, -}); - -instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({ - template: "FieldMonetary", - widget_class: 'oe_form_field_float oe_form_field_monetary', - init: function() { - this._super.apply(this, arguments); - this.set({"currency": false}); - if (this.options.currency_field) { - this.field_manager.on("field_changed:" + this.options.currency_field, this, function() { - this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)}); - }); - } - this.on("change:currency", this, this.get_currency_info); - this.get_currency_info(); - this.ci_dm = new instance.web.DropMisordered(); - }, - start: function() { - var tmp = this._super(); - this.on("change:currency_info", this, this.reinitialize); - return tmp; - }, - get_currency_info: function() { - var self = this; - if (this.get("currency") === false) { - this.set({"currency_info": null}); - return; - } - return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"]) - .filter([["id", "=", self.get("currency")]]).first())).then(function(res) { - self.set({"currency_info": res}); - }); - }, - parse_value: function(val, def) { - return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def); - }, - format_value: function(val, def) { - return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def); - }, -}); - -/* - This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each - record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records - will be added to the relation. -*/ -instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - className: "oe_form_many2many_checkboxes", - init: function() { - this._super.apply(this, arguments); - this.set("value", {}); - this.set("records", []); - this.field_manager.on("view_content_has_changed", this, function() { - var domain = new openerp.web.CompoundDomain(this.build_domain()).eval(); - if (! _.isEqual(domain, this.get("domain"))) { - this.set("domain", domain); - } - }); - this.records_orderer = new instance.web.DropMisordered(); - }, - initialize_field: function() { - instance.web.form.ReinitializeFieldMixin.initialize_field.call(this); - this.on("change:domain", this, this.query_records); - this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval()); - this.on("change:records", this, this.render_value); - }, - query_records: function() { - var self = this; - var model = new openerp.Model(openerp.session, this.field.relation); - this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) { - return model.call("name_get", [record_ids] , {"context": self.build_context()}); - })).then(function(res) { - self.set("records", res); - }); - }, - render_value: function() { - this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")})); - var inputs = this.$("input"); - inputs.change(_.bind(this.from_dom, this)); - if (this.get("effective_readonly")) - inputs.attr("disabled", "true"); - }, - from_dom: function() { - var new_value = {}; - this.$("input").each(function() { - var elem = $(this); - new_value[elem.data("record-id")] = elem.is(':checked') ? true : undefined; - }); - if (! _.isEqual(new_value, this.get("value"))) - this.internal_set_value(new_value); - }, - // WARNING: (mostly) duplicated in 4 other M2M widgets - set_value: function(value_) { - value_ = value_ || []; - if (value_.length >= 1 && value_[0] instanceof Array) { - // value_ is a list of m2m commands. We only process - // LINK_TO and REPLACE_WITH in this context - var val = []; - _.each(value_, function (command) { - if (command[0] === commands.LINK_TO) { - val.push(command[1]); // (4, id[, _]) - } else if (command[0] === commands.REPLACE_WITH) { - val = command[2]; // (6, _, ids) - } - }); - value_ = val; - } - var formatted = {}; - _.each(value_, function(el) { - formatted[JSON.stringify(el)] = true; - }); - this._super(formatted); - }, - get_value: function() { - var value = _.filter(_.keys(this.get("value")), function(el) { - return this.get("value")[el]; - }, this); - value = _.map(value, function(el) { - return JSON.parse(el); - }); - return [commands.replace_with(value)]; - }, -}); - -/** - This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is - "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window - action on the model of the relation and show only the linked records. - - Widget options: - - * views: The views to display in the act_window action. Must be a list of tuples whose first element is the id of the view - to display (or False to take the default one) and the second element is the type of the view. Defaults to - [[false, "tree"], [false, "form"]] . -*/ -instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { - className: "oe_form_x2many_counter", - init: function() { - this._super.apply(this, arguments); - this.set("value", []); - _.defaults(this.options, { - "views": [[false, "tree"], [false, "form"]], - }); - }, - render_value: function() { - var text = _.str.sprintf("%d %s", this.val().length, this.string); - this.$().html(QWeb.render("X2ManyCounter", {text: text})); - this.$("a").click(_.bind(this.go_to, this)); - }, - go_to: function() { - return this.view.recursive_save().then(_.bind(function() { - var val = this.val(); - var context = {}; - if (this.field.type === "one2many") { - context["default_" + this.field.relation_field] = this.view.datarecord.id; - } - var domain = [["id", "in", val]]; - return this.do_action({ - type: 'ir.actions.act_window', - name: this.string, - res_model: this.field.relation, - views: this.options.views, - target: 'current', - context: context, - domain: domain, - }); - }, this)); - }, - val: function() { - var value = this.get("value") || []; - if (value.length >= 1 && value[0] instanceof Array) { - value = value[0][2]; - } - return value; - } -}); - -/** - This widget is intended to be used on stat button numeric fields. It will display - the value many2many and one2many. It is a read-only field that will - display a simple string "<value of field> <label of the field>" -*/ -instance.web.form.StatInfo = instance.web.form.AbstractField.extend({ - is_field_number: true, - init: function() { - this._super.apply(this, arguments); - this.internal_set_value(0); - }, - set_value: function(value_) { - if (value_ === false || value_ === undefined) { - value_ = 0; - } - this._super.apply(this, [value_]); - }, - render_value: function() { - var options = { - value: this.get("value") || 0, - }; - if (! this.node.attrs.nolabel) { - if(this.options.label_field && this.view.datarecord[this.options.label_field]) { - options.text = this.view.datarecord[this.options.label_field]; - } - else { - options.text = this.string; - } - } - this.$el.html(QWeb.render("StatInfo", options)); - }, - -}); - -/** - This widget is intended to be used on boolean fields. It toggles a button - switching between a green bullet / gray bullet. -*/ -instance.web.form.FieldToggleBoolean = instance.web.form.AbstractField.extend({ - render_value: function () { - this.icon = this.get_value() ? 'gtk-yes.png' : 'gtk-normal.png'; - this.$el.html(QWeb.render("FieldToggleBoolean", {'widget': this})); - this.$('.oe_toggle_button').on('click', this.set_toggle_button.bind(this)); - }, - set_toggle_button: function () { - var self = this; - var toggle_value = this.get_value() === false ? true: false; - if (this.view.get('actual_mode') == 'view') { - var rec_values = {}; - rec_values[self.node.attrs.name] = toggle_value; - return this.view.dataset._model.call( - 'write', [ - [this.view.datarecord.id], - rec_values, - self.view.dataset.get_context() - ]).done(function () { self.reload_record(); }); - } - else { - this.set_value(toggle_value); - } - }, - reload_record: function () { - this.view.reload(); - }, -}); - -/** - * Registry of form fields, called by :js:`instance.web.FormView`. - * - * All referenced classes must implement FieldInterface. Those represent the classes whose instances - * will substitute to the <field> tags as defined in OpenERP's views. - */ -instance.web.form.widgets = new instance.web.Registry({ - 'char' : 'instance.web.form.FieldChar', - 'id' : 'instance.web.form.FieldID', - 'email' : 'instance.web.form.FieldEmail', - 'url' : 'instance.web.form.FieldUrl', - 'text' : 'instance.web.form.FieldText', - 'html' : 'instance.web.form.FieldTextHtml', - 'char_domain': 'instance.web.form.FieldCharDomain', - 'date' : 'instance.web.form.FieldDate', - 'datetime' : 'instance.web.form.FieldDatetime', - 'selection' : 'instance.web.form.FieldSelection', - 'radio' : 'instance.web.form.FieldRadio', - 'many2one' : 'instance.web.form.FieldMany2One', - 'many2onebutton' : 'instance.web.form.Many2OneButton', - 'many2many' : 'instance.web.form.FieldMany2Many', - 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags', - 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban', - 'one2many' : 'instance.web.form.FieldOne2Many', - 'one2many_list' : 'instance.web.form.FieldOne2Many', - 'reference' : 'instance.web.form.FieldReference', - 'boolean' : 'instance.web.form.FieldBoolean', - 'toggle_button' : 'instance.web.form.FieldToggleBoolean', - 'float' : 'instance.web.form.FieldFloat', - 'percentpie': 'instance.web.form.FieldPercentPie', - 'barchart': 'instance.web.form.FieldBarChart', - 'integer': 'instance.web.form.FieldFloat', - 'float_time': 'instance.web.form.FieldFloat', - 'progressbar': 'instance.web.form.FieldProgressBar', - 'image': 'instance.web.form.FieldBinaryImage', - 'binary': 'instance.web.form.FieldBinaryFile', - 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles', - 'statusbar': 'instance.web.form.FieldStatus', - 'monetary': 'instance.web.form.FieldMonetary', - 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes', - 'x2many_counter': 'instance.web.form.X2ManyCounter', - 'priority':'instance.web.form.Priority', - 'kanban_state_selection':'instance.web.form.KanbanSelection', - 'statinfo': 'instance.web.form.StatInfo', -}); - -/** - * Registry of widgets usable in the form view that can substitute to any possible - * tags defined in OpenERP's form views. - * - * Every referenced class should extend FormWidget. - */ -instance.web.form.tags = new instance.web.Registry({ - 'button' : 'instance.web.form.WidgetButton', -}); - -instance.web.form.custom_widgets = new instance.web.Registry({ -}); - -})(); |
