3 var instance = openerp;
4 var _t = instance.web._t,
5 _lt = instance.web._lt;
6 var QWeb = instance.web.qweb;
9 instance.web.form = {};
12 * Interface implemented by the form view or any other object
13 * able to provide the features necessary for the fields to work.
16 * - display_invalid_fields : if true, all fields where is_valid() return true should
17 * be displayed as invalid.
18 * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
20 * - view_content_has_changed : when the values of the fields have changed. When
21 * this event is triggered all fields should reprocess their modifiers.
22 * - field_changed:<field_name> : when the value of a field change, an event is triggered
23 * named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
24 * This event is not related to the on_change mechanism of OpenERP and is always called
25 * when the value of a field is setted or changed. This event is only triggered when the
26 * value of the field is syntactically valid, but it can be triggered when the value
27 * is sematically invalid (ie, when a required field is false). It is possible that an event
28 * about a precise field is never triggered even if that field exists in the view, in that
29 * case the value of the field is assumed to be false.
31 instance.web.form.FieldManagerMixin = {
33 * Must return the asked field as in fields_get.
35 get_field_desc: function(field_name) {},
37 * Returns the current value of a field present in the view. See the get_value() method
38 * method in FieldInterface for further information.
40 get_field_value: function(field_name) {},
42 Gives new values for the fields contained in the view. The new values could not be setted
43 right after the call to this method. Setting new values can trigger on_changes.
45 @param (dict) values A dictonnary with key = field name and value = new value.
46 @return (Deferred) Is resolved after all the values are setted.
48 set_values: function(values) {},
50 Computes an OpenERP domain.
52 @param (list) expression An OpenERP domain.
53 @return (boolean) The computed value of the domain.
55 compute_domain: function(expression) {},
57 Builds an evaluation context for the resolution of the fields' contexts. Please note
58 the field are only supposed to use this context to evualuate their own, they should not
61 @return (CompoundContext) An OpenERP context.
63 build_eval_context: function() {},
66 instance.web.views.add('form', 'instance.web.FormView');
69 * - actual_mode: always "view", "edit" or "create". Read-only property. Determines
70 * the mode used by the view.
72 instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
74 * Indicates that this view is not searchable, and thus that no search
75 * view should be displayed (if there is one active).
79 display_name: _lt('Form'),
82 * @constructs instance.web.FormView
83 * @extends instance.web.View
85 * @param {instance.web.Session} session the current openerp session
86 * @param {instance.web.DataSet} dataset the dataset this view will work with
87 * @param {String} view_id the identifier of the OpenERP view object
88 * @param {Object} options
89 * - resize_textareas : [true|false|max_height]
91 * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
93 init: function(parent, dataset, view_id, options) {
96 this.ViewManager = parent;
97 this.set_default_options(options);
98 this.dataset = dataset;
99 this.model = dataset.model;
100 this.view_id = view_id || false;
101 this.fields_view = {};
103 this.fields_order = [];
104 this.datarecord = {};
105 this.default_focus_field = null;
106 this.default_focus_button = null;
107 this.fields_registry = instance.web.form.widgets;
108 this.tags_registry = instance.web.form.tags;
109 this.widgets_registry = instance.web.form.custom_widgets;
110 this.has_been_loaded = $.Deferred();
111 this.translatable_fields = [];
112 _.defaults(this.options, {
113 "not_interactible_on_create": false,
114 "initial_mode": "view",
115 "disable_autofocus": false,
116 "footer_to_buttons": false,
118 this.is_initialized = $.Deferred();
119 this.mutating_mutex = new $.Mutex();
120 this.on_change_list = [];
122 this.reload_mutex = new $.Mutex();
123 this.__clicked_inside = false;
124 this.__blur_timeout = null;
125 this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
126 self.set({actual_mode: self.options.initial_mode});
127 this.has_been_loaded.done(function() {
128 self.on("change:actual_mode", self, self.check_actual_mode);
129 self.check_actual_mode();
130 self.on("change:actual_mode", self, self.init_pager);
133 self.on("load_record", self, self.load_record);
134 instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
135 if (!this.can_be_discarded()) {
140 view_loading: function(r) {
141 return this.load_form(r);
143 destroy: function() {
144 _.each(this.get_widgets(), function(w) {
145 w.off('focused blurred');
149 this.$el.off('.formBlur');
153 load_form: function(data) {
156 throw new Error(_t("No data provided."));
159 throw "Form view does not support multiple calls to load_form";
161 this.fields_order = [];
162 this.fields_view = data;
164 this.rendering_engine.set_fields_registry(this.fields_registry);
165 this.rendering_engine.set_tags_registry(this.tags_registry);
166 this.rendering_engine.set_widgets_registry(this.widgets_registry);
167 this.rendering_engine.set_fields_view(data);
168 var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
169 this.rendering_engine.render_to($dest);
171 this.$el.on('mousedown.formBlur', function () {
172 self.__clicked_inside = true;
175 this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
176 if (this.options.$buttons) {
177 this.$buttons.appendTo(this.options.$buttons);
179 this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
181 this.$buttons.on('click', '.oe_form_button_create',
182 this.guard_active(this.on_button_create));
183 this.$buttons.on('click', '.oe_form_button_edit',
184 this.guard_active(this.on_button_edit));
185 this.$buttons.on('click', '.oe_form_button_save',
186 this.guard_active(this.on_button_save));
187 this.$buttons.on('click', '.oe_form_button_cancel',
188 this.guard_active(this.on_button_cancel));
189 if (this.options.footer_to_buttons) {
190 this.$el.find('footer').appendTo(this.$buttons);
193 this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
194 if (!this.sidebar && this.options.$sidebar) {
195 this.sidebar = new instance.web.Sidebar(this);
196 this.sidebar.appendTo(this.$sidebar);
197 if (this.fields_view.toolbar) {
198 this.sidebar.add_toolbar(this.fields_view.toolbar);
200 this.sidebar.add_items('other', _.compact([
201 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
202 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
206 this.has_been_loaded.resolve();
208 // Add bounce effect on button 'Edit' when click on readonly page view.
209 this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
210 if(self.get("actual_mode") == "view") {
211 var $button = self.options.$buttons.find(".oe_form_button_edit");
212 $button.openerpBounce();
214 instance.web.bus.trigger('click', e);
217 //bounce effect on red button when click on statusbar.
218 this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
219 if((self.get("actual_mode") == "view")) {
220 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
221 $button.openerpBounce();
225 this.trigger('form_view_loaded', data);
228 widgetFocused: function() {
229 // Clear click flag if used to focus a widget
230 this.__clicked_inside = false;
231 if (this.__blur_timeout) {
232 clearTimeout(this.__blur_timeout);
233 this.__blur_timeout = null;
236 widgetBlurred: function() {
237 if (this.__clicked_inside) {
238 // clicked in an other section of the form (than the currently
239 // focused widget) => just ignore the blurring entirely?
240 this.__clicked_inside = false;
244 // clear timeout, if any
245 this.widgetFocused();
246 this.__blur_timeout = setTimeout(function () {
247 self.trigger('blurred');
251 do_load_state: function(state, warm) {
252 if (state.id && this.datarecord.id != state.id) {
253 if (this.dataset.get_id_index(state.id) === null) {
254 this.dataset.ids.push(state.id);
256 this.dataset.select_id(state.id);
257 this.do_show({ reload: warm });
262 * @param {Object} [options]
263 * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
264 * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
265 * @return {$.Deferred}
267 do_show: function (options) {
269 options = options || {};
271 this.sidebar.$el.show();
274 this.$buttons.show();
276 this.$el.show().css({
278 filter: 'alpha(opacity = 0)'
280 this.$el.add(this.$buttons).removeClass('oe_form_dirty');
282 var shown = this.has_been_loaded;
283 if (options.reload !== false) {
284 shown = shown.then(function() {
285 if (self.dataset.index === null) {
286 // null index means we should start a new record
287 return self.on_button_new();
289 var fields = _.keys(self.fields_view.fields);
290 fields.push('display_name');
291 return self.dataset.read_index(fields, {
292 context: { 'bin_size': true, 'future_display_name' : true }
293 }).then(function(r) {
294 self.trigger('load_record', r);
298 return shown.then(function() {
299 self._actualize_mode(options.mode || self.options.initial_mode);
302 filter: 'alpha(opacity = 100)'
306 do_hide: function () {
308 this.sidebar.$el.hide();
311 this.$buttons.hide();
318 load_record: function(record) {
319 var self = this, set_values = [];
321 this.set({ 'title' : undefined });
322 this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
323 return $.Deferred().reject();
325 this.datarecord = record;
326 this._actualize_mode();
327 this.set({ 'title' : record.id ? record.display_name : _t("New") });
329 _(this.fields).each(function (field, f) {
330 field._dirty_flag = false;
331 field._inhibit_on_change_flag = true;
332 var result = field.set_value(self.datarecord[f] || false);
333 field._inhibit_on_change_flag = false;
334 set_values.push(result);
336 return $.when.apply(null, set_values).then(function() {
338 // New record: Second pass in order to trigger the onchanges
339 // respecting the fields order defined in the view
340 _.each(self.fields_order, function(field_name) {
341 if (record[field_name] !== undefined) {
342 var field = self.fields[field_name];
343 field._dirty_flag = true;
344 self.do_onchange(field);
348 self.on_form_changed();
349 self.rendering_engine.init_fields();
350 self.is_initialized.resolve();
351 self.do_update_pager(record.id === null || record.id === undefined);
353 self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
356 self.do_push_state({id:record.id});
358 self.do_push_state({});
360 self.$el.add(self.$buttons).removeClass('oe_form_dirty');
365 * Loads and sets up the default values for the model as the current
368 * @return {$.Deferred}
370 load_defaults: function () {
372 var keys = _.keys(this.fields_view.fields);
374 return this.dataset.default_get(keys).then(function(r) {
375 self.trigger('load_record', r);
378 return self.trigger('load_record', {});
380 on_form_changed: function() {
381 this.trigger("view_content_has_changed");
383 do_notify_change: function() {
384 this.$el.add(this.$buttons).addClass('oe_form_dirty');
386 execute_pager_action: function(action) {
387 if (this.can_be_discarded()) {
390 this.dataset.index = 0;
393 this.dataset.previous();
399 this.dataset.index = this.dataset.ids.length - 1;
402 var def = this.reload();
403 this.trigger('pager_action_executed');
408 init_pager: function() {
411 this.$pager.remove();
412 if (this.get("actual_mode") === "create")
414 this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
415 if (this.options.$pager) {
416 this.$pager.appendTo(this.options.$pager);
418 this.$el.find('.oe_form_pager').replaceWith(this.$pager);
420 this.$pager.on('click','a[data-pager-action]',function() {
422 if ($el.attr("disabled"))
424 var action = $el.data('pager-action');
425 var def = $.when(self.execute_pager_action(action));
426 $el.attr("disabled");
427 def.always(function() {
428 $el.removeAttr("disabled");
431 this.do_update_pager();
433 do_update_pager: function(hide_index) {
434 this.$pager.toggle(this.dataset.ids.length > 1);
436 $(".oe_form_pager_state", this.$pager).html("");
438 $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
441 parse_on_change: function (on_change, widget) {
443 var onchange = _.str.trim(on_change);
444 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
446 throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
449 var method = call[1];
450 if (!_.str.trim(call[2])) {
451 return {method: method, args: []};
454 var argument_replacement = {
455 'False': function () {return false;},
456 'True': function () {return true;},
457 'None': function () {return null;},
458 'context': function () {
459 return new instance.web.CompoundContext(
460 self.dataset.get_context(),
461 widget.build_context() ? widget.build_context() : {});
464 var parent_fields = null;
465 var args = _.map(call[2].split(','), function (a, i) {
466 var field = _.str.trim(a);
468 // literal constant or context
469 if (field in argument_replacement) {
470 return argument_replacement[field]();
473 if (/^-?\d+(\.\d+)?$/.test(field)) {
474 return Number(field);
477 if (self.fields[field]) {
478 var value_ = self.fields[field].get_value();
479 return value_ === null || value_ === undefined ? false : value_;
482 var splitted = field.split('.');
483 if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
484 if (parent_fields === null) {
485 parent_fields = self.dataset.parent_view.get_fields_values();
487 var p_val = parent_fields[_.str.trim(splitted[1])];
488 if (p_val !== undefined) {
489 return p_val === null || p_val === undefined ? false : p_val;
493 var first_char = field[0], last_char = field[field.length-1];
494 if ((first_char === '"' && last_char === '"')
495 || (first_char === "'" && last_char === "'")) {
496 return field.slice(1, -1);
499 throw new Error("Could not get field with name '" + field +
500 "' for onchange '" + onchange + "'");
508 do_onchange: function(widget, processed) {
510 this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
511 return this._process_operations();
513 _process_onchange: function(on_change_obj) {
515 var widget = on_change_obj.widget;
516 var processed = on_change_obj.processed;
519 processed = processed || [];
520 processed.push(widget.name);
521 var on_change = widget.node.attrs.on_change;
523 var change_spec = self.parse_on_change(on_change, widget);
525 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
526 // In case of a o2m virtual id, we should pass an empty ids list
527 ids.push(self.datarecord.id);
529 def = self.alive(new instance.web.Model(self.dataset.model).call(
530 change_spec.method, [ids].concat(change_spec.args)));
534 return def.then(function(response) {
535 if (widget.field['change_default']) {
536 var fieldname = widget.name;
538 if (response.value && (fieldname in response.value)) {
539 // Use value from onchange if onchange executed
540 value_ = response.value[fieldname];
542 // otherwise get form value for field
543 value_ = self.fields[fieldname].get_value();
545 var condition = fieldname + '=' + value_;
548 return self.alive(new instance.web.Model('ir.values').call(
549 'get_defaults', [self.model, condition]
550 )).then(function (results) {
551 if (!results.length) {
554 if (!response.value) {
557 for(var i=0; i<results.length; ++i) {
558 // [whatever, key, value]
559 var triplet = results[i];
560 response.value[triplet[1]] = triplet[2];
567 }).then(function(response) {
568 return self.on_processed_onchange(response, processed);
572 instance.webclient.crashmanager.show_message(e);
573 return $.Deferred().reject();
576 on_processed_onchange: function(result, processed) {
578 var fields = this.fields;
579 _(result.domain).each(function (domain, fieldname) {
580 var field = fields[fieldname];
581 if (!field) { return; }
582 field.node.attrs.domain = domain;
586 this._internal_set_values(result.value, processed);
588 if (!_.isEmpty(result.warning)) {
589 instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
590 title:result.warning.title,
593 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
598 return $.Deferred().resolve();
601 instance.webclient.crashmanager.show_message(e);
602 return $.Deferred().reject();
605 _process_operations: function() {
607 return this.mutating_mutex.exec(function() {
609 var on_change_obj = self.on_change_list.shift();
611 return self._process_onchange(on_change_obj).then(function() {
616 _.each(self.fields, function(field) {
617 defs.push(field.commit_value());
619 var args = _.toArray(arguments);
620 return $.when.apply($, defs).then(function() {
621 if (self.on_change_list.length !== 0) {
624 var save_obj = self.save_list.pop();
626 return self._process_save(save_obj).then(function() {
627 save_obj.ret = _.toArray(arguments);
630 save_obj.error = true;
635 self.save_list.pop();
642 _internal_set_values: function(values, exclude) {
643 exclude = exclude || [];
644 for (var f in values) {
645 if (!values.hasOwnProperty(f)) { continue; }
646 var field = this.fields[f];
647 // If field is not defined in the view, just ignore it
649 var value_ = values[f];
650 if (field.get_value() != value_) {
651 field._inhibit_on_change_flag = true;
652 field.set_value(value_);
653 field._inhibit_on_change_flag = false;
654 field._dirty_flag = true;
655 if (!_.contains(exclude, field.name)) {
656 this.do_onchange(field, exclude);
661 this.on_form_changed();
663 set_values: function(values) {
665 return this.mutating_mutex.exec(function() {
666 self._internal_set_values(values);
670 * Ask the view to switch to view mode if possible. The view may not do it
671 * if the current record is not yet saved. It will then stay in create mode.
673 to_view_mode: function() {
674 this._actualize_mode("view");
677 * Ask the view to switch to edit mode if possible. The view may not do it
678 * if the current record is not yet saved. It will then stay in create mode.
680 to_edit_mode: function() {
681 this._actualize_mode("edit");
684 * Ask the view to switch to a precise mode if possible. The view is free to
685 * not respect this command if the state of the dataset is not compatible with
686 * the new mode. For example, it is not possible to switch to edit mode if
687 * the current record is not yet saved in database.
689 * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
690 * undefined the view will test the actual mode to check if it is still consistent
691 * with the dataset state.
693 _actualize_mode: function(switch_to) {
694 var mode = switch_to || this.get("actual_mode");
695 if (! this.datarecord.id) {
697 } else if (mode === "create") {
700 this.set({actual_mode: mode});
702 check_actual_mode: function(source, options) {
704 if(this.get("actual_mode") === "view") {
705 self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
706 self.$buttons.find('.oe_form_buttons_edit').hide();
707 self.$buttons.find('.oe_form_buttons_view').show();
708 self.$sidebar.show();
710 self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
711 self.$buttons.find('.oe_form_buttons_edit').show();
712 self.$buttons.find('.oe_form_buttons_view').hide();
713 self.$sidebar.hide();
717 autofocus: function() {
718 if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
719 var fields_order = this.fields_order.slice(0);
720 if (this.default_focus_field) {
721 fields_order.unshift(this.default_focus_field.name);
723 for (var i = 0; i < fields_order.length; i += 1) {
724 var field = this.fields[fields_order[i]];
725 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
726 if (field.focus() !== false) {
733 on_button_save: function() {
735 return this.save().done(function(result) {
736 self.trigger("save", result);
737 self.reload().then(function() {
739 var parent = self.ViewManager.ActionManager.getParent();
741 parent.menu.do_reload_needaction();
746 on_button_cancel: function(event) {
747 if (this.can_be_discarded()) {
748 if (this.get('actual_mode') === 'create') {
749 this.trigger('history_back');
752 this.trigger('load_record', this.datarecord);
755 this.trigger('on_button_cancel');
758 on_button_new: function() {
761 return $.when(this.has_been_loaded).then(function() {
762 if (self.can_be_discarded()) {
763 return self.load_defaults();
767 on_button_edit: function() {
768 return this.to_edit_mode();
770 on_button_create: function() {
771 this.dataset.index = null;
774 on_button_duplicate: function() {
776 return this.has_been_loaded.then(function() {
777 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
778 self.record_created(new_id);
783 on_button_delete: function() {
785 var def = $.Deferred();
786 this.has_been_loaded.done(function() {
787 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
788 self.dataset.unlink([self.datarecord.id]).done(function() {
789 if (self.dataset.size()) {
790 self.execute_pager_action('next');
792 self.do_action('history_back');
797 $.async_when().done(function () {
802 return def.promise();
804 can_be_discarded: function() {
805 if (this.$el.is('.oe_form_dirty')) {
806 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
809 this.$el.removeClass('oe_form_dirty');
814 * Triggers saving the form's record. Chooses between creating a new
815 * record or saving an existing one depending on whether the record
816 * already has an id property.
818 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
819 * record, should that record be inserted at the start of the dataset (by
820 * default, records are added at the end)
822 save: function(prepend_on_create) {
824 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
825 this.save_list.push(save_obj);
826 return this._process_operations().then(function() {
828 return $.Deferred().reject();
829 return $.when.apply($, save_obj.ret);
831 self.$el.removeClass('oe_form_dirty');
834 _process_save: function(save_obj) {
836 var prepend_on_create = save_obj.prepend_on_create;
838 var form_invalid = false,
840 first_invalid_field = null,
841 readonly_values = {};
842 for (var f in self.fields) {
843 if (!self.fields.hasOwnProperty(f)) { continue; }
847 if (!first_invalid_field) {
848 first_invalid_field = f;
850 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
851 // Special case 'id' field, do not save this field
852 // on 'create' : save all non readonly fields
853 // on 'edit' : save non readonly modified fields
854 if (!f.get("readonly")) {
855 values[f.name] = f.get_value();
857 readonly_values[f.name] = f.get_value();
862 self.set({'display_invalid_fields': true});
863 first_invalid_field.focus();
865 return $.Deferred().reject();
867 self.set({'display_invalid_fields': false});
869 if (!self.datarecord.id) {
871 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
872 return self.record_created(r, prepend_on_create);
874 } else if (_.isEmpty(values)) {
875 // Not dirty, noop save
876 save_deferral = $.Deferred().resolve({}).promise();
879 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
880 return self.record_saved(r);
883 return save_deferral;
887 return $.Deferred().reject();
890 on_invalid: function() {
891 var warnings = _(this.fields).chain()
892 .filter(function (f) { return !f.is_valid(); })
894 return _.str.sprintf('<li>%s</li>',
897 warnings.unshift('<ul>');
898 warnings.push('</ul>');
899 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
902 * Reload the form after saving
904 * @param {Object} r result of the write function.
906 record_saved: function(r) {
907 this.trigger('record_saved', r);
909 // should not happen in the server, but may happen for internal purpose
910 return $.Deferred().reject();
915 * Updates the form' dataset to contain the new record:
917 * * Adds the newly created record to the current dataset (at the end by
919 * * Selects that record (sets the dataset's index to point to the new
921 * * Updates the pager and sidebar displays
924 * @param {Boolean} [prepend_on_create=false] adds the newly created record
925 * at the beginning of the dataset instead of the end
927 record_created: function(r, prepend_on_create) {
930 // should not happen in the server, but may happen for internal purpose
931 this.trigger('record_created', r);
932 return $.Deferred().reject();
934 this.datarecord.id = r;
935 if (!prepend_on_create) {
936 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
937 this.dataset.index = this.dataset.ids.length - 1;
939 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
940 this.dataset.index = 0;
942 this.do_update_pager();
944 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
946 //openerp.log("The record has been created with id #" + this.datarecord.id);
947 return $.when(this.reload()).then(function () {
948 self.trigger('record_created', r);
949 return _.extend(r, {created: true});
953 on_action: function (action) {
954 console.debug('Executing action', action);
958 return this.reload_mutex.exec(function() {
959 if (self.dataset.index === null || self.dataset.index === undefined) {
960 self.trigger("previous_view");
961 return $.Deferred().reject().promise();
963 if (self.dataset.index < 0) {
964 return $.when(self.on_button_new());
966 var fields = _.keys(self.fields_view.fields);
967 fields.push('display_name');
968 return self.dataset.read_index(fields,
972 'future_display_name': true
974 check_access_rule: true
975 }).then(function(r) {
976 self.trigger('load_record', r);
978 self.do_action('history_back');
983 get_widgets: function() {
984 return _.filter(this.getChildren(), function(obj) {
985 return obj instanceof instance.web.form.FormWidget;
988 get_fields_values: function() {
990 var ids = this.get_selected_ids();
991 values["id"] = ids.length > 0 ? ids[0] : false;
992 _.each(this.fields, function(value_, key) {
993 values[key] = value_.get_value();
997 get_selected_ids: function() {
998 var id = this.dataset.ids[this.dataset.index];
999 return id ? [id] : [];
1001 recursive_save: function() {
1003 return $.when(this.save()).then(function(res) {
1004 if (self.dataset.parent_view)
1005 return self.dataset.parent_view.recursive_save();
1008 recursive_reload: function() {
1011 if (self.dataset.parent_view)
1012 pre = self.dataset.parent_view.recursive_reload();
1013 return pre.then(function() {
1014 return self.reload();
1017 is_dirty: function() {
1018 return _.any(this.fields, function (value_) {
1019 return value_._dirty_flag;
1022 is_interactible_record: function() {
1023 var id = this.datarecord.id;
1025 if (this.options.not_interactible_on_create)
1027 } else if (typeof(id) === "string") {
1028 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1033 sidebar_eval_context: function () {
1034 return $.when(this.build_eval_context());
1036 open_defaults_dialog: function () {
1038 var display = function (field, value) {
1039 if (!value) { return value; }
1040 if (field instanceof instance.web.form.FieldSelection) {
1041 return _(field.get('values')).find(function (option) {
1042 return option[0] === value;
1044 } else if (field instanceof instance.web.form.FieldMany2One) {
1045 return field.get_displayed();
1049 var fields = _.chain(this.fields)
1050 .map(function (field) {
1051 var value = field.get_value();
1052 // ignore fields which are empty, invisible, readonly, o2m
1055 || field.get('invisible')
1056 || field.get("readonly")
1057 || field.field.type === 'one2many'
1058 || field.field.type === 'many2many'
1059 || field.field.type === 'binary'
1060 || field.password) {
1066 string: field.string,
1068 displayed: display(field, value),
1072 .sortBy(function (field) { return field.string; })
1074 var conditions = _.chain(self.fields)
1075 .filter(function (field) { return field.field.change_default; })
1076 .map(function (field) {
1077 var value = field.get_value();
1080 string: field.string,
1082 displayed: display(field, value),
1087 var d = new instance.web.Dialog(this, {
1088 title: _t("Set Default"),
1091 conditions: conditions
1094 {text: _t("Close"), click: function () { d.close(); }},
1095 {text: _t("Save default"), click: function () {
1096 var $defaults = d.$el.find('#formview_default_fields');
1097 var field_to_set = $defaults.val();
1098 if (!field_to_set) {
1099 $defaults.parent().addClass('oe_form_invalid');
1102 var condition = d.$el.find('#formview_default_conditions').val(),
1103 all_users = d.$el.find('#formview_default_all').is(':checked');
1104 new instance.web.DataSet(self, 'ir.values').call(
1108 self.fields[field_to_set].get_value(),
1112 ]).done(function () { d.close(); });
1116 d.template = 'FormView.set_default';
1119 register_field: function(field, name) {
1120 this.fields[name] = field;
1121 this.fields_order.push(name);
1122 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1123 this.default_focus_field = field;
1126 field.on('focused', null, this.proxy('widgetFocused'))
1127 .on('blurred', null, this.proxy('widgetBlurred'));
1128 if (this.get_field_desc(name).translate) {
1129 this.translatable_fields.push(field);
1131 field.on('changed_value', this, function() {
1132 if (field.is_syntax_valid()) {
1133 this.trigger('field_changed:' + name);
1135 if (field._inhibit_on_change_flag) {
1138 field._dirty_flag = true;
1139 if (field.is_syntax_valid()) {
1140 this.do_onchange(field);
1141 this.on_form_changed(true);
1142 this.do_notify_change();
1146 get_field_desc: function(field_name) {
1147 return this.fields_view.fields[field_name];
1149 get_field_value: function(field_name) {
1150 return this.fields[field_name].get_value();
1152 compute_domain: function(expression) {
1153 return instance.web.form.compute_domain(expression, this.fields);
1155 _build_view_fields_values: function() {
1156 var a_dataset = this.dataset;
1157 var fields_values = this.get_fields_values();
1158 var active_id = a_dataset.ids[a_dataset.index];
1159 _.extend(fields_values, {
1160 active_id: active_id || false,
1161 active_ids: active_id ? [active_id] : [],
1162 active_model: a_dataset.model,
1165 if (a_dataset.parent_view) {
1166 fields_values.parent = a_dataset.parent_view.get_fields_values();
1168 return fields_values;
1170 build_eval_context: function() {
1171 var a_dataset = this.dataset;
1172 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1177 * Interface to be implemented by rendering engines for the form view.
1179 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1180 set_fields_view: function(fields_view) {},
1181 set_fields_registry: function(fields_registry) {},
1182 render_to: function($el) {},
1186 * Default rendering engine for the form view.
1188 * It is necessary to set the view using set_view() before usage.
1190 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1191 init: function(view) {
1194 set_fields_view: function(fvg) {
1196 this.version = parseFloat(this.fvg.arch.attrs.version);
1197 if (isNaN(this.version)) {
1201 set_tags_registry: function(tags_registry) {
1202 this.tags_registry = tags_registry;
1204 set_fields_registry: function(fields_registry) {
1205 this.fields_registry = fields_registry;
1207 set_widgets_registry: function(widgets_registry) {
1208 this.widgets_registry = widgets_registry;
1210 // Backward compatibility tools, current default version: v6.1
1211 process_version: function() {
1212 if (this.version < 7.0) {
1213 this.$form.find('form:first').wrapInner('<group col="4"/>');
1214 this.$form.find('page').each(function() {
1215 if (!$(this).parents('field').length) {
1216 $(this).wrapInner('<group col="4"/>');
1221 get_arch_fragment: function() {
1222 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1223 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1224 $('button', doc).each(function() {
1225 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1227 // IE's html parser is also a css parser. How convenient...
1228 $('board', doc).each(function() {
1229 $(this).attr('layout', $(this).attr('style'));
1231 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1233 render_to: function($target) {
1235 this.$target = $target;
1237 this.$form = this.get_arch_fragment();
1239 this.process_version();
1241 this.fields_to_init = [];
1242 this.tags_to_init = [];
1243 this.widgets_to_init = [];
1245 this.process(this.$form);
1247 this.$form.appendTo(this.$target);
1249 this.to_replace = [];
1251 _.each(this.fields_to_init, function($elem) {
1252 var name = $elem.attr("name");
1253 if (!self.fvg.fields[name]) {
1254 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1256 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1258 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1260 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1261 var $label = self.labels[$elem.attr("name")];
1263 w.set_input_id($label.attr("for"));
1265 self.alter_field(w);
1266 self.view.register_field(w, $elem.attr("name"));
1267 self.to_replace.push([w, $elem]);
1269 _.each(this.tags_to_init, function($elem) {
1270 var tag_name = $elem[0].tagName.toLowerCase();
1271 var obj = self.tags_registry.get_object(tag_name);
1272 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1273 self.to_replace.push([w, $elem]);
1275 _.each(this.widgets_to_init, function($elem) {
1276 var widget_type = $elem.attr("type");
1277 var obj = self.widgets_registry.get_object(widget_type);
1278 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1279 self.to_replace.push([w, $elem]);
1282 init_fields: function() {
1284 _.each(this.to_replace, function(el) {
1285 defs.push(el[0].replace(el[1]));
1287 this.to_replace = [];
1288 return $.when.apply($, defs);
1290 render_element: function(template /* dictionaries */) {
1291 var dicts = [].slice.call(arguments).slice(1);
1292 var dict = _.extend.apply(_, dicts);
1293 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1294 return $(QWeb.render(template, dict));
1296 alter_field: function(field) {
1298 toggle_layout_debugging: function() {
1299 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1300 this.$target.find('[title]').removeAttr('title');
1301 this.$target.find('.oe_form_group_cell').each(function() {
1302 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1303 $(this).attr('title', text);
1306 this.$target.toggleClass('oe_layout_debugging');
1308 process: function($tag) {
1310 var tagname = $tag[0].nodeName.toLowerCase();
1311 if (this.tags_registry.contains(tagname)) {
1312 this.tags_to_init.push($tag);
1315 var fn = self['process_' + tagname];
1317 var args = [].slice.call(arguments);
1319 return fn.apply(self, args);
1321 // generic tag handling, just process children
1322 $tag.children().each(function() {
1323 self.process($(this));
1325 self.handle_common_properties($tag, $tag);
1326 $tag.removeAttr("modifiers");
1330 process_widget: function($widget) {
1331 this.widgets_to_init.push($widget);
1334 process_sheet: function($sheet) {
1335 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1336 this.handle_common_properties($new_sheet, $sheet);
1337 var $dst = $new_sheet.find('.oe_form_sheet');
1338 $sheet.contents().appendTo($dst);
1339 $sheet.before($new_sheet).remove();
1340 this.process($new_sheet);
1342 process_form: function($form) {
1343 if ($form.find('> sheet').length === 0) {
1344 $form.addClass('oe_form_nosheet');
1346 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1347 this.handle_common_properties($new_form, $form);
1348 $form.contents().appendTo($new_form);
1349 if ($form[0] === this.$form[0]) {
1350 // If root element, replace it
1351 this.$form = $new_form;
1353 $form.before($new_form).remove();
1355 this.process($new_form);
1358 * Used by direct <field> children of a <group> tag only
1359 * This method will add the implicit <label...> for every field
1362 preprocess_field: function($field) {
1364 var name = $field.attr('name'),
1365 field_colspan = parseInt($field.attr('colspan'), 10),
1366 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1368 if ($field.attr('nolabel') === '1')
1370 $field.attr('nolabel', '1');
1372 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1373 $(el).parents().each(function(unused, tag) {
1374 var name = tag.tagName.toLowerCase();
1375 if (name === "field" || name in self.tags_registry.map)
1382 var $label = $('<label/>').attr({
1384 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1385 "string": $field.attr('string'),
1386 "help": $field.attr('help'),
1387 "class": $field.attr('class'),
1389 $label.insertBefore($field);
1390 if (field_colspan > 1) {
1391 $field.attr('colspan', field_colspan - 1);
1395 process_field: function($field) {
1396 if ($field.parent().is('group')) {
1397 // No implicit labels for normal fields, only for <group> direct children
1398 var $label = this.preprocess_field($field);
1400 this.process($label);
1403 this.fields_to_init.push($field);
1406 process_group: function($group) {
1408 $group.children('field').each(function() {
1409 self.preprocess_field($(this));
1411 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1413 if ($new_group.first().is('table.oe_form_group')) {
1414 $table = $new_group;
1415 } else if ($new_group.filter('table.oe_form_group').length) {
1416 $table = $new_group.filter('table.oe_form_group').first();
1418 $table = $new_group.find('table.oe_form_group').first();
1422 cols = parseInt($group.attr('col') || 2, 10),
1426 $group.children().each(function(a,b,c) {
1427 var $child = $(this);
1428 var colspan = parseInt($child.attr('colspan') || 1, 10);
1429 var tagName = $child[0].tagName.toLowerCase();
1430 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1431 var newline = tagName === 'newline';
1433 // Note FME: those classes are used in layout debug mode
1434 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1435 $tr.addClass('oe_form_group_row_incomplete');
1437 $tr.addClass('oe_form_group_row_newline');
1444 if (!$tr || row_cols < colspan) {
1445 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1447 } else if (tagName==='group') {
1448 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1449 $td.addClass('oe_group_right');
1451 row_cols -= colspan;
1453 // invisibility transfer
1454 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1455 var invisible = field_modifiers.invisible;
1456 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1458 $tr.append($td.append($child));
1459 children.push($child[0]);
1461 if (row_cols && $td) {
1462 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1464 $group.before($new_group).remove();
1466 $table.find('> tbody > tr').each(function() {
1467 var to_compute = [],
1470 $(this).children().each(function() {
1472 $child = $td.children(':first');
1473 if ($child.attr('cell-class')) {
1474 $td.addClass($child.attr('cell-class'));
1476 switch ($child[0].tagName.toLowerCase()) {
1480 if ($child.attr('for')) {
1481 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1482 row_cols-= $td.attr('colspan') || 1;
1487 var width = _.str.trim($child.attr('width') || ''),
1488 iwidth = parseInt(width, 10);
1490 if (width.substr(-1) === '%') {
1492 width = iwidth + '%';
1495 $td.css('min-width', width + 'px');
1497 $td.attr('width', width);
1498 $child.removeAttr('width');
1499 row_cols-= $td.attr('colspan') || 1;
1501 to_compute.push($td);
1507 var unit = Math.floor(total / row_cols);
1508 if (!$(this).is('.oe_form_group_row_incomplete')) {
1509 _.each(to_compute, function($td, i) {
1510 var width = parseInt($td.attr('colspan'), 10) * unit;
1511 $td.attr('width', width + '%');
1517 _.each(children, function(el) {
1518 self.process($(el));
1520 this.handle_common_properties($new_group, $group);
1523 process_notebook: function($notebook) {
1526 $notebook.find('> page').each(function() {
1527 var $page = $(this);
1528 var page_attrs = $page.getAttributes();
1529 page_attrs.id = _.uniqueId('notebook_page_');
1530 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1531 $page.contents().appendTo($new_page);
1532 $page.before($new_page).remove();
1533 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1534 page_attrs.__page = $new_page;
1535 page_attrs.__ic = ic;
1536 pages.push(page_attrs);
1538 $new_page.children().each(function() {
1539 self.process($(this));
1542 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1543 $notebook.contents().appendTo($new_notebook);
1544 $notebook.before($new_notebook).remove();
1545 self.process($($new_notebook.children()[0]));
1546 //tabs and invisibility handling
1547 $new_notebook.tabs();
1548 _.each(pages, function(page, i) {
1551 page.__ic.on("change:effective_invisible", null, function() {
1552 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1553 $new_notebook.tabs('select', i);
1556 var current = $new_notebook.tabs("option", "selected");
1557 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1559 var first_visible = _.find(_.range(pages.length), function(i2) {
1560 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1562 if (first_visible !== undefined) {
1563 $new_notebook.tabs('select', first_visible);
1568 this.handle_common_properties($new_notebook, $notebook);
1569 return $new_notebook;
1571 process_separator: function($separator) {
1572 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1573 $separator.before($new_separator).remove();
1574 this.handle_common_properties($new_separator, $separator);
1575 return $new_separator;
1577 process_label: function($label) {
1578 var name = $label.attr("for"),
1579 field_orm = this.fvg.fields[name];
1581 string: $label.attr('string') || (field_orm || {}).string || '',
1582 help: $label.attr('help') || (field_orm || {}).help || '',
1583 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1585 var align = parseFloat(dict.align);
1586 if (isNaN(align) || align === 1) {
1588 } else if (align === 0) {
1594 var $new_label = this.render_element('FormRenderingLabel', dict);
1595 $label.before($new_label).remove();
1596 this.handle_common_properties($new_label, $label);
1598 this.labels[name] = $new_label;
1602 handle_common_properties: function($new_element, $node) {
1603 var str_modifiers = $node.attr("modifiers") || "{}";
1604 var modifiers = JSON.parse(str_modifiers);
1606 if (modifiers.invisible !== undefined)
1607 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1608 $new_element.addClass($node.attr("class") || "");
1609 $new_element.attr('style', $node.attr('style'));
1610 return {invisibility_changer: ic,};
1617 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1618 a form view. Before going further, you must understand that those fields were never really created for
1619 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1620 you to hack the system with more style.
1622 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1623 init: function(parent, eval_context) {
1624 this._super(parent);
1625 this.field_descs = {};
1626 this.eval_context = eval_context || {};
1628 display_invalid_fields: false,
1629 actual_mode: 'create',
1632 get_field_desc: function(field_name) {
1633 if (this.field_descs[field_name] === undefined) {
1634 this.field_descs[field_name] = {
1638 return this.field_descs[field_name];
1640 extend_field_desc: function(fields) {
1642 _.each(fields, function(v, k) {
1643 _.extend(self.get_field_desc(k), v);
1646 get_field_value: function(field_name) {
1649 set_values: function(values) {
1652 compute_domain: function(expression) {
1653 return instance.web.form.compute_domain(expression, {});
1655 build_eval_context: function() {
1656 return new instance.web.CompoundContext(this.eval_context);
1660 instance.web.form.compute_domain = function(expr, fields) {
1661 if (! (expr instanceof Array))
1664 for (var i = expr.length - 1; i >= 0; i--) {
1666 if (ex.length == 1) {
1667 var top = stack.pop();
1670 stack.push(stack.pop() || top);
1673 stack.push(stack.pop() && top);
1679 throw new Error(_.str.sprintf(
1680 _t("Unknown operator %s in domain %s"),
1681 ex, JSON.stringify(expr)));
1685 var field = fields[ex[0]];
1687 throw new Error(_.str.sprintf(
1688 _t("Unknown field %s in domain %s"),
1689 ex[0], JSON.stringify(expr)));
1691 var field_value = field.get_value ? field.get_value() : field.value;
1695 switch (op.toLowerCase()) {
1698 stack.push(_.isEqual(field_value, val));
1702 stack.push(!_.isEqual(field_value, val));
1705 stack.push(field_value < val);
1708 stack.push(field_value > val);
1711 stack.push(field_value <= val);
1714 stack.push(field_value >= val);
1717 if (!_.isArray(val)) val = [val];
1718 stack.push(_(val).contains(field_value));
1721 if (!_.isArray(val)) val = [val];
1722 stack.push(!_(val).contains(field_value));
1726 _t("Unsupported operator %s in domain %s"),
1727 op, JSON.stringify(expr));
1730 return _.all(stack, _.identity);
1733 instance.web.form.is_bin_size = function(v) {
1734 return (/^\d+(\.\d*)? \w+$/).test(v);
1738 * Must be applied over an class already possessing the PropertiesMixin.
1740 * Apply the result of the "invisible" domain to this.$el.
1742 instance.web.form.InvisibilityChangerMixin = {
1743 init: function(field_manager, invisible_domain) {
1745 this._ic_field_manager = field_manager;
1746 this._ic_invisible_modifier = invisible_domain;
1747 this._ic_field_manager.on("view_content_has_changed", this, function() {
1748 var result = self._ic_invisible_modifier === undefined ? false :
1749 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1750 self.set({"invisible": result});
1752 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1753 var check = function() {
1754 if (self.get("invisible") || self.get('force_invisible')) {
1755 self.set({"effective_invisible": true});
1757 self.set({"effective_invisible": false});
1760 this.on('change:invisible', this, check);
1761 this.on('change:force_invisible', this, check);
1765 this.on("change:effective_invisible", this, this._check_visibility);
1766 this._check_visibility();
1768 _check_visibility: function() {
1769 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1773 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1774 init: function(parent, field_manager, invisible_domain, $el) {
1775 this.setParent(parent);
1776 instance.web.PropertiesMixin.init.call(this);
1777 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1784 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1787 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1788 the values of the "readonly" property and the "mode" property on the field manager.
1790 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1792 * @constructs instance.web.form.FormWidget
1793 * @extends instance.web.Widget
1795 * @param field_manager
1798 init: function(field_manager, node) {
1799 this._super(field_manager);
1800 this.field_manager = field_manager;
1801 if (this.field_manager instanceof instance.web.FormView)
1802 this.view = this.field_manager;
1804 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1805 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1807 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1813 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1814 // "mode" on field_manager
1816 var test_effective_readonly = function() {
1817 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1819 this.on("change:readonly", this, test_effective_readonly);
1820 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1821 test_effective_readonly.call(this);
1823 renderElement: function() {
1824 this.process_modifiers();
1826 this.$el.addClass(this.node.attrs["class"] || "");
1828 destroy: function() {
1830 this._super.apply(this, arguments);
1833 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1835 * This method is an utility method that is meant to be called by child classes.
1837 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1839 setupFocus: function ($e) {
1842 focus: function () { self.trigger('focused'); },
1843 blur: function () { self.trigger('blurred'); }
1846 process_modifiers: function() {
1848 for (var a in this.modifiers) {
1849 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1850 if (!_.include(["invisible"], a)) {
1851 var val = this.field_manager.compute_domain(this.modifiers[a]);
1857 do_attach_tooltip: function(widget, trigger, options) {
1858 widget = widget || this;
1859 trigger = trigger || this.$el;
1860 options = _.extend({
1865 var template = widget.template + '.tooltip';
1866 if (!QWeb.has_template(template)) {
1867 template = 'WidgetLabel.tooltip';
1869 return QWeb.render(template, {
1870 debug: instance.session.debug,
1874 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1879 $(trigger).tipsy(options);
1882 * Builds a new context usable for operations related to fields by merging
1883 * the fields'context with the action's context.
1885 build_context: function() {
1886 // only use the model's context if there is not context on the node
1887 var v_context = this.node.attrs.context;
1889 v_context = (this.field || {}).context || {};
1892 if (v_context.__ref || true) { //TODO: remove true
1893 var fields_values = this.field_manager.build_eval_context();
1894 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1898 build_domain: function() {
1899 var f_domain = this.field.domain || [];
1900 var n_domain = this.node.attrs.domain || null;
1901 // if there is a domain on the node, overrides the model's domain
1902 var final_domain = n_domain !== null ? n_domain : f_domain;
1903 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1904 var fields_values = this.field_manager.build_eval_context();
1905 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1907 return final_domain;
1911 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1912 template: 'WidgetButton',
1913 init: function(field_manager, node) {
1914 node.attrs.type = node.attrs['data-button-type'];
1915 this._super(field_manager, node);
1916 this.force_disabled = false;
1917 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1918 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1919 // TODO fme: provide enter key binding to widgets
1920 this.view.default_focus_button = this;
1922 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1923 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1927 this._super.apply(this, arguments);
1928 this.view.on('view_content_has_changed', this, this.check_disable);
1929 this.check_disable();
1930 this.$el.click(this.on_click);
1931 if (this.node.attrs.help || instance.session.debug) {
1932 this.do_attach_tooltip();
1934 this.setupFocus(this.$el);
1936 on_click: function() {
1938 this.force_disabled = true;
1939 this.check_disable();
1940 this.execute_action().always(function() {
1941 self.force_disabled = false;
1942 self.check_disable();
1945 execute_action: function() {
1947 var exec_action = function() {
1948 if (self.node.attrs.confirm) {
1949 var def = $.Deferred();
1950 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1951 title: _t('Confirm'),
1954 {text: _t("Cancel"), click: function() {
1955 $(this).dialog("close");
1958 {text: _t("Ok"), click: function() {
1960 self.on_confirmed().always(function() {
1961 $(self2).dialog("close");
1966 beforeClose: function() {
1970 return def.promise();
1972 return self.on_confirmed();
1975 if (!this.node.attrs.special) {
1976 return this.view.recursive_save().then(exec_action);
1978 return exec_action();
1981 on_confirmed: function() {
1984 var context = this.build_context();
1986 return this.view.do_execute_action(
1987 _.extend({}, this.node.attrs, {context: context}),
1988 this.view.dataset, this.view.datarecord.id, function (reason) {
1989 if (!_.isObject(reason)) {
1990 self.view.recursive_reload();
1994 check_disable: function() {
1995 var disabled = (this.force_disabled || !this.view.is_interactible_record());
1996 this.$el.prop('disabled', disabled);
1997 this.$el.css('color', disabled ? 'grey' : '');
2002 * Interface to be implemented by fields.
2005 * - changed_value: triggered when the value of the field has changed. This can be due
2006 * to a user interaction or a call to set_value().
2009 instance.web.form.FieldInterface = {
2011 * Constructor takes 2 arguments:
2012 * - field_manager: Implements FieldManagerMixin
2013 * - node: the "<field>" node in json form
2015 init: function(field_manager, node) {},
2017 * Called by the form view to indicate the value of the field.
2019 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2020 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2021 * before the widget is inserted into the DOM.
2023 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2024 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2025 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2026 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2027 * no information is ever given to know which format is used.
2029 set_value: function(value_) {},
2031 * Get the current value of the widget.
2033 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2034 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2035 * For example if the field is marked as "required", a call to get_value() can return false.
2037 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2038 * return a default value according to the type of field.
2040 * This method is always assumed to perform synchronously, it can not return a promise.
2042 * If there was no user interaction to modify the value of the field, it is always assumed that
2043 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2044 * although the syntax can be different. This can be the case for type of fields that have a different
2045 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2047 get_value: function() {},
2049 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2052 set_input_id: function(id) {},
2054 * Returns true if is_syntax_valid() returns true and the value is semantically
2055 * valid too according to the semantic restrictions applied to the field.
2057 is_valid: function() {},
2059 * Returns true if the field holds a value which is syntactically correct, ignoring
2060 * the potential semantic restrictions applied to the field.
2062 is_syntax_valid: function() {},
2064 * Must set the focus on the field. Return false if field is not focusable.
2066 focus: function() {},
2068 * Called when the translate button is clicked.
2070 on_translate: function() {},
2072 This method is called by the form view before reading on_change values and before saving. It tells
2073 the field to save its value before reading it using get_value(). Must return a promise.
2075 commit_value: function() {},
2079 * Abstract class for classes implementing FieldInterface.
2082 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2083 * set and retrieve the value property. Changing the value property also triggers automatically
2084 * a 'changed_value' event that inform the view to trigger on_changes.
2087 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2089 * @constructs instance.web.form.AbstractField
2090 * @extends instance.web.form.FormWidget
2092 * @param field_manager
2095 init: function(field_manager, node) {
2097 this._super(field_manager, node);
2098 this.name = this.node.attrs.name;
2099 this.field = this.field_manager.get_field_desc(this.name);
2100 this.widget = this.node.attrs.widget;
2101 this.string = this.node.attrs.string || this.field.string || this.name;
2102 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2103 this.set({'value': false});
2105 this.on("change:value", this, function() {
2106 this.trigger('changed_value');
2107 this._check_css_flags();
2110 renderElement: function() {
2113 if (this.field.translate && this.view) {
2114 this.$el.addClass('oe_form_field_translatable');
2115 this.$el.find('.oe_field_translate').click(this.on_translate);
2117 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2118 if (instance.session.debug) {
2119 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2120 this.$label.off('dblclick').on('dblclick', function() {
2121 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2123 console.log("window.w =", window.w);
2126 if (!this.disable_utility_classes) {
2127 this.off("change:required", this, this._set_required);
2128 this.on("change:required", this, this._set_required);
2129 this._set_required();
2131 this._check_visibility();
2132 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2133 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2134 this._check_css_flags();
2137 var tmp = this._super();
2138 this.on("change:value", this, function() {
2139 if (! this.no_rerender)
2140 this.render_value();
2142 this.render_value();
2145 * Private. Do not use.
2147 _set_required: function() {
2148 this.$el.toggleClass('oe_form_required', this.get("required"));
2150 set_value: function(value_) {
2151 this.set({'value': value_});
2153 get_value: function() {
2154 return this.get('value');
2157 Utility method that all implementations should use to change the
2158 value without triggering a re-rendering.
2160 internal_set_value: function(value_) {
2161 var tmp = this.no_rerender;
2162 this.no_rerender = true;
2163 this.set({'value': value_});
2164 this.no_rerender = tmp;
2167 This method is called each time the value is modified.
2169 render_value: function() {},
2170 is_valid: function() {
2171 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2173 is_syntax_valid: function() {
2177 * Method useful to implement to ease validity testing. Must return true if the current
2178 * value is similar to false in OpenERP.
2180 is_false: function() {
2181 return this.get('value') === false;
2183 _check_css_flags: function() {
2184 if (this.field.translate) {
2185 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2187 if (!this.disable_utility_classes) {
2188 if (this.field_manager.get('display_invalid_fields')) {
2189 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2196 set_input_id: function(id) {
2197 this.id_for_label = id;
2199 on_translate: function() {
2201 var trans = new instance.web.DataSet(this, 'ir.translation');
2202 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2207 set_dimensions: function (height, width) {
2213 commit_value: function() {
2219 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2222 instance.web.form.ReinitializeWidgetMixin = {
2224 * Default implementation of, you should not override it, use initialize_field() instead.
2227 this.initialize_field();
2230 initialize_field: function() {
2231 this.on("change:effective_readonly", this, this.reinitialize);
2232 this.initialize_content();
2234 reinitialize: function() {
2235 this.destroy_content();
2236 this.renderElement();
2237 this.initialize_content();
2240 * Called to destroy anything that could have been created previously, called before a
2241 * re-initialization.
2243 destroy_content: function() {},
2245 * Called to initialize the content.
2247 initialize_content: function() {},
2251 * A mixin to apply on any field that has to completely re-render when its readonly state
2254 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2255 reinitialize: function() {
2256 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2257 this.render_value();
2262 Some hack to make placeholders work in ie9.
2264 if (!('placeholder' in document.createElement('input'))) {
2265 document.addEventListener("DOMNodeInserted",function(event){
2266 var nodename = event.target.nodeName.toLowerCase();
2267 if ( nodename === "input" || nodename == "textarea" ) {
2268 $(event.target).placeholder();
2273 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2274 template: 'FieldChar',
2275 widget_class: 'oe_form_field_char',
2277 'change input': 'store_dom_value',
2279 init: function (field_manager, node) {
2280 this._super(field_manager, node);
2281 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2283 initialize_content: function() {
2284 this.setupFocus(this.$('input'));
2286 store_dom_value: function () {
2287 if (!this.get('effective_readonly')
2288 && this.$('input').length
2289 && this.is_syntax_valid()) {
2290 this.internal_set_value(
2292 this.$('input').val()));
2295 commit_value: function () {
2296 this.store_dom_value();
2297 return this._super();
2299 render_value: function() {
2300 var show_value = this.format_value(this.get('value'), '');
2301 if (!this.get("effective_readonly")) {
2302 this.$el.find('input').val(show_value);
2304 if (this.password) {
2305 show_value = new Array(show_value.length + 1).join('*');
2307 this.$(".oe_form_char_content").text(show_value);
2310 is_syntax_valid: function() {
2311 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2313 this.parse_value(this.$('input').val(), '');
2321 parse_value: function(val, def) {
2322 return instance.web.parse_value(val, this, def);
2324 format_value: function(val, def) {
2325 return instance.web.format_value(val, this, def);
2327 is_false: function() {
2328 return this.get('value') === '' || this._super();
2331 var input = this.$('input:first')[0];
2332 return input ? input.focus() : false;
2334 set_dimensions: function (height, width) {
2335 this._super(height, width);
2336 this.$('input').css({
2343 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2344 process_modifiers: function () {
2346 this.set({ readonly: true });
2350 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2351 template: 'FieldEmail',
2352 initialize_content: function() {
2354 var $button = this.$el.find('button');
2355 $button.click(this.on_button_clicked);
2356 this.setupFocus($button);
2358 render_value: function() {
2359 if (!this.get("effective_readonly")) {
2363 .attr('href', 'mailto:' + this.get('value'))
2364 .text(this.get('value') || '');
2367 on_button_clicked: function() {
2368 if (!this.get('value') || !this.is_syntax_valid()) {
2369 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2371 location.href = 'mailto:' + this.get('value');
2376 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2377 template: 'FieldUrl',
2378 initialize_content: function() {
2380 var $button = this.$el.find('button');
2381 $button.click(this.on_button_clicked);
2382 this.setupFocus($button);
2384 render_value: function() {
2385 if (!this.get("effective_readonly")) {
2388 var tmp = this.get('value');
2389 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2391 tmp = "http://" + this.get('value');
2393 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2394 this.$el.find('a').attr('href', tmp).text(text);
2397 on_button_clicked: function() {
2398 if (!this.get('value')) {
2399 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2401 var url = $.trim(this.get('value'));
2402 if(/^www\./i.test(url))
2403 url = 'http://'+url;
2409 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2410 is_field_number: true,
2411 widget_class: 'oe_form_field_float',
2412 init: function (field_manager, node) {
2413 this._super(field_manager, node);
2414 this.internal_set_value(0);
2415 if (this.node.attrs.digits) {
2416 this.digits = this.node.attrs.digits;
2418 this.digits = this.field.digits;
2421 set_value: function(value_) {
2422 if (value_ === false || value_ === undefined) {
2423 // As in GTK client, floats default to 0
2426 this._super.apply(this, [value_]);
2428 focus: function () {
2429 var $input = this.$('input:first');
2430 return $input.length ? $input.select() : false;
2434 instance.web.DateTimeWidget = instance.web.Widget.extend({
2435 template: "web.datepicker",
2436 jqueryui_object: 'datetimepicker',
2437 type_of_date: "datetime",
2439 'change .oe_datepicker_master': 'change_datetime',
2441 init: function(parent) {
2442 this._super(parent);
2443 this.name = parent.name;
2447 this.$input = this.$el.find('input.oe_datepicker_master');
2448 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2450 $.datepicker.setDefaults({
2451 clearText: _t('Clear'),
2452 clearStatus: _t('Erase the current date'),
2453 closeText: _t('Done'),
2454 closeStatus: _t('Close without change'),
2455 prevText: _t('<Prev'),
2456 prevStatus: _t('Show the previous month'),
2457 nextText: _t('Next>'),
2458 nextStatus: _t('Show the next month'),
2459 currentText: _t('Today'),
2460 currentStatus: _t('Show the current month'),
2461 monthNames: Date.CultureInfo.monthNames,
2462 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2463 monthStatus: _t('Show a different month'),
2464 yearStatus: _t('Show a different year'),
2465 weekHeader: _t('Wk'),
2466 weekStatus: _t('Week of the year'),
2467 dayNames: Date.CultureInfo.dayNames,
2468 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2469 dayNamesMin: Date.CultureInfo.shortestDayNames,
2470 dayStatus: _t('Set DD as first week day'),
2471 dateStatus: _t('Select D, M d'),
2472 firstDay: Date.CultureInfo.firstDayOfWeek,
2473 initStatus: _t('Select a date'),
2476 $.timepicker.setDefaults({
2477 timeOnlyTitle: _t('Choose Time'),
2478 timeText: _t('Time'),
2479 hourText: _t('Hour'),
2480 minuteText: _t('Minute'),
2481 secondText: _t('Second'),
2482 currentText: _t('Now'),
2483 closeText: _t('Done')
2487 onClose: this.on_picker_select,
2488 onSelect: this.on_picker_select,
2492 showButtonPanel: true,
2493 firstDay: Date.CultureInfo.firstDayOfWeek
2495 // Some clicks in the datepicker dialog are not stopped by the
2496 // datepicker and "bubble through", unexpectedly triggering the bus's
2497 // click event. Prevent that.
2498 this.picker('widget').click(function (e) { e.stopPropagation(); });
2500 this.$el.find('img.oe_datepicker_trigger').click(function() {
2501 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2502 self.$input.focus();
2505 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2506 self.$input_picker.show();
2507 self.picker('show');
2508 self.$input_picker.hide();
2510 this.set_readonly(false);
2511 this.set({'value': false});
2513 picker: function() {
2514 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2516 on_picker_select: function(text, instance_) {
2517 var date = this.picker('getDate');
2519 .val(date ? this.format_client(date) : '')
2523 set_value: function(value_) {
2524 this.set({'value': value_});
2525 this.$input.val(value_ ? this.format_client(value_) : '');
2527 get_value: function() {
2528 return this.get('value');
2530 set_value_from_ui_: function() {
2531 var value_ = this.$input.val() || false;
2532 this.set({'value': this.parse_client(value_)});
2534 set_readonly: function(readonly) {
2535 this.readonly = readonly;
2536 this.$input.prop('readonly', this.readonly);
2537 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2539 is_valid_: function() {
2540 var value_ = this.$input.val();
2541 if (value_ === "") {
2545 this.parse_client(value_);
2552 parse_client: function(v) {
2553 return instance.web.parse_value(v, {"widget": this.type_of_date});
2555 format_client: function(v) {
2556 return instance.web.format_value(v, {"widget": this.type_of_date});
2558 change_datetime: function() {
2559 if (this.is_valid_()) {
2560 this.set_value_from_ui_();
2561 this.trigger("datetime_changed");
2564 commit_value: function () {
2565 this.change_datetime();
2569 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2570 jqueryui_object: 'datepicker',
2571 type_of_date: "date"
2574 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2575 template: "FieldDatetime",
2576 build_widget: function() {
2577 return new instance.web.DateTimeWidget(this);
2579 destroy_content: function() {
2580 if (this.datewidget) {
2581 this.datewidget.destroy();
2582 this.datewidget = undefined;
2585 initialize_content: function() {
2586 if (!this.get("effective_readonly")) {
2587 this.datewidget = this.build_widget();
2588 this.datewidget.on('datetime_changed', this, _.bind(function() {
2589 this.internal_set_value(this.datewidget.get_value());
2591 this.datewidget.appendTo(this.$el);
2592 this.setupFocus(this.datewidget.$input);
2595 render_value: function() {
2596 if (!this.get("effective_readonly")) {
2597 this.datewidget.set_value(this.get('value'));
2599 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2602 is_syntax_valid: function() {
2603 if (!this.get("effective_readonly") && this.datewidget) {
2604 return this.datewidget.is_valid_();
2608 is_false: function() {
2609 return this.get('value') === '' || this._super();
2612 var input = this.datewidget && this.datewidget.$input[0];
2613 return input ? input.focus() : false;
2615 set_dimensions: function (height, width) {
2616 this._super(height, width);
2617 this.datewidget.$input.css('height', height);
2621 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2622 template: "FieldDate",
2623 build_widget: function() {
2624 return new instance.web.DateWidget(this);
2628 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2629 template: 'FieldText',
2631 'keyup': function (e) {
2632 if (e.which === $.ui.keyCode.ENTER) {
2633 e.stopPropagation();
2636 'keypress': function (e) {
2637 if (e.which === $.ui.keyCode.ENTER) {
2638 e.stopPropagation();
2641 'change textarea': 'store_dom_value',
2643 initialize_content: function() {
2645 if (! this.get("effective_readonly")) {
2646 this.$textarea = this.$el.find('textarea');
2647 this.auto_sized = false;
2648 this.default_height = this.$textarea.css('height');
2649 if (this.get("effective_readonly")) {
2650 this.$textarea.attr('disabled', 'disabled');
2652 this.setupFocus(this.$textarea);
2654 this.$textarea = undefined;
2657 commit_value: function () {
2658 if (! this.get("effective_readonly") && this.$textarea) {
2659 this.store_dom_value();
2661 return this._super();
2663 store_dom_value: function () {
2664 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2666 render_value: function() {
2667 if (! this.get("effective_readonly")) {
2668 var show_value = instance.web.format_value(this.get('value'), this, '');
2669 if (show_value === '') {
2670 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2672 this.$textarea.val(show_value);
2673 if (! this.auto_sized) {
2674 this.auto_sized = true;
2675 this.$textarea.autosize();
2677 this.$textarea.trigger("autosize");
2680 var txt = this.get("value") || '';
2681 this.$(".oe_form_text_content").text(txt);
2684 is_syntax_valid: function() {
2685 if (!this.get("effective_readonly") && this.$textarea) {
2687 instance.web.parse_value(this.$textarea.val(), this, '');
2695 is_false: function() {
2696 return this.get('value') === '' || this._super();
2698 focus: function($el) {
2699 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2700 return input ? input.focus() : false;
2702 set_dimensions: function (height, width) {
2703 this._super(height, width);
2704 if (!this.get("effective_readonly") && this.$textarea) {
2705 this.$textarea.css({
2714 * FieldTextHtml Widget
2715 * Intended for FieldText widgets meant to display HTML content. This
2716 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2717 * To find more information about CLEditor configutation: go to
2718 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2720 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2721 template: 'FieldTextHtml',
2723 this._super.apply(this, arguments);
2725 initialize_content: function() {
2727 if (! this.get("effective_readonly")) {
2728 self._updating_editor = false;
2729 this.$textarea = this.$el.find('textarea');
2730 var width = ((this.node.attrs || {}).editor_width || '100%');
2731 var height = ((this.node.attrs || {}).editor_height || 250);
2732 this.$textarea.cleditor({
2733 width: width, // width not including margins, borders or padding
2734 height: height, // height not including margins, borders or padding
2735 controls: // controls to add to the toolbar
2736 "bold italic underline strikethrough " +
2737 "| removeformat | bullets numbering | outdent " +
2738 "indent | link unlink | source",
2739 bodyStyle: // style to assign to document body contained within the editor
2740 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2742 this.$cleditor = this.$textarea.cleditor()[0];
2743 this.$cleditor.change(function() {
2744 if (! self._updating_editor) {
2745 self.$cleditor.updateTextArea();
2746 self.internal_set_value(self.$textarea.val());
2749 if (this.field.translate) {
2750 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"/>')
2751 .click(this.on_translate);
2752 this.$cleditor.$toolbar.append($img);
2756 render_value: function() {
2757 if (! this.get("effective_readonly")) {
2758 this.$textarea.val(this.get('value') || '');
2759 this._updating_editor = true;
2760 this.$cleditor.updateFrame();
2761 this._updating_editor = false;
2763 this.$el.html(this.get('value'));
2768 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2769 template: 'FieldBoolean',
2772 this.$checkbox = $("input", this.$el);
2773 this.setupFocus(this.$checkbox);
2774 this.$el.click(_.bind(function() {
2775 this.internal_set_value(this.$checkbox.is(':checked'));
2777 var check_readonly = function() {
2778 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2779 self.click_disabled_boolean();
2781 this.on("change:effective_readonly", this, check_readonly);
2782 check_readonly.call(this);
2783 this._super.apply(this, arguments);
2785 render_value: function() {
2786 this.$checkbox[0].checked = this.get('value');
2789 var input = this.$checkbox && this.$checkbox[0];
2790 return input ? input.focus() : false;
2792 click_disabled_boolean: function(){
2793 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2794 $disabled.each(function (){
2795 $(this).next('div').remove();
2796 $(this).closest("span").append($('<div class="boolean"></div>'));
2802 The progressbar field expect a float from 0 to 100.
2804 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2805 template: 'FieldProgressBar',
2806 render_value: function() {
2807 this.$el.progressbar({
2808 value: this.get('value') || 0,
2809 disabled: this.get("effective_readonly")
2811 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2812 this.$('span').html(formatted_value + '%');
2817 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2818 template: 'FieldSelection',
2820 'change select': 'store_dom_value',
2822 init: function(field_manager, node) {
2824 this._super(field_manager, node);
2825 this.set("value", false);
2826 this.set("values", []);
2827 this.records_orderer = new instance.web.DropMisordered();
2828 this.field_manager.on("view_content_has_changed", this, function() {
2829 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
2830 if (! _.isEqual(domain, this.get("domain"))) {
2831 this.set("domain", domain);
2835 initialize_field: function() {
2836 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
2837 this.on("change:domain", this, this.query_values);
2838 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
2839 this.on("change:values", this, this.render_value);
2841 query_values: function() {
2844 if (this.field.type === "many2one") {
2845 var model = new openerp.Model(openerp.session, this.field.relation);
2846 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
2848 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
2849 def = $.when(values);
2851 this.records_orderer.add(def).then(function(values) {
2852 if (! _.isEqual(values, self.get("values"))) {
2853 self.set("values", values);
2857 initialize_content: function() {
2858 // Flag indicating whether we're in an event chain containing a change
2859 // event on the select, in order to know what to do on keyup[RETURN]:
2860 // * If the user presses [RETURN] as part of changing the value of a
2861 // selection, we should just let the value change and not let the
2862 // event broadcast further (e.g. to validating the current state of
2863 // the form in editable list view, which would lead to saving the
2864 // current row or switching to the next one)
2865 // * If the user presses [RETURN] with a select closed (side-effect:
2866 // also if the user opened the select and pressed [RETURN] without
2867 // changing the selected value), takes the action as validating the
2869 var ischanging = false;
2870 var $select = this.$el.find('select')
2871 .change(function () { ischanging = true; })
2872 .click(function () { ischanging = false; })
2873 .keyup(function (e) {
2874 if (e.which !== 13 || !ischanging) { return; }
2875 e.stopPropagation();
2878 this.setupFocus($select);
2880 commit_value: function () {
2881 this.store_dom_value();
2882 return this._super();
2884 store_dom_value: function () {
2885 if (!this.get('effective_readonly') && this.$('select').length) {
2886 var val = JSON.parse(this.$('select').val());
2887 this.internal_set_value(val);
2890 set_value: function(value_) {
2891 value_ = value_ === null ? false : value_;
2892 value_ = value_ instanceof Array ? value_[0] : value_;
2893 this._super(value_);
2895 render_value: function() {
2896 var values = this.get("values");
2897 values = [[false, this.node.attrs.placeholder || '']].concat(values);
2898 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
2900 found = [this.get("value"), _t('Unknown')];
2901 values = [found].concat(values);
2903 if (! this.get("effective_readonly")) {
2904 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
2905 this.$("select").val(JSON.stringify(found[0]));
2907 this.$el.text(found[1]);
2911 var input = this.$('select:first')[0];
2912 return input ? input.focus() : false;
2914 set_dimensions: function (height, width) {
2915 this._super(height, width);
2916 this.$('select').css({
2923 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2924 template: 'FieldRadio',
2926 'click input': 'click_change_value'
2928 init: function(field_manager, node) {
2929 /* Radio button widget: Attributes options:
2930 * - "horizontal" to display in column
2931 * - "no_radiolabel" don't display text values
2933 this._super(field_manager, node);
2934 this.selection = _.clone(this.field.selection) || [];
2935 this.domain = false;
2937 initialize_content: function () {
2938 this.uniqueId = _.uniqueId("radio");
2939 this.on("change:effective_readonly", this, this.render_value);
2940 this.field_manager.on("view_content_has_changed", this, this.get_selection);
2941 this.get_selection();
2943 click_change_value: function (event) {
2944 var val = $(event.target).val();
2945 val = this.field.type == "selection" ? val : +val;
2946 if (val == this.get_value()) {
2947 this.set_value(false);
2949 this.set_value(val);
2952 /** Get the selection and render it
2953 * selection: [[identifier, value_to_display], ...]
2954 * For selection fields: this is directly given by this.field.selection
2955 * For many2one fields: perform a search on the relation of the many2one field
2957 get_selection: function() {
2960 var def = $.Deferred();
2961 if (self.field.type == "many2one") {
2962 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
2963 if (! _.isEqual(self.domain, domain)) {
2964 self.domain = domain;
2965 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
2966 ds.call('search', [self.domain])
2967 .then(function (records) {
2968 ds.name_get(records).then(function (records) {
2969 selection = records;
2974 selection = self.selection;
2978 else if (self.field.type == "selection") {
2979 selection = self.field.selection || [];
2982 return def.then(function () {
2983 if (! _.isEqual(selection, self.selection)) {
2984 self.selection = _.clone(selection);
2985 self.renderElement();
2986 self.render_value();
2990 set_value: function (value_) {
2992 if (this.field.type == "selection") {
2993 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
2995 else if (!this.selection.length) {
2996 this.selection = [value_];
2999 this._super(value_);
3001 get_value: function () {
3002 var value = this.get('value');
3003 return value instanceof Array ? value[0] : value;
3005 render_value: function () {
3007 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3008 this.$("input:checked").prop("checked", false);
3009 if (this.get_value()) {
3010 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3011 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3016 // jquery autocomplete tweak to allow html and classnames
3018 var proto = $.ui.autocomplete.prototype,
3019 initSource = proto._initSource;
3021 function filter( array, term ) {
3022 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3023 return $.grep( array, function(value_) {
3024 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3029 _initSource: function() {
3030 if ( this.options.html && $.isArray(this.options.source) ) {
3031 this.source = function( request, response ) {
3032 response( filter( this.options.source, request.term ) );
3035 initSource.call( this );
3039 _renderItem: function( ul, item) {
3040 return $( "<li></li>" )
3041 .data( "item.autocomplete", item )
3042 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3044 .addClass(item.classname);
3050 A mixin containing some useful methods to handle completion inputs.
3052 The widget containing this option can have these arguments in its widget options:
3053 - no_quick_create: if true, it will disable the quick create
3055 instance.web.form.CompletionFieldMixin = {
3058 this.orderer = new instance.web.DropMisordered();
3061 * Call this method to search using a string.
3063 get_search_result: function(search_val) {
3066 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3067 var blacklist = this.get_search_blacklist();
3068 this.last_query = search_val;
3070 return this.orderer.add(dataset.name_search(
3071 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
3072 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3073 self.last_search = data;
3074 // possible selections for the m2o
3075 var values = _.map(data, function(x) {
3076 x[1] = x[1].split("\n")[0];
3078 label: _.str.escapeHTML(x[1]),
3085 // search more... if more results that max
3086 if (values.length > self.limit) {
3087 values = values.slice(0, self.limit);
3089 label: _t("Search More..."),
3090 action: function() {
3091 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3092 self._search_create_popup("search", data);
3095 classname: 'oe_m2o_dropdown_option'
3099 var raw_result = _(data.result).map(function(x) {return x[1];});
3100 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3101 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3103 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3104 $('<span />').text(search_val).html()),
3105 action: function() {
3106 self._quick_create(search_val);
3108 classname: 'oe_m2o_dropdown_option'
3112 if (!(self.options && self.options.no_create)){
3114 label: _t("Create and Edit..."),
3115 action: function() {
3116 self._search_create_popup("form", undefined, self._create_context(search_val));
3118 classname: 'oe_m2o_dropdown_option'
3125 get_search_blacklist: function() {
3128 _quick_create: function(name) {
3130 var slow_create = function () {
3131 self._search_create_popup("form", undefined, self._create_context(name));
3133 if (self.options.quick_create === undefined || self.options.quick_create) {
3134 new instance.web.DataSet(this, this.field.relation, self.build_context())
3135 .name_create(name).done(function(data) {
3136 if (!self.get('effective_readonly'))
3137 self.add_id(data[0]);
3138 }).fail(function(error, event) {
3139 event.preventDefault();
3145 // all search/create popup handling
3146 _search_create_popup: function(view, ids, context) {
3148 var pop = new instance.web.form.SelectCreatePopup(this);
3150 self.field.relation,
3152 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3153 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3155 disable_multiple_selection: true
3157 self.build_domain(),
3158 new instance.web.CompoundContext(self.build_context(), context || {})
3160 pop.on("elements_selected", self, function(element_ids) {
3161 self.add_id(element_ids[0]);
3168 add_id: function(id) {},
3169 _create_context: function(name) {
3171 var field = (this.options || {}).create_name_field;
3172 if (field === undefined)
3174 if (field !== false && name && (this.options || {}).quick_create !== false)
3175 tmp["default_" + field] = name;
3180 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3181 template: "M2ODialog",
3182 init: function(parent) {
3183 this._super(parent, {
3184 title: _.str.sprintf(_t("Add %s"), parent.string),
3190 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3191 this.$("input").val(this.getParent().last_query);
3192 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3193 self.getParent()._quick_create(self.$("input").val());
3196 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3197 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3200 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3206 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3207 template: "FieldMany2One",
3209 'keydown input': function (e) {
3211 case $.ui.keyCode.UP:
3212 case $.ui.keyCode.DOWN:
3213 e.stopPropagation();
3217 init: function(field_manager, node) {
3218 this._super(field_manager, node);
3219 instance.web.form.CompletionFieldMixin.init.call(this);
3220 this.set({'value': false});
3221 this.display_value = {};
3222 this.display_value_backup = {};
3223 this.last_search = [];
3224 this.floating = false;
3225 this.current_display = null;
3226 this.is_started = false;
3228 reinit_value: function(val) {
3229 this.internal_set_value(val);
3230 this.floating = false;
3231 if (this.is_started)
3232 this.render_value();
3234 initialize_field: function() {
3235 this.is_started = true;
3236 instance.web.bus.on('click', this, function() {
3237 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3238 this.$input.autocomplete("close");
3241 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3243 initialize_content: function() {
3244 if (!this.get("effective_readonly"))
3245 this.render_editable();
3247 destroy_content: function () {
3248 if (this.$drop_down) {
3249 this.$drop_down.off('click');
3250 delete this.$drop_down;
3253 this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3254 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3255 'focus focusout change keydown');
3258 if (this.$follow_button) {
3259 this.$follow_button.off('blur focus click');
3260 delete this.$follow_button;
3263 destroy: function () {
3264 this.destroy_content();
3265 return this._super();
3267 init_error_displayer: function() {
3270 hide_error_displayer: function() {
3273 show_error_displayer: function() {
3274 new instance.web.form.M2ODialog(this).open();
3276 render_editable: function() {
3278 this.$input = this.$el.find("input");
3280 this.init_error_displayer();
3282 self.$input.on('focus', function() {
3283 self.hide_error_displayer();
3286 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3287 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3289 this.$follow_button.click(function(ev) {
3290 ev.preventDefault();
3291 if (!self.get('value')) {
3295 var pop = new instance.web.form.FormOpenPopup(self);
3297 self.field.relation,
3299 self.build_context(),
3301 title: _t("Open: ") + self.string
3304 pop.on('write_completed', self, function(){
3305 self.display_value = {};
3306 self.display_value_backup = {};
3307 self.render_value();
3309 self.view.do_onchange(self);
3313 // some behavior for input
3314 var input_changed = function() {
3315 if (self.current_display !== self.$input.val()) {
3316 self.current_display = self.$input.val();
3317 if (self.$input.val() === "") {
3318 self.internal_set_value(false);
3319 self.floating = false;
3321 self.floating = true;
3325 this.$input.keydown(input_changed);
3326 this.$input.change(input_changed);
3327 this.$drop_down.click(function() {
3328 self.$input.focus();
3329 if (self.$input.autocomplete("widget").is(":visible")) {
3330 self.$input.autocomplete("close");
3332 if (self.get("value") && ! self.floating) {
3333 self.$input.autocomplete("search", "");
3335 self.$input.autocomplete("search");
3340 // Autocomplete close on dialog content scroll
3341 var close_autocomplete = _.debounce(function() {
3342 if (self.$input.autocomplete("widget").is(":visible")) {
3343 self.$input.autocomplete("close");
3346 this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3348 self.ed_def = $.Deferred();
3349 self.uned_def = $.Deferred();
3351 var ed_duration = 15000;
3352 var anyoneLoosesFocus = function (e) {
3354 if (self.floating) {
3355 if (self.last_search.length > 0) {
3356 if (self.last_search[0][0] != self.get("value")) {
3357 self.display_value = {};
3358 self.display_value_backup = {};
3359 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3360 self.reinit_value(self.last_search[0][0]);
3363 self.render_value();
3367 self.reinit_value(false);
3369 self.floating = false;
3371 if (used && self.get("value") === false && ! self.no_ed) {
3372 self.ed_def.reject();
3373 self.uned_def.reject();
3374 self.ed_def = $.Deferred();
3375 self.ed_def.done(function() {
3376 self.show_error_displayer();
3377 ignore_blur = false;
3378 self.trigger('focused');
3381 setTimeout(function() {
3382 self.ed_def.resolve();
3383 self.uned_def.reject();
3384 self.uned_def = $.Deferred();
3385 self.uned_def.done(function() {
3386 self.hide_error_displayer();
3388 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3392 self.ed_def.reject();
3395 var ignore_blur = false;
3397 focusout: anyoneLoosesFocus,
3398 focus: function () { self.trigger('focused'); },
3399 autocompleteopen: function () { ignore_blur = true; },
3400 autocompleteclose: function () { ignore_blur = false; },
3402 // autocomplete open
3403 if (ignore_blur) { return; }
3404 if (_(self.getChildren()).any(function (child) {
3405 return child instanceof instance.web.form.AbstractFormPopup;
3407 self.trigger('blurred');
3411 var isSelecting = false;
3413 this.$input.autocomplete({
3414 source: function(req, resp) {
3415 self.get_search_result(req.term).done(function(result) {
3419 select: function(event, ui) {
3423 self.display_value = {};
3424 self.display_value_backup = {};
3425 self.display_value["" + item.id] = item.name;
3426 self.reinit_value(item.id);
3427 } else if (item.action) {
3429 // Cancel widget blurring, to avoid form blur event
3430 self.trigger('focused');
3434 focus: function(e, ui) {
3438 // disabled to solve a bug, but may cause others
3439 //close: anyoneLoosesFocus,
3443 this.$input.autocomplete("widget").openerpClass();
3444 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3445 this.$input.keyup(function(e) {
3446 if (e.which === 13) { // ENTER
3448 e.stopPropagation();
3450 isSelecting = false;
3452 this.setupFocus(this.$follow_button);
3454 render_value: function(no_recurse) {
3456 if (! this.get("value")) {
3457 this.display_string("");
3460 var display = this.display_value["" + this.get("value")];
3462 this.display_string(display);
3466 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3467 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3469 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3472 self.display_value["" + self.get("value")] = data[0][1];
3473 self.render_value(true);
3474 }).fail( function (data, event) {
3475 // avoid displaying crash errors as many2One should be name_get compliant
3476 event.preventDefault();
3477 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3478 self.render_value(true);
3482 display_string: function(str) {
3484 if (!this.get("effective_readonly")) {
3485 this.$input.val(str.split("\n")[0]);
3486 this.current_display = this.$input.val();
3487 if (this.is_false()) {
3488 this.$('.oe_m2o_cm_button').css({'display':'none'});
3490 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3493 var lines = _.escape(str).split("\n");
3497 follow = _.rest(lines).join("<br />");
3500 var $link = this.$el.find('.oe_form_uri')
3503 if (! this.options.no_open)
3504 $link.click(function () {
3506 type: 'ir.actions.act_window',
3507 res_model: self.field.relation,
3508 res_id: self.get("value"),
3509 views: [[false, 'form']],
3511 context: self.build_context().eval(),
3515 $(".oe_form_m2o_follow", this.$el).html(follow);
3518 set_value: function(value_) {
3520 if (value_ instanceof Array) {
3521 this.display_value = {};
3522 this.display_value_backup = {};
3523 if (! this.options.always_reload) {
3524 this.display_value["" + value_[0]] = value_[1];
3527 this.display_value_backup["" + value_[0]] = value_[1];
3531 value_ = value_ || false;
3532 this.reinit_value(value_);
3534 get_displayed: function() {
3535 return this.display_value["" + this.get("value")];
3537 add_id: function(id) {
3538 this.display_value = {};
3539 this.display_value_backup = {};
3540 this.reinit_value(id);
3542 is_false: function() {
3543 return ! this.get("value");
3545 focus: function () {
3546 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3547 return input ? input.focus() : false;
3549 _quick_create: function() {
3551 this.ed_def.reject();
3552 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3554 _search_create_popup: function() {
3556 this.ed_def.reject();
3557 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3559 set_dimensions: function (height, width) {
3560 this._super(height, width);
3561 this.$input.css('height', height);
3565 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3566 template: 'Many2OneButton',
3567 init: function(field_manager, node) {
3568 this._super.apply(this, arguments);
3571 this._super.apply(this, arguments);
3574 set_button: function() {
3577 this.$button.remove();
3580 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3581 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3582 this.$button.addClass('oe_link').css({'padding':'4px'});
3583 this.$el.append(this.$button);
3584 this.$button.on('click', self.on_click);
3586 on_click: function(ev) {
3588 this.popup = new instance.web.form.FormOpenPopup(this);
3589 this.popup.show_element(
3590 this.field.relation,
3592 this.build_context(),
3593 {title: this.string}
3595 this.popup.on('create_completed', self, function(r) {
3599 set_value: function(value_) {
3601 if (value_ instanceof Array) {
3604 value_ = value_ || false;
3605 this.set('value', value_);
3611 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3612 * the big ugly button in the header.
3614 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3615 * the corresponding field's readonly or effective_readonly property) to
3616 * decide whether the special row should or should not be inserted.
3618 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3619 * set on the insertion row.
3621 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3622 pad_table_to: function (count) {
3623 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3628 this._super(count > 0 ? count - 1 : 0);
3631 var columns = _(this.columns).filter(function (column) {
3632 return column.invisible !== '1';
3634 if (this.options.selectable) { columns++; }
3635 if (this.options.deletable) { columns++; }
3637 var $cell = $('<td>', {
3639 'class': this._add_row_class || ''
3641 $('<a>', {href: '#'}).text(_t("Add an item"))
3642 .mousedown(function () {
3643 // FIXME: needs to be an official API somehow
3644 if (self.view.editor.is_editing()) {
3645 self.view.__ignore_blur = true;
3648 .click(function (e) {
3650 e.stopPropagation();
3651 // FIXME: there should also be an API for that one
3652 if (self.view.editor.form.__blur_timeout) {
3653 clearTimeout(self.view.editor.form.__blur_timeout);
3654 self.view.editor.form.__blur_timeout = false;
3656 self.view.ensure_saved().done(function () {
3657 self.view.do_add_record();
3661 var $padding = this.$current.find('tr:not([data-id]):first');
3662 var $newrow = $('<tr>').append($cell);
3663 if ($padding.length) {
3664 $padding.before($newrow);
3666 this.$current.append($newrow)
3672 # Values: (0, 0, { fields }) create
3673 # (1, ID, { fields }) update
3674 # (2, ID) remove (delete)
3675 # (3, ID) unlink one (target id or target of relation)
3677 # (5) unlink all (only valid for one2many)
3682 'create': function (values) {
3683 return [commands.CREATE, false, values];
3685 // (1, id, {values})
3687 'update': function (id, values) {
3688 return [commands.UPDATE, id, values];
3692 'delete': function (id) {
3693 return [commands.DELETE, id, false];
3695 // (3, id[, _]) removes relation, but not linked record itself
3697 'forget': function (id) {
3698 return [commands.FORGET, id, false];
3702 'link_to': function (id) {
3703 return [commands.LINK_TO, id, false];
3707 'delete_all': function () {
3708 return [5, false, false];
3710 // (6, _, ids) replaces all linked records with provided ids
3712 'replace_with': function (ids) {
3713 return [6, false, ids];
3716 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3717 multi_selection: false,
3718 disable_utility_classes: true,
3719 init: function(field_manager, node) {
3720 this._super(field_manager, node);
3721 lazy_build_o2m_kanban_view();
3722 this.is_loaded = $.Deferred();
3723 this.initial_is_loaded = this.is_loaded;
3724 this.form_last_update = $.Deferred();
3725 this.init_form_last_update = this.form_last_update;
3726 this.is_started = false;
3727 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3728 this.dataset.o2m = this;
3729 this.dataset.parent_view = this.view;
3730 this.dataset.child_name = this.name;
3732 this.dataset.on('dataset_changed', this, function() {
3733 self.trigger_on_change();
3738 this._super.apply(this, arguments);
3739 this.$el.addClass('oe_form_field oe_form_field_one2many');
3744 this.is_loaded.done(function() {
3745 self.on("change:effective_readonly", self, function() {
3746 self.is_loaded = self.is_loaded.then(function() {
3747 self.viewmanager.destroy();
3748 return $.when(self.load_views()).done(function() {
3749 self.reload_current_view();
3754 this.is_started = true;
3755 this.reload_current_view();
3757 trigger_on_change: function() {
3758 this.trigger('changed_value');
3760 load_views: function() {
3763 var modes = this.node.attrs.mode;
3764 modes = !!modes ? modes.split(",") : ["tree"];
3766 _.each(modes, function(mode) {
3767 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3768 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3772 view_type: mode == "tree" ? "list" : mode,
3775 if (self.field.views && self.field.views[mode]) {
3776 view.embedded_view = self.field.views[mode];
3778 if(view.view_type === "list") {
3779 _.extend(view.options, {
3781 selectable: self.multi_selection,
3783 import_enabled: false,
3786 if (self.get("effective_readonly")) {
3787 _.extend(view.options, {
3792 } else if (view.view_type === "form") {
3793 if (self.get("effective_readonly")) {
3794 view.view_type = 'form';
3796 _.extend(view.options, {
3797 not_interactible_on_create: true,
3799 } else if (view.view_type === "kanban") {
3800 _.extend(view.options, {
3801 confirm_on_delete: false,
3803 if (self.get("effective_readonly")) {
3804 _.extend(view.options, {
3805 action_buttons: false,
3806 quick_creatable: false,
3808 read_only_mode: true,
3816 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3817 this.viewmanager.o2m = self;
3818 var once = $.Deferred().done(function() {
3819 self.init_form_last_update.resolve();
3821 var def = $.Deferred().done(function() {
3822 self.initial_is_loaded.resolve();
3824 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3825 controller.o2m = self;
3826 if (view_type == "list") {
3827 if (self.get("effective_readonly")) {
3828 controller.on('edit:before', self, function (e) {
3831 _(controller.columns).find(function (column) {
3832 if (!(column instanceof instance.web.list.Handle)) {
3835 column.modifiers.invisible = true;
3839 } else if (view_type === "form") {
3840 if (self.get("effective_readonly")) {
3841 $(".oe_form_buttons", controller.$el).children().remove();
3843 controller.on("load_record", self, function(){
3846 controller.on('pager_action_executed',self,self.save_any_view);
3847 } else if (view_type == "graph") {
3848 self.reload_current_view();
3852 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3853 $.when(self.save_any_view()).done(function() {
3854 if (n_mode === "list") {
3855 $.async_when().done(function() {
3856 self.reload_current_view();
3861 $.async_when().done(function () {
3862 self.viewmanager.appendTo(self.$el);
3866 reload_current_view: function() {
3868 self.is_loaded = self.is_loaded.then(function() {
3869 var active_view = self.viewmanager.active_view;
3870 var view = self.viewmanager.views[active_view].controller;
3871 if(active_view === "list") {
3872 return view.reload_content();
3873 } else if (active_view === "form") {
3874 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3875 self.dataset.index = 0;
3877 var act = function() {
3878 return view.do_show();
3880 self.form_last_update = self.form_last_update.then(act, act);
3881 return self.form_last_update;
3882 } else if (view.do_search) {
3883 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3886 return self.is_loaded;
3888 set_value: function(value_) {
3889 value_ = value_ || [];
3891 this.dataset.reset_ids([]);
3893 if(value_.length >= 1 && value_[0] instanceof Array) {
3895 _.each(value_, function(command) {
3896 var obj = {values: command[2]};
3897 switch (command[0]) {
3898 case commands.CREATE:
3899 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3901 self.dataset.to_create.push(obj);
3902 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3905 case commands.UPDATE:
3906 obj['id'] = command[1];
3907 self.dataset.to_write.push(obj);
3908 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3911 case commands.DELETE:
3912 self.dataset.to_delete.push({id: command[1]});
3914 case commands.LINK_TO:
3915 ids.push(command[1]);
3917 case commands.DELETE_ALL:
3918 self.dataset.delete_all = true;
3923 this.dataset.set_ids(ids);
3924 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3926 this.dataset.delete_all = true;
3927 _.each(value_, function(command) {
3928 var obj = {values: command};
3929 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3931 self.dataset.to_create.push(obj);
3932 self.dataset.cache.push(_.clone(obj));
3936 this.dataset.set_ids(ids);
3938 this._super(value_);
3939 this.dataset.reset_ids(value_);
3941 if (this.dataset.index === null && this.dataset.ids.length > 0) {
3942 this.dataset.index = 0;
3944 this.trigger_on_change();
3945 if (this.is_started) {
3946 return self.reload_current_view();
3951 get_value: function() {
3955 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3956 val = val.concat(_.map(this.dataset.ids, function(id) {
3957 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3959 return commands.create(alter_order.values);
3961 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3963 return commands.update(alter_order.id, alter_order.values);
3965 return commands.link_to(id);
3967 return val.concat(_.map(
3968 this.dataset.to_delete, function(x) {
3969 return commands['delete'](x.id);}));
3971 commit_value: function() {
3972 return this.save_any_view();
3974 save_any_view: function() {
3975 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3976 this.viewmanager.views[this.viewmanager.active_view] &&
3977 this.viewmanager.views[this.viewmanager.active_view].controller) {
3978 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3979 if (this.viewmanager.active_view === "form") {
3980 if (view.is_initialized.state() !== 'resolved') {
3981 return $.when(false);
3983 return $.when(view.save());
3984 } else if (this.viewmanager.active_view === "list") {
3985 return $.when(view.ensure_saved());
3988 return $.when(false);
3990 is_syntax_valid: function() {
3991 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3993 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3994 switch (this.viewmanager.active_view) {
3996 return _(view.fields).chain()
4001 return view.is_valid();
4007 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4008 template: 'One2Many.viewmanager',
4009 init: function(parent, dataset, views, flags) {
4010 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4011 this.registry = this.registry.extend({
4012 list: 'instance.web.form.One2ManyListView',
4013 form: 'instance.web.form.One2ManyFormView',
4014 kanban: 'instance.web.form.One2ManyKanbanView',
4016 this.__ignore_blur = false;
4018 switch_mode: function(mode, unused) {
4019 if (mode !== 'form') {
4020 return this._super(mode, unused);
4023 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4024 var pop = new instance.web.form.FormOpenPopup(this);
4025 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4026 title: _t("Open: ") + self.o2m.string,
4027 create_function: function(data, options) {
4028 return self.o2m.dataset.create(data, options).done(function(r) {
4029 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4030 self.o2m.dataset.trigger("dataset_changed", r);
4033 write_function: function(id, data, options) {
4034 return self.o2m.dataset.write(id, data, {}).done(function() {
4035 self.o2m.reload_current_view();
4038 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4039 parent_view: self.o2m.view,
4040 child_name: self.o2m.name,
4041 read_function: function() {
4042 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4044 form_view_options: {'not_interactible_on_create':true},
4045 readonly: self.o2m.get("effective_readonly")
4047 pop.on("elements_selected", self, function() {
4048 self.o2m.reload_current_view();
4053 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4054 get_context: function() {
4055 this.context = this.o2m.build_context();
4056 return this.context;
4060 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4061 _template: 'One2Many.listview',
4062 init: function (parent, dataset, view_id, options) {
4063 this._super(parent, dataset, view_id, _.extend(options || {}, {
4064 GroupsType: instance.web.form.One2ManyGroups,
4065 ListType: instance.web.form.One2ManyList
4067 this.on('edit:after', this, this.proxy('_after_edit'));
4068 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4071 .bind('add', this.proxy("changed_records"))
4072 .bind('edit', this.proxy("changed_records"))
4073 .bind('remove', this.proxy("changed_records"));
4075 start: function () {
4076 var ret = this._super();
4078 .off('mousedown.handleButtons')
4079 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4082 changed_records: function () {
4083 this.o2m.trigger_on_change();
4085 is_valid: function () {
4086 var editor = this.editor;
4087 var form = editor.form;
4088 // If no edition is pending, the listview can not be invalid (?)
4089 if (!editor.record) {
4092 // If the form has not been modified, the view can only be valid
4093 // NB: is_dirty will also be set on defaults/onchanges/whatever?
4094 // oe_form_dirty seems to only be set on actual user actions
4095 if (!form.$el.is('.oe_form_dirty')) {
4098 this.o2m._dirty_flag = true;
4100 // Otherwise validate internal form
4101 return _(form.fields).chain()
4102 .invoke(function () {
4103 this._check_css_flags();
4104 return this.is_valid();
4109 do_add_record: function () {
4110 if (this.editable()) {
4111 this._super.apply(this, arguments);
4114 var pop = new instance.web.form.SelectCreatePopup(this);
4116 self.o2m.field.relation,
4118 title: _t("Create: ") + self.o2m.string,
4119 initial_view: "form",
4120 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4121 create_function: function(data, options) {
4122 return self.o2m.dataset.create(data, options).done(function(r) {
4123 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4124 self.o2m.dataset.trigger("dataset_changed", r);
4127 read_function: function() {
4128 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4130 parent_view: self.o2m.view,
4131 child_name: self.o2m.name,
4132 form_view_options: {'not_interactible_on_create':true}
4134 self.o2m.build_domain(),
4135 self.o2m.build_context()
4137 pop.on("elements_selected", self, function() {
4138 self.o2m.reload_current_view();
4142 do_activate_record: function(index, id) {
4144 var pop = new instance.web.form.FormOpenPopup(self);
4145 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4146 title: _t("Open: ") + self.o2m.string,
4147 write_function: function(id, data) {
4148 return self.o2m.dataset.write(id, data, {}).done(function() {
4149 self.o2m.reload_current_view();
4152 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4153 parent_view: self.o2m.view,
4154 child_name: self.o2m.name,
4155 read_function: function() {
4156 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4158 form_view_options: {'not_interactible_on_create':true},
4159 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4162 do_button_action: function (name, id, callback) {
4163 if (!_.isNumber(id)) {
4164 instance.webclient.notification.warn(
4165 _t("Action Button"),
4166 _t("The o2m record must be saved before an action can be used"));
4169 var parent_form = this.o2m.view;
4171 this.ensure_saved().then(function () {
4173 return parent_form.save();
4176 }).done(function () {
4177 if (!self.o2m.options.reload_on_button) {
4178 self.handle_button(name, id, callback);
4180 self.handle_button(name, id, function(){
4181 self.o2m.view.reload();
4187 _after_edit: function () {
4188 this.__ignore_blur = false;
4189 this.editor.form.on('blurred', this, this._on_form_blur);
4191 // The form's blur thing may be jiggered during the edition setup,
4192 // potentially leading to the o2m instasaving the row. Cancel any
4193 // blurring triggered the edition startup here
4194 this.editor.form.widgetFocused();
4196 _before_unedit: function () {
4197 this.editor.form.off('blurred', this, this._on_form_blur);
4199 _button_down: function () {
4200 // If a button is clicked (usually some sort of action button), it's
4201 // the button's responsibility to ensure the editable list is in the
4202 // correct state -> ignore form blurring
4203 this.__ignore_blur = true;
4206 * Handles blurring of the nested form (saves the currently edited row),
4207 * unless the flag to ignore the event is set to ``true``
4209 * Makes the internal form go away
4211 _on_form_blur: function () {
4212 if (this.__ignore_blur) {
4213 this.__ignore_blur = false;
4216 // FIXME: why isn't there an API for this?
4217 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4218 this.ensure_saved();
4221 this.cancel_edition();
4223 keypress_ENTER: function () {
4224 // blurring caused by hitting the [Return] key, should skip the
4225 // autosave-on-blur and let the handler for [Return] do its thing (save
4226 // the current row *anyway*, then create a new one/edit the next one)
4227 this.__ignore_blur = true;
4228 this._super.apply(this, arguments);
4230 do_delete: function (ids) {
4231 var confirm = window.confirm;
4232 window.confirm = function () { return true; };
4234 return this._super(ids);
4236 window.confirm = confirm;
4239 reload_record: function (record) {
4240 // Evict record.id from cache to ensure it will be reloaded correctly
4241 this.dataset.evict_record(record.get('id'));
4243 return this._super(record);
4246 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4247 setup_resequence_rows: function () {
4248 if (!this.view.o2m.get('effective_readonly')) {
4249 this._super.apply(this, arguments);
4253 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4254 _add_row_class: 'oe_form_field_one2many_list_row_add',
4255 is_readonly: function () {
4256 return this.view.o2m.get('effective_readonly');
4260 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4261 form_template: 'One2Many.formview',
4262 load_form: function(data) {
4265 this.$buttons.find('button.oe_form_button_create').click(function() {
4266 self.save().done(self.on_button_new);
4269 do_notify_change: function() {
4270 if (this.dataset.parent_view) {
4271 this.dataset.parent_view.do_notify_change();
4273 this._super.apply(this, arguments);
4278 var lazy_build_o2m_kanban_view = function() {
4279 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4281 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4285 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4286 template: "FieldMany2ManyTags",
4287 tag_template: "FieldMany2ManyTag",
4289 this._super.apply(this, arguments);
4290 instance.web.form.CompletionFieldMixin.init.call(this);
4291 this.set({"value": []});
4292 this._display_orderer = new instance.web.DropMisordered();
4293 this._drop_shown = false;
4295 initialize_texttext: function(){
4298 plugins : 'tags arrow autocomplete',
4300 render: function(suggestion) {
4301 return $('<span class="text-label"/>').
4302 data('index', suggestion['index']).html(suggestion['label']);
4307 selectFromDropdown: function() {
4308 this.trigger('hideDropdown');
4309 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4310 var data = self.search_result[index];
4312 self.add_id(data.id);
4314 self.ignore_blur = true;
4317 this.trigger('setSuggestions', {result : []});
4321 isTagAllowed: function(tag) {
4325 removeTag: function(tag) {
4326 var id = tag.data("id");
4327 self.set({"value": _.without(self.get("value"), id)});
4329 renderTag: function(stuff) {
4330 return $.fn.textext.TextExtTags.prototype.renderTag.
4331 call(this, stuff).data("id", stuff.id);
4335 itemToString: function(item) {
4340 onSetInputData: function(e, data) {
4342 this._plugins.autocomplete._suggestions = null;
4344 this.input().val(data);
4350 initialize_content: function() {
4351 if (this.get("effective_readonly"))
4354 self.ignore_blur = false;
4355 self.$text = this.$("textarea");
4356 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4358 var str = !!data ? data.query || '' : '';
4359 self.get_search_result(str).done(function(result) {
4360 self.search_result = result;
4361 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4362 return _.extend(el, {index:i});
4365 }).bind('hideDropdown', function() {
4366 self._drop_shown = false;
4367 }).bind('showDropdown', function() {
4368 self._drop_shown = true;
4370 self.tags = self.$text.textext()[0].tags();
4372 .focusin(function () {
4373 self.trigger('focused');
4374 self.ignore_blur = false;
4376 .focusout(function() {
4377 self.$text.trigger("setInputData", "");
4378 if (!self.ignore_blur) {
4379 self.trigger('blurred');
4381 }).keydown(function(e) {
4382 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4383 self.$text.textext()[0].autocomplete().selectFromDropdown();
4387 set_value: function(value_) {
4388 value_ = value_ || [];
4389 if (value_.length >= 1 && value_[0] instanceof Array) {
4390 value_ = value_[0][2];
4392 this._super(value_);
4394 is_false: function() {
4395 return _(this.get("value")).isEmpty();
4397 get_value: function() {
4398 var tmp = [commands.replace_with(this.get("value"))];
4401 get_search_blacklist: function() {
4402 return this.get("value");
4404 map_tag: function(data){
4405 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4407 get_render_data: function(ids){
4409 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4410 return dataset.name_get(ids);
4412 render_tag: function(data) {
4414 if (! self.get("effective_readonly")) {
4415 self.tags.containerElement().children().remove();
4416 self.$('textarea').css("padding-left", "3px");
4417 self.tags.addTags(self.map_tag(data));
4419 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4422 render_value: function() {
4424 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4425 var values = self.get("value");
4426 var handle_names = function(data) {
4427 if (self.isDestroyed())
4430 _.each(data, function(el) {
4431 indexed[el[0]] = el;
4433 data = _.map(values, function(el) { return indexed[el]; });
4434 self.render_tag(data);
4436 if (! values || values.length > 0) {
4437 this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4443 add_id: function(id) {
4444 this.set({'value': _.uniq(this.get('value').concat([id]))});
4446 focus: function () {
4447 var input = this.$text && this.$text[0];
4448 return input ? input.focus() : false;
4450 set_dimensions: function (height, width) {
4451 this._super(height, width);
4452 this.$("textarea").css({
4457 _search_create_popup: function() {
4458 self.ignore_blur = true;
4459 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4465 - reload_on_button: Reload the whole form view if click on a button in a list view.
4466 If you see this options, do not use it, it's basically a dirty hack to make one
4467 precise o2m to behave the way we want.
4469 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4470 multi_selection: false,
4471 disable_utility_classes: true,
4472 init: function(field_manager, node) {
4473 this._super(field_manager, node);
4474 this.is_loaded = $.Deferred();
4475 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4476 this.dataset.m2m = this;
4478 this.dataset.on('unlink', self, function(ids) {
4479 self.dataset_changed();
4482 this.list_dm = new instance.web.DropMisordered();
4483 this.render_value_dm = new instance.web.DropMisordered();
4485 initialize_content: function() {
4488 this.$el.addClass('oe_form_field oe_form_field_many2many');
4490 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4492 'deletable': this.get("effective_readonly") ? false : true,
4493 'selectable': this.multi_selection,
4495 'reorderable': false,
4496 'import_enabled': false,
4498 var embedded = (this.field.views || {}).tree;
4500 this.list_view.set_embedded_view(embedded);
4502 this.list_view.m2m_field = this;
4503 var loaded = $.Deferred();
4504 this.list_view.on("list_view_loaded", this, function() {
4507 this.list_view.appendTo(this.$el);
4509 var old_def = self.is_loaded;
4510 self.is_loaded = $.Deferred().done(function() {
4513 this.list_dm.add(loaded).then(function() {
4514 self.is_loaded.resolve();
4517 destroy_content: function() {
4518 this.list_view.destroy();
4519 this.list_view = undefined;
4521 set_value: function(value_) {
4522 value_ = value_ || [];
4523 if (value_.length >= 1 && value_[0] instanceof Array) {
4524 value_ = value_[0][2];
4526 this._super(value_);
4528 get_value: function() {
4529 return [commands.replace_with(this.get('value'))];
4531 is_false: function () {
4532 return _(this.get("value")).isEmpty();
4534 render_value: function() {
4536 this.dataset.set_ids(this.get("value"));
4537 this.render_value_dm.add(this.is_loaded).then(function() {
4538 return self.list_view.reload_content();
4541 dataset_changed: function() {
4542 this.internal_set_value(this.dataset.ids);
4546 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4547 get_context: function() {
4548 this.context = this.m2m.build_context();
4549 return this.context;
4555 * @extends instance.web.ListView
4557 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4558 init: function (parent, dataset, view_id, options) {
4559 this._super(parent, dataset, view_id, _.extend(options || {}, {
4560 ListType: instance.web.form.Many2ManyList,
4563 do_add_record: function () {
4564 var pop = new instance.web.form.SelectCreatePopup(this);
4568 title: _t("Add: ") + this.m2m_field.string
4570 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4571 this.m2m_field.build_context()
4574 pop.on("elements_selected", self, function(element_ids) {
4576 _(element_ids).each(function (id) {
4577 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4578 self.dataset.set_ids(self.dataset.ids.concat([id]));
4579 self.m2m_field.dataset_changed();
4584 self.reload_content();
4588 do_activate_record: function(index, id) {
4590 var pop = new instance.web.form.FormOpenPopup(this);
4591 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4592 title: _t("Open: ") + this.m2m_field.string,
4593 readonly: this.getParent().get("effective_readonly")
4595 pop.on('write_completed', self, self.reload_content);
4597 do_button_action: function(name, id, callback) {
4599 var _sup = _.bind(this._super, this);
4600 if (! this.m2m_field.options.reload_on_button) {
4601 return _sup(name, id, callback);
4603 return this.m2m_field.view.save().then(function() {
4604 return _sup(name, id, function() {
4605 self.m2m_field.view.reload();
4610 is_action_enabled: function () { return true; },
4612 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4613 _add_row_class: 'oe_form_field_many2many_list_row_add',
4614 is_readonly: function () {
4615 return this.view.m2m_field.get('effective_readonly');
4619 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4620 disable_utility_classes: true,
4621 init: function(field_manager, node) {
4622 this._super(field_manager, node);
4623 instance.web.form.CompletionFieldMixin.init.call(this);
4624 m2m_kanban_lazy_init();
4625 this.is_loaded = $.Deferred();
4626 this.initial_is_loaded = this.is_loaded;
4629 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4630 this.dataset.m2m = this;
4631 this.dataset.on('unlink', self, function(ids) {
4632 self.dataset_changed();
4636 this._super.apply(this, arguments);
4641 self.on("change:effective_readonly", self, function() {
4642 self.is_loaded = self.is_loaded.then(function() {
4643 self.kanban_view.destroy();
4644 return $.when(self.load_view()).done(function() {
4645 self.render_value();
4650 set_value: function(value_) {
4651 value_ = value_ || [];
4652 if (value_.length >= 1 && value_[0] instanceof Array) {
4653 value_ = value_[0][2];
4655 this._super(value_);
4657 get_value: function() {
4658 return [commands.replace_with(this.get('value'))];
4660 load_view: function() {
4662 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4663 'create_text': _t("Add"),
4664 'creatable': self.get("effective_readonly") ? false : true,
4665 'quick_creatable': self.get("effective_readonly") ? false : true,
4666 'read_only_mode': self.get("effective_readonly") ? true : false,
4667 'confirm_on_delete': false,
4669 var embedded = (this.field.views || {}).kanban;
4671 this.kanban_view.set_embedded_view(embedded);
4673 this.kanban_view.m2m = this;
4674 var loaded = $.Deferred();
4675 this.kanban_view.on("kanban_view_loaded",self,function() {
4676 self.initial_is_loaded.resolve();
4679 this.kanban_view.on('switch_mode', this, this.open_popup);
4680 $.async_when().done(function () {
4681 self.kanban_view.appendTo(self.$el);
4685 render_value: function() {
4687 this.dataset.set_ids(this.get("value"));
4688 this.is_loaded = this.is_loaded.then(function() {
4689 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4692 dataset_changed: function() {
4693 this.set({'value': this.dataset.ids});
4695 open_popup: function(type, unused) {
4696 if (type !== "form")
4700 if (this.dataset.index === null) {
4701 pop = new instance.web.form.SelectCreatePopup(this);
4703 this.field.relation,
4705 title: _t("Add: ") + this.string
4707 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4708 this.build_context()
4710 pop.on("elements_selected", self, function(element_ids) {
4711 _.each(element_ids, function(one_id) {
4712 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4713 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4714 self.dataset_changed();
4715 self.render_value();
4720 var id = self.dataset.ids[self.dataset.index];
4721 pop = new instance.web.form.FormOpenPopup(this);
4722 pop.show_element(self.field.relation, id, self.build_context(), {
4723 title: _t("Open: ") + self.string,
4724 write_function: function(id, data, options) {
4725 return self.dataset.write(id, data, {}).done(function() {
4726 self.render_value();
4729 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4730 parent_view: self.view,
4731 child_name: self.name,
4732 readonly: self.get("effective_readonly")
4736 add_id: function(id) {
4737 this.quick_create.add_id(id);
4741 function m2m_kanban_lazy_init() {
4742 if (instance.web.form.Many2ManyKanbanView)
4744 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4745 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4746 _is_quick_create_enabled: function() {
4747 return this._super() && ! this.group_by;
4750 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4751 template: 'Many2ManyKanban.quick_create',
4754 * close_btn: If true, the widget will display a "Close" button able to trigger
4757 init: function(parent, dataset, context, buttons) {
4758 this._super(parent);
4759 this.m2m = this.getParent().view.m2m;
4760 this.m2m.quick_create = this;
4761 this._dataset = dataset;
4762 this._buttons = buttons || false;
4763 this._context = context || {};
4765 start: function () {
4767 self.$text = this.$el.find('input').css("width", "200px");
4768 self.$text.textext({
4769 plugins : 'arrow autocomplete',
4771 render: function(suggestion) {
4772 return $('<span class="text-label"/>').
4773 data('index', suggestion['index']).html(suggestion['label']);
4778 selectFromDropdown: function() {
4779 $(this).trigger('hideDropdown');
4780 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4781 var data = self.search_result[index];
4783 self.add_id(data.id);
4790 itemToString: function(item) {
4795 }).bind('getSuggestions', function(e, data) {
4797 var str = !!data ? data.query || '' : '';
4798 self.m2m.get_search_result(str).done(function(result) {
4799 self.search_result = result;
4800 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4801 return _.extend(el, {index:i});
4805 self.$text.focusout(function() {
4810 this.$text[0].focus();
4812 add_id: function(id) {
4815 self.trigger('added', id);
4816 this.m2m.dataset_changed();
4822 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4824 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4825 template: "AbstractFormPopup.render",
4828 * -readonly: only applicable when not in creation mode, default to false
4829 * - alternative_form_view
4836 * - form_view_options
4838 init_popup: function(model, row_id, domain, context, options) {
4839 this.row_id = row_id;
4841 this.domain = domain || [];
4842 this.context = context || {};
4843 this.options = options;
4844 _.defaults(this.options, {
4847 init_dataset: function() {
4849 this.created_elements = [];
4850 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4851 this.dataset.read_function = this.options.read_function;
4852 this.dataset.create_function = function(data, options, sup) {
4853 var fct = self.options.create_function || sup;
4854 return fct.call(this, data, options).done(function(r) {
4855 self.trigger('create_completed saved', r);
4856 self.created_elements.push(r);
4859 this.dataset.write_function = function(id, data, options, sup) {
4860 var fct = self.options.write_function || sup;
4861 return fct.call(this, id, data, options).done(function(r) {
4862 self.trigger('write_completed saved', r);
4865 this.dataset.parent_view = this.options.parent_view;
4866 this.dataset.child_name = this.options.child_name;
4868 display_popup: function() {
4870 this.renderElement();
4871 var dialog = new instance.web.Dialog(this, {
4873 dialogClass: 'oe_act_window',
4875 self.check_exit(true);
4877 title: this.options.title || "",
4878 }, this.$el).open();
4879 this.$buttonpane = dialog.$buttons;
4882 setup_form_view: function() {
4885 this.dataset.ids = [this.row_id];
4886 this.dataset.index = 0;
4888 this.dataset.index = null;
4890 var options = _.clone(self.options.form_view_options) || {};
4891 if (this.row_id !== null) {
4892 options.initial_mode = this.options.readonly ? "view" : "edit";
4895 $buttons: this.$buttonpane,
4897 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4898 if (this.options.alternative_form_view) {
4899 this.view_form.set_embedded_view(this.options.alternative_form_view);
4901 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4902 this.view_form.on("form_view_loaded", self, function() {
4903 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4904 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4905 multi_select: multi_select,
4906 readonly: self.row_id !== null && self.options.readonly,
4908 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4909 $snbutton.click(function() {
4910 $.when(self.view_form.save()).done(function() {
4911 self.view_form.reload_mutex.exec(function() {
4912 self.view_form.on_button_new();
4916 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4917 $sbutton.click(function() {
4918 $.when(self.view_form.save()).done(function() {
4919 self.view_form.reload_mutex.exec(function() {
4924 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4925 $cbutton.click(function() {
4926 self.view_form.trigger('on_button_cancel');
4929 self.view_form.do_show();
4932 select_elements: function(element_ids) {
4933 this.trigger("elements_selected", element_ids);
4935 check_exit: function(no_destroy) {
4936 if (this.created_elements.length > 0) {
4937 this.select_elements(this.created_elements);
4938 this.created_elements = [];
4940 this.trigger('closed');
4943 destroy: function () {
4944 this.trigger('closed');
4945 if (this.$el.is(":data(dialog)")) {
4946 this.$el.dialog('close');
4953 * Class to display a popup containing a form view.
4955 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4956 show_element: function(model, row_id, context, options) {
4957 this.init_popup(model, row_id, [], context, options);
4958 _.defaults(this.options, {
4960 this.display_popup();
4964 this.init_dataset();
4965 this.setup_form_view();
4970 * Class to display a popup to display a list to search a row. It also allows
4971 * to switch to a form view to create a new row.
4973 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4977 * - initial_view: form or search (default search)
4978 * - disable_multiple_selection
4979 * - list_view_options
4981 select_element: function(model, options, domain, context) {
4982 this.init_popup(model, null, domain, context, options);
4984 _.defaults(this.options, {
4985 initial_view: "search",
4987 this.initial_ids = this.options.initial_ids;
4988 this.display_popup();
4992 this.init_dataset();
4993 if (this.options.initial_view == "search") {
4994 instance.web.pyeval.eval_domains_and_contexts({
4996 contexts: [this.context]
4997 }).done(function (results) {
4998 var search_defaults = {};
4999 _.each(results.context, function (value_, key) {
5000 var match = /^search_default_(.*)$/.exec(key);
5002 search_defaults[match[1]] = value_;
5005 self.setup_search_view(search_defaults);
5011 setup_search_view: function(search_defaults) {
5013 if (this.searchview) {
5014 this.searchview.destroy();
5016 this.searchview = new instance.web.SearchView(this,
5017 this.dataset, false, search_defaults);
5018 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5019 if (self.initial_ids) {
5020 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5021 contexts.concat(self.context), groupbys);
5022 self.initial_ids = undefined;
5024 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5027 this.searchview.on("search_view_loaded", self, function() {
5028 self.view_list = new instance.web.form.SelectCreateListView(self,
5029 self.dataset, false,
5030 _.extend({'deletable': false,
5031 'selectable': !self.options.disable_multiple_selection,
5032 'import_enabled': false,
5033 '$buttons': self.$buttonpane,
5034 'disable_editable_mode': true,
5035 '$pager': self.$('.oe_popup_list_pager'),
5036 }, self.options.list_view_options || {}));
5037 self.view_list.on('edit:before', self, function (e) {
5040 self.view_list.popup = self;
5041 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5042 self.view_list.do_show();
5043 }).then(function() {
5044 self.searchview.do_search();
5046 self.view_list.on("list_view_loaded", self, function() {
5047 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5048 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5049 $cbutton.click(function() {
5052 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5053 $sbutton.click(function() {
5054 self.select_elements(self.selected_ids);
5057 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5058 $cbutton.click(function() {
5063 this.searchview.appendTo($(".oe_popup_search", self.$el));
5065 do_search: function(domains, contexts, groupbys) {
5067 instance.web.pyeval.eval_domains_and_contexts({
5068 domains: domains || [],
5069 contexts: contexts || [],
5070 group_by_seq: groupbys || []
5071 }).done(function (results) {
5072 self.view_list.do_search(results.domain, results.context, results.group_by);
5075 on_click_element: function(ids) {
5077 this.selected_ids = ids || [];
5078 if(this.selected_ids.length > 0) {
5079 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5081 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5084 new_object: function() {
5085 if (this.searchview) {
5086 this.searchview.hide();
5088 if (this.view_list) {
5089 this.view_list.do_hide();
5091 this.setup_form_view();
5095 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5096 do_add_record: function () {
5097 this.popup.new_object();
5099 select_record: function(index) {
5100 this.popup.select_elements([this.dataset.ids[index]]);
5101 this.popup.destroy();
5103 do_select: function(ids, records) {
5104 this._super(ids, records);
5105 this.popup.on_click_element(ids);
5109 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5110 template: 'FieldReference',
5111 init: function(field_manager, node) {
5112 this._super(field_manager, node);
5113 this.reference_ready = true;
5115 destroy_content: function() {
5118 this.fm = undefined;
5121 initialize_content: function() {
5123 var fm = new instance.web.form.DefaultFieldManager(this);
5125 fm.extend_field_desc({
5127 selection: this.field_manager.get_field_desc(this.name).selection,
5135 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5137 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5139 this.selection.on("change:value", this, this.on_selection_changed);
5140 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5142 .on('focused', null, function () {self.trigger('focused');})
5143 .on('blurred', null, function () {self.trigger('blurred');});
5145 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5146 name: 'Referenced Document',
5147 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5149 this.m2o.on("change:value", this, this.data_changed);
5150 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5152 .on('focused', null, function () {self.trigger('focused');})
5153 .on('blurred', null, function () {self.trigger('blurred');});
5155 on_selection_changed: function() {
5156 if (this.reference_ready) {
5157 this.internal_set_value([this.selection.get_value(), false]);
5158 this.render_value();
5161 data_changed: function() {
5162 if (this.reference_ready) {
5163 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5166 set_value: function(val) {
5168 val = val.split(',');
5169 val[0] = val[0] || false;
5170 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5172 this._super(val || [false, false]);
5174 get_value: function() {
5175 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5177 render_value: function() {
5178 this.reference_ready = false;
5179 if (!this.get("effective_readonly")) {
5180 this.selection.set_value(this.get('value')[0]);
5182 this.m2o.field.relation = this.get('value')[0];
5183 this.m2o.set_value(this.get('value')[1]);
5184 this.m2o.$el.toggle(!!this.get('value')[0]);
5185 this.reference_ready = true;
5189 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5190 init: function(field_manager, node) {
5192 this._super(field_manager, node);
5193 this.binary_value = false;
5194 this.useFileAPI = !!window.FileReader;
5195 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5196 if (!this.useFileAPI) {
5197 this.fileupload_id = _.uniqueId('oe_fileupload');
5198 $(window).on(this.fileupload_id, function() {
5199 var args = [].slice.call(arguments).slice(1);
5200 self.on_file_uploaded.apply(self, args);
5205 if (!this.useFileAPI) {
5206 $(window).off(this.fileupload_id);
5208 this._super.apply(this, arguments);
5210 initialize_content: function() {
5211 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5212 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5213 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5215 on_file_change: function(e) {
5217 var file_node = e.target;
5218 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5219 if (this.useFileAPI) {
5220 var file = file_node.files[0];
5221 if (file.size > this.max_upload_size) {
5222 var msg = _t("The selected file exceed the maximum file size of %s.");
5223 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5226 var filereader = new FileReader();
5227 filereader.readAsDataURL(file);
5228 filereader.onloadend = function(upload) {
5229 var data = upload.target.result;
5230 data = data.split(',')[1];
5231 self.on_file_uploaded(file.size, file.name, file.type, data);
5234 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5235 this.$el.find('form.oe_form_binary_form').submit();
5237 this.$el.find('.oe_form_binary_progress').show();
5238 this.$el.find('.oe_form_binary').hide();
5241 on_file_uploaded: function(size, name, content_type, file_base64) {
5242 if (size === false) {
5243 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5244 // TODO: use openerp web crashmanager
5245 console.warn("Error while uploading file : ", name);
5247 this.filename = name;
5248 this.on_file_uploaded_and_valid.apply(this, arguments);
5250 this.$el.find('.oe_form_binary_progress').hide();
5251 this.$el.find('.oe_form_binary').show();
5253 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5255 on_save_as: function(ev) {
5256 var value = this.get('value');
5258 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5259 ev.stopPropagation();
5261 instance.web.blockUI();
5262 var c = instance.webclient.crashmanager;
5263 this.session.get_file({
5264 url: '/web/binary/saveas_ajax',
5265 data: {data: JSON.stringify({
5266 model: this.view.dataset.model,
5267 id: (this.view.datarecord.id || ''),
5269 filename_field: (this.node.attrs.filename || ''),
5270 data: instance.web.form.is_bin_size(value) ? null : value,
5271 context: this.view.dataset.get_context()
5273 complete: instance.web.unblockUI,
5274 error: c.rpc_error.bind(c)
5276 ev.stopPropagation();
5280 set_filename: function(value) {
5281 var filename = this.node.attrs.filename;
5284 tmp[filename] = value;
5285 this.field_manager.set_values(tmp);
5288 on_clear: function() {
5289 if (this.get('value') !== false) {
5290 this.binary_value = false;
5291 this.internal_set_value(false);
5297 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5298 template: 'FieldBinaryFile',
5299 initialize_content: function() {
5301 if (this.get("effective_readonly")) {
5303 this.$el.find('a').click(function(ev) {
5304 if (self.get('value')) {
5305 self.on_save_as(ev);
5311 render_value: function() {
5313 if (!this.get("effective_readonly")) {
5314 if (this.node.attrs.filename) {
5315 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5317 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5319 this.$el.find('input').eq(0).val(show_value);
5321 this.$el.find('a').toggle(!!this.get('value'));
5322 if (this.get('value')) {
5323 show_value = _t("Download");
5325 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5326 this.$el.find('a').text(show_value);
5330 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5331 this.binary_value = true;
5332 this.internal_set_value(file_base64);
5333 var show_value = name + " (" + instance.web.human_size(size) + ")";
5334 this.$el.find('input').eq(0).val(show_value);
5335 this.set_filename(name);
5337 on_clear: function() {
5338 this._super.apply(this, arguments);
5339 this.$el.find('input').eq(0).val('');
5340 this.set_filename('');
5344 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5345 template: 'FieldBinaryImage',
5346 placeholder: "/web/static/src/img/placeholder.png",
5347 render_value: function() {
5350 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5351 url = 'data:image/png;base64,' + this.get('value');
5352 } else if (this.get('value')) {
5353 var id = JSON.stringify(this.view.datarecord.id || null);
5354 var field = this.name;
5355 if (this.options.preview_image)
5356 field = this.options.preview_image;
5357 url = this.session.url('/web/binary/image', {
5358 model: this.view.dataset.model,
5361 t: (new Date().getTime()),
5364 url = this.placeholder;
5366 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5367 $($img).click(function(e) {
5368 if(self.view.get("actual_mode") == "view") {
5369 var $button = $(".oe_form_button_edit");
5370 $button.openerpBounce();
5371 e.stopPropagation();
5374 this.$el.find('> img').remove();
5375 this.$el.prepend($img);
5376 $img.load(function() {
5377 if (! self.options.size)
5379 $img.css("max-width", "" + self.options.size[0] + "px");
5380 $img.css("max-height", "" + self.options.size[1] + "px");
5381 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5382 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5384 $img.on('error', function() {
5385 $img.attr('src', self.placeholder);
5386 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5389 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5390 this.internal_set_value(file_base64);
5391 this.binary_value = true;
5392 this.render_value();
5393 this.set_filename(name);
5395 on_clear: function() {
5396 this._super.apply(this, arguments);
5397 this.render_value();
5398 this.set_filename('');
5403 * Widget for (many2many field) to upload one or more file in same time and display in list.
5404 * The user can delete his files.
5405 * Options on attribute ; "blockui" {Boolean} block the UI or not
5406 * during the file is uploading
5408 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5409 template: "FieldBinaryFileUploader",
5410 init: function(field_manager, node) {
5411 this._super(field_manager, node);
5412 this.field_manager = field_manager;
5414 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5415 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);
5419 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5420 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5421 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5425 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5427 set_value: function(value_) {
5428 value_ = value_ || [];
5429 if (value_.length >= 1 && value_[0] instanceof Array) {
5430 value_ = value_[0][2];
5432 this._super(value_);
5434 get_value: function() {
5435 var tmp = [commands.replace_with(this.get("value"))];
5438 get_file_url: function (attachment) {
5439 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5441 read_name_values : function () {
5443 // don't reset know values
5444 var ids = this.get('value');
5445 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5446 // send request for get_name
5447 if (_value.length) {
5448 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5449 _.each(datas, function (data) {
5450 data.no_unlink = true;
5451 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5452 self.data[data.id] = data;
5460 render_value: function () {
5462 this.read_name_values().then(function (ids) {
5463 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5464 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5465 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5467 // reinit input type file
5468 var $input = self.$('input.oe_form_binary_file');
5469 $input.after($input.clone(true)).remove();
5470 self.$(".oe_fileupload").show();
5474 on_file_change: function (event) {
5475 event.stopPropagation();
5477 var $target = $(event.target);
5478 if ($target.val() !== '') {
5479 var filename = $target.val().replace(/.*[\\\/]/,'');
5480 // don't uplode more of one file in same time
5481 if (self.data[0] && self.data[0].upload ) {
5484 for (var id in this.get('value')) {
5485 // if the files exits, delete the file before upload (if it's a new file)
5486 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5487 self.ds_file.unlink([id]);
5492 if(this.node.attrs.blockui>0) {
5493 instance.web.blockUI();
5496 // TODO : unactivate send on wizard and form
5499 this.$('form.oe_form_binary_form').submit();
5500 this.$(".oe_fileupload").hide();
5501 // add file on data result
5505 'filename': filename,
5511 on_file_loaded: function (event, result) {
5512 var files = this.get('value');
5515 if(this.node.attrs.blockui>0) {
5516 instance.web.unblockUI();
5519 if (result.error || !result.id ) {
5520 this.do_warn( _t('Uploading Error'), result.error);
5521 delete this.data[0];
5523 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5524 delete this.data[0];
5525 this.data[result.id] = {
5527 'name': result.name,
5528 'filename': result.filename,
5529 'url': this.get_file_url(result)
5532 this.data[result.id] = {
5534 'name': result.name,
5535 'filename': result.filename,
5536 'url': this.get_file_url(result)
5539 var values = _.clone(this.get('value'));
5540 values.push(result.id);
5541 this.set({'value': values});
5543 this.render_value();
5545 on_file_delete: function (event) {
5546 event.stopPropagation();
5547 var file_id=$(event.target).data("id");
5549 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5550 if(!this.data[file_id].no_unlink) {
5551 this.ds_file.unlink([file_id]);
5553 this.set({'value': files});
5558 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5559 template: "FieldStatus",
5560 init: function(field_manager, node) {
5561 this._super(field_manager, node);
5562 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5563 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5564 this.set({value: false});
5565 this.selection = {'unfolded': [], 'folded': []};
5566 this.set("selection", {'unfolded': [], 'folded': []});
5567 this.selection_dm = new instance.web.DropMisordered();
5568 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5571 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5573 this.on("change:value", this, this.get_selection);
5574 this.on("change:evaluated_selection_domain", this, this.get_selection);
5575 this.on("change:selection", this, function() {
5576 this.selection = this.get("selection");
5577 this.render_value();
5579 this.get_selection();
5580 if (this.options.clickable) {
5581 this.$el.on('click','li[data-id]',this.on_click_stage);
5583 if (this.$el.parent().is('header')) {
5584 this.$el.after('<div class="oe_clear"/>');
5588 set_value: function(value_) {
5589 if (value_ instanceof Array) {
5592 this._super(value_);
5594 render_value: function() {
5596 var content = QWeb.render("FieldStatus.content", {
5598 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5600 self.$el.html(content);
5602 calc_domain: function() {
5603 var d = instance.web.pyeval.eval('domain', this.build_domain());
5604 var domain = []; //if there is no domain defined, fetch all the records
5607 domain = ['|',['id', '=', this.get('value')]].concat(d);
5610 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5611 this.set("evaluated_selection_domain", domain);
5614 /** Get the selection and render it
5615 * selection: [[identifier, value_to_display], ...]
5616 * For selection fields: this is directly given by this.field.selection
5617 * For many2one fields: perform a search on the relation of the many2one field
5619 get_selection: function() {
5621 var selection_unfolded = [];
5622 var selection_folded = [];
5623 var fold_field = this.options.fold_field;
5625 var calculation = _.bind(function() {
5626 if (this.field.type == "many2one") {
5627 return self.get_distant_fields().then(function (fields) {
5628 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5629 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5630 var ids = _.pluck(records, 'id');
5631 return self.dataset.name_get(ids).then(function (records_name) {
5632 _.each(records, function (record) {
5633 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5634 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5635 selection_folded.push([record.id, name]);
5637 selection_unfolded.push([record.id, name]);
5644 // For field type selection filter values according to
5645 // statusbar_visible attribute of the field. For example:
5646 // statusbar_visible="draft,open".
5647 var select = this.field.selection;
5648 for(var i=0; i < select.length; i++) {
5649 var key = select[i][0];
5650 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5651 selection_unfolded.push(select[i]);
5657 this.selection_dm.add(calculation()).then(function () {
5658 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5659 if (! _.isEqual(selection, self.get("selection"))) {
5660 self.set("selection", selection);
5665 * :deprecated: this feature will probably be removed with OpenERP v8
5667 get_distant_fields: function() {
5669 if (! this.options.fold_field) {
5670 this.distant_fields = {}
5672 if (this.distant_fields) {
5673 return $.when(this.distant_fields);
5675 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
5676 self.distant_fields = fields;
5680 on_click_stage: function (ev) {
5682 var $li = $(ev.currentTarget);
5684 if (this.field.type == "many2one") {
5685 val = parseInt($li.data("id"), 10);
5688 val = $li.data("id");
5690 if (val != self.get('value')) {
5691 this.view.recursive_save().done(function() {
5693 change[self.name] = val;
5694 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5702 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5703 template: "FieldMonetary",
5704 widget_class: 'oe_form_field_float oe_form_field_monetary',
5706 this._super.apply(this, arguments);
5707 this.set({"currency": false});
5708 if (this.options.currency_field) {
5709 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5710 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5713 this.on("change:currency", this, this.get_currency_info);
5714 this.get_currency_info();
5715 this.ci_dm = new instance.web.DropMisordered();
5718 var tmp = this._super();
5719 this.on("change:currency_info", this, this.reinitialize);
5722 get_currency_info: function() {
5724 if (this.get("currency") === false) {
5725 this.set({"currency_info": null});
5728 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5729 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5730 self.set({"currency_info": res});
5733 parse_value: function(val, def) {
5734 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5736 format_value: function(val, def) {
5737 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5742 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
5743 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
5744 will be added to the relation.
5746 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5747 className: "oe_form_many2many_checkboxes",
5749 this._super.apply(this, arguments);
5750 this.set("value", {});
5751 this.set("records", []);
5752 this.field_manager.on("view_content_has_changed", this, function() {
5753 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
5754 if (! _.isEqual(domain, this.get("domain"))) {
5755 this.set("domain", domain);
5758 this.records_orderer = new instance.web.DropMisordered();
5760 initialize_field: function() {
5761 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
5762 this.on("change:domain", this, this.query_records);
5763 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
5764 this.on("change:records", this, this.render_value);
5766 query_records: function() {
5768 var model = new openerp.Model(openerp.session, this.field.relation);
5769 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
5770 return model.call("name_get", [record_ids] , {"context": self.build_context()});
5771 })).then(function(res) {
5772 self.set("records", res);
5775 render_value: function() {
5776 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
5777 var inputs = this.$("input");
5778 inputs.change(_.bind(this.from_dom, this));
5779 if (this.get("effective_readonly"))
5780 inputs.attr("disabled", "true");
5782 from_dom: function() {
5784 this.$("input").each(function() {
5786 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
5788 if (! _.isEqual(new_value, this.get("value")))
5789 this.internal_set_value(new_value);
5791 set_value: function(value) {
5792 value = value || [];
5793 if (value.length >= 1 && value[0] instanceof Array) {
5794 value = value[0][2];
5797 _.each(value, function(el) {
5798 formatted[JSON.stringify(el)] = true;
5800 this._super(formatted);
5802 get_value: function() {
5803 var value = _.filter(_.keys(this.get("value")), function(el) {
5804 return this.get("value")[el];
5806 value = _.map(value, function(el) {
5807 return JSON.parse(el);
5809 return [commands.replace_with(value)];
5814 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
5815 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
5816 action on the model of the relation and show only the linked records.
5820 * 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
5821 to display (or False to take the default one) and the second element is the type of the view. Defaults to
5822 [[false, "tree"], [false, "form"]] .
5824 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5825 className: "oe_form_x2many_counter",
5827 this._super.apply(this, arguments);
5828 this.set("value", []);
5829 _.defaults(this.options, {
5830 "views": [[false, "tree"], [false, "form"]],
5833 render_value: function() {
5834 var text = _.str.sprintf("%d %s", this.val().length, this.string);
5835 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
5836 this.$("a").click(_.bind(this.go_to, this));
5839 return this.view.recursive_save().then(_.bind(function() {
5840 var val = this.val();
5842 if (this.field.type === "one2many") {
5843 context["default_" + this.field.relation_field] = this.view.datarecord.id;
5845 var domain = [["id", "in", val]];
5846 return this.do_action({
5847 type: 'ir.actions.act_window',
5849 res_model: this.field.relation,
5850 views: this.options.views,
5858 var value = this.get("value") || [];
5859 if (value.length >= 1 && value[0] instanceof Array) {
5860 value = value[0][2];
5867 * Registry of form fields, called by :js:`instance.web.FormView`.
5869 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5870 * will substitute to the <field> tags as defined in OpenERP's views.
5872 instance.web.form.widgets = new instance.web.Registry({
5873 'char' : 'instance.web.form.FieldChar',
5874 'id' : 'instance.web.form.FieldID',
5875 'email' : 'instance.web.form.FieldEmail',
5876 'url' : 'instance.web.form.FieldUrl',
5877 'text' : 'instance.web.form.FieldText',
5878 'html' : 'instance.web.form.FieldTextHtml',
5879 'date' : 'instance.web.form.FieldDate',
5880 'datetime' : 'instance.web.form.FieldDatetime',
5881 'selection' : 'instance.web.form.FieldSelection',
5882 'radio' : 'instance.web.form.FieldRadio',
5883 'many2one' : 'instance.web.form.FieldMany2One',
5884 'many2onebutton' : 'instance.web.form.Many2OneButton',
5885 'many2many' : 'instance.web.form.FieldMany2Many',
5886 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5887 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5888 'one2many' : 'instance.web.form.FieldOne2Many',
5889 'one2many_list' : 'instance.web.form.FieldOne2Many',
5890 'reference' : 'instance.web.form.FieldReference',
5891 'boolean' : 'instance.web.form.FieldBoolean',
5892 'float' : 'instance.web.form.FieldFloat',
5893 'integer': 'instance.web.form.FieldFloat',
5894 'float_time': 'instance.web.form.FieldFloat',
5895 'progressbar': 'instance.web.form.FieldProgressBar',
5896 'image': 'instance.web.form.FieldBinaryImage',
5897 'binary': 'instance.web.form.FieldBinaryFile',
5898 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5899 'statusbar': 'instance.web.form.FieldStatus',
5900 'monetary': 'instance.web.form.FieldMonetary',
5901 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
5902 'x2many_counter': 'instance.web.form.X2ManyCounter',
5906 * Registry of widgets usable in the form view that can substitute to any possible
5907 * tags defined in OpenERP's form views.
5909 * Every referenced class should extend FormWidget.
5911 instance.web.form.tags = new instance.web.Registry({
5912 'button' : 'instance.web.form.WidgetButton',
5915 instance.web.form.custom_widgets = new instance.web.Registry({
5920 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: